# Copyright (c) 2017 Orange.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import sqlalchemy as sa

from oslo_config import cfg
from oslo_log import helpers as log_helpers
from oslo_log import log as logging

from neutron_lib import context as n_context
from neutron_lib.db import api as db_api
from neutron_lib.db import model_base


LOG = logging.getLogger(__name__)

sfc_bagpipe_opts = [
    cfg.IntOpt('as_number', default=64512,
               help=_("Autonomous System number used to generate BGP Route "
                      "Targets that will be used for Port Chain allocations")),
    cfg.ListOpt('rtnn',
                default=[5000, 5999],
                help=_("List containing <rtnn_min>, <rtnn_max> "
                       "defining a range of BGP Route Targets that will "
                       "be used for Port Chain allocations. This range MUST "
                       "not intersect the one used for network segmentation "
                       "identifiers")),
]

cfg.CONF.register_opts(sfc_bagpipe_opts, "sfc_bagpipe")


class BaGPipePpgRTAssoc(model_base.BASEV2, model_base.HasId):
    '''Mapping from Port Pair Group to the NN part of an AS:NN route target'''

    __tablename__ = 'sfc_bagpipe_ppg_rtnn_associations'
    ppg_id = sa.Column(sa.String(36), primary_key=True)
    rtnn = sa.Column(sa.Integer, unique=True, nullable=False)
    is_redirect = sa.Column(sa.Boolean(), nullable=False)
    reverse = sa.Column(sa.Boolean(), nullable=False)


def singleton(class_):
    instances = {}

    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return getinstance


@singleton
class RTAllocator(object):
    def __init__(self):
        self.config = cfg.CONF.sfc_bagpipe
        self.ctx = n_context.get_admin_context()
        self.session = self.ctx.session

    def _get_rt_from_rtnn(self, rtnn):
        return ':'.join([str(self.config.as_number), str(rtnn)])

    @log_helpers.log_method_call
    def allocate_rt(self, ppg_id, is_redirect=False, reverse=False):
        ctx = n_context.get_admin_context()
        with db_api.CONTEXT_READER.using(ctx):
            query = ctx.session.query(
                BaGPipePpgRTAssoc).order_by(
                BaGPipePpgRTAssoc.rtnn)

            allocated_rtnns = {obj.rtnn for obj in query.all()}

        # Find first one available in range
        start, end = int(self.config.rtnn[0]), int(self.config.rtnn[1]) + 1
        for rtnn in range(start, end):
            if rtnn not in allocated_rtnns:
                with db_api.CONTEXT_WRITER.using(ctx):
                    ppg_rtnn = BaGPipePpgRTAssoc(ppg_id=ppg_id,
                                                 rtnn=rtnn,
                                                 is_redirect=is_redirect,
                                                 reverse=reverse)
                    ctx.session.add(ppg_rtnn)

                return self._get_rt_from_rtnn(rtnn)

        LOG.error("Can't allocate route target, all range in use")
        return None

    @log_helpers.log_method_call
    def get_rts_by_ppg(self, ppg_id):
        ctx = n_context.get_admin_context()
        with db_api.CONTEXT_READER.using(ctx):
            ppg_rts = ctx.session.query(
                BaGPipePpgRTAssoc).filter_by(ppg_id=ppg_id).all()
            if not ppg_rts:
                return None

        return [ppg_rt.rtnn for ppg_rt in ppg_rts]

    @log_helpers.log_method_call
    def get_redirect_rt_by_ppg(self, ppg_id, reverse=False):
        ctx = n_context.get_admin_context()
        with db_api.CONTEXT_READER.using(ctx):
            ppg_redirect_rt = ctx.session.query(
                BaGPipePpgRTAssoc).filter_by(ppg_id=ppg_id,
                                             is_redirect=True,
                                             reverse=reverse).one()

            return self._get_rt_from_rtnn(ppg_redirect_rt.rtnn)

    @log_helpers.log_method_call
    def release_rt(self, rtnn):
        ctx = n_context.get_admin_context()
        with db_api.CONTEXT_WRITER.using(ctx):
            ppg_rtnn = ctx.session.query(
                BaGPipePpgRTAssoc).filter_by(rtnn=rtnn).first()

            if ppg_rtnn:
                ctx.session.delete(ppg_rtnn)
            else:
                LOG.warning("Can't release route target %s, not used", rtnn)


class BaGPipeChainHop(model_base.BASEV2, model_base.HasId,
                      model_base.HasProject):
    __tablename__ = 'sfc_bagpipe_chain_hops'
    ingress_ppg = sa.Column(sa.String(36),
                            nullable=True)
    egress_ppg = sa.Column(sa.String(36),
                           nullable=True)
    ingress_network = sa.Column(sa.String(36),
                                sa.ForeignKey('networks.id'),
                                nullable=True)
    egress_network = sa.Column(sa.String(36),
                               sa.ForeignKey('networks.id'),
                               nullable=True)
    rts = sa.Column(sa.String(255))
    ingress_gw = sa.Column(sa.String(64))
    egress_gw = sa.Column(sa.String(64))
    readv_from_rts = sa.Column(sa.String(255))
    readv_to_rt = sa.Column(sa.String(255))
    attract_to_rt = sa.Column(sa.String(255))
    redirect_rts = sa.Column(sa.String(255))
    classifiers = sa.Column(sa.String(255))
    reverse_hop = sa.Column(sa.Boolean(), nullable=False)
    portchain_id = sa.Column(sa.String(36),
                             nullable=False,
                             primary_key=True)


class BaGPipeSfcDriverDB(object):

    def initialize(self):
        self.admin_context = n_context.get_admin_context()
