#    Copyright 2019, A10 Networks
#
#    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 datetime

from oslo_config import cfg
from oslo_log import log as logging
from sqlalchemy.orm import noload
from sqlalchemy import or_
from sqlalchemy import and_

from octavia.common import constants as consts
from octavia.db import models as base_models
from octavia.db import repositories as repo

from a10_octavia.db import models

CONF = cfg.CONF

LOG = logging.getLogger(__name__)


class BaseRepository(object):
    model_class = None

    def count(self, session, **filters):
        """Retrieves a count of entities from the database.

        :param session: A Sql Alchemy database session.
        :param filters: Filters to decide which entities should be retrieved.
        :returns: int
        """
        return session.query(self.model_class).filter_by(**filters).count()

    def create(self, session, **model_kwargs):
        """Base create method for a database entity.

        :param session: A Sql Alchemy database session.
        :param model_kwargs: Attributes of the model to insert.
        :returns: octavia.common.data_model
        """
        with session.begin(subtransactions=True):
            model = self.model_class(**model_kwargs)
            session.add(model)
        return model.to_data_model()

    def delete(self, session, **filters):
        """Deletes entities from the database.

        :param session: A Sql Alchemy database session.
        :param filters: Filters to decide which entity should be deleted.
        :returns: None
        :raises: sqlalchemy.orm.exc.NoResultFound
        """
        models = session.query(self.model_class).filter_by(**filters).all()
        for model in models:
            with session.begin(subtransactions=True):
                session.delete(model)
                session.flush()

    def delete_batch(self, session, ids=None):
        """Batch deletes by entity ids."""
        ids = ids or []
        for id in ids:
            self.delete(session, id=id)

    def update(self, session, id, **model_kwargs):
        """Updates an entity in the database.

        :param session: A Sql Alchemy database session.
        :param model_kwargs: Entity attributes that should be updates.
        :returns: octavia.common.data_model
        """
        with session.begin(subtransactions=True):
            session.query(self.model_class).filter_by(
                id=id).update(model_kwargs)

    def get(self, session, **filters):
        """Retrieves an entity from the database.

        :param session: A Sql Alchemy database session.
        :param filters: Filters to decide which entity should be retrieved.
        :returns: octavia.common.data_model
        """
        deleted = filters.pop('show_deleted', True)
        model = session.query(self.model_class).filter_by(**filters)

        if not deleted:
            if hasattr(self.model_class, 'status'):
                model = model.filter(
                    self.model_class.status != consts.DELETED)
            else:
                model = model.filter(
                    self.model_class.provisioning_status != consts.DELETED)

        model = model.first()

        if not model:
            return None

        return model.to_data_model()

    def get_all(self, session, pagination_helper=None,
                query_options=None, **filters):
        """Retrieves a list of entities from the database.

        :param session: A Sql Alchemy database session.
        :param pagination_helper: Helper to apply pagination and sorting.
        :param query_options: Optional query options to apply.
        :param filters: Filters to decide which entities should be retrieved.
        :returns: [octavia.common.data_model]
        """
        deleted = filters.pop('show_deleted', True)
        query = session.query(self.model_class).filter_by(**filters)
        if query_options:
            query = query.options(query_options)

        if not deleted:
            if hasattr(self.model_class, 'status'):
                query = query.filter(
                    self.model_class.status != consts.DELETED)
            else:
                query = query.filter(
                    self.model_class.provisioning_status != consts.DELETED)

        if pagination_helper:
            model_list, links = pagination_helper.apply(
                query, self.model_class)
        else:
            links = None
            model_list = query.all()

        data_model_list = [model.to_data_model() for model in model_list]
        return data_model_list, links

    def exists(self, session, id):
        """Determines whether an entity exists in the database by its id.

        :param session: A Sql Alchemy database session.
        :param id: id of entity to check for existence.
        :returns: octavia.common.data_model
        """
        return bool(session.query(self.model_class).filter_by(id=id).first())

    def get_all_deleted_expiring(self, session, exp_age):
        """Get all previously deleted resources that are now expiring.

        :param session: A Sql Alchemy database session.
        :param exp_age: A standard datetime delta which is used to see for how
                        long can a resource live without updates before
                        it is considered expired
        :returns: A list of resource IDs
                """

        expiry_time = datetime.datetime.utcnow() - exp_age

        query = session.query(self.model_class).filter(
            self.model_class.updated_at < expiry_time)
        if hasattr(self.model_class, 'status'):
            query = query.filter_by(status=consts.DELETED)
        else:
            query = query.filter_by(provisioning_status=consts.DELETED)
        # Do not load any relationship
        query = query.options(noload('*'))
        model_list = query.all()

        id_list = [model.id for model in model_list]
        return id_list


class VThunderRepository(BaseRepository):
    model_class = models.VThunder

    def get_recently_updated_thunders(self, session):
        query = session.query(self.model_class).filter(
            or_(self.model_class.updated_at >= self.model_class.last_write_mem,
                self.model_class.last_write_mem == None)).filter(
            or_(self.model_class.role == "STANDALONE",
                self.model_class.role == "MASTER")).filter(
            or_(self.model_class.status == 'ACTIVE', self.model_class.status == 'DELETED'))
        query = query.options(noload('*'))
        return query.all()

    def get_stale_vthunders(self, session, initial_setup_wait_time, failover_wait_time):
        model = session.query(self.model_class).filter(
            self.model_class.created_at < initial_setup_wait_time).filter(
            self.model_class.last_udp_update < failover_wait_time).filter(
            or_(self.model_class.status == 'ACTIVE',
                self.model_class.status == 'READY')).filter(
            self.model_class.health_state == 'UP').first()
        if model is None:
            return None
        return model.to_data_model()

    def get_compute_vthunders(self, session, compute_id):
        vthunder_list = []
        query = session.query(self.model_class).filter(
            self.model_class.compute_id == compute_id)
        model_list = query.all()
        for data in model_list:
            vthunder_list.append(data.to_data_model())
        return vthunder_list

    def set_vthunder_health_state(self, session, id, new_state):
        self.update(session, id, health_state=new_state)

    def unset_vthunder_busy_health_state(self, session):
        with session.begin(subtransactions=True):
            session.query(self.model_class).filter_by(
                health_state='BUSY').update({"health_state": 'UP'})

    def get_vthunder_from_lb(self, session, lb_id):
        model = session.query(self.model_class).filter(
            self.model_class.loadbalancer_id == lb_id).filter(
            or_(self.model_class.role == "STANDALONE",
                self.model_class.role == "MASTER")).first()

        if not model:
            return None

        return model.to_data_model()

    def get_backup_vthunder_from_lb(self, session, lb_id):
        model = session.query(self.model_class).filter(
            self.model_class.loadbalancer_id == lb_id).filter(
            or_(self.model_class.role == "STANDALONE",
                self.model_class.role == "BACKUP")).first()

        if not model:
            return None

        return model.to_data_model()

    def get_health_vthunder_count_for_lb(self, session, lb_id):
        count = session.query(self.model_class).filter(
            self.model_class.loadbalancer_id == lb_id).filter(
            self.model_class.health_state == "UP").count()
        return count

    def get_vthunder_by_project_id(self, session, project_id):
        model = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.status == "ACTIVE",
                 or_(self.model_class.role == "STANDALONE",
                     self.model_class.role == "MASTER"))).first()

        if not model:
            return None

        return model.to_data_model()

    def get_vthunder_with_different_topology(self, session, project_id, topology):
        model = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.status == "ACTIVE",
                 or_(self.model_class.role == "STANDALONE",
                     self.model_class.role == "MASTER"))).filter(
                self.model_class.compute_id != None).filter(
                self.model_class.topology != topology).first()

        if not model:
            return None

        return model.to_data_model()

    def get_vthunders_by_project_id(self, session, project_id):
        model_list = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.status == "ACTIVE",
                 or_(self.model_class.role == "STANDALONE",
                     self.model_class.role == "MASTER")))
        id_list = [model.id for model in model_list]
        return id_list

    def get_vthunders_by_ip_address(self, session, ip_address, vthunders=False):
        model_list = session.query(self.model_class).filter(
            self.model_class.ip_address == ip_address).filter(
            and_(self.model_class.status == "ACTIVE",
                 or_(self.model_class.role == "STANDALONE",
                     self.model_class.role == "MASTER")))

        if vthunders == False:
            id_list = [model.id for model in model_list]
            return id_list
        else:
            model_list = model_list.options(noload('*'))
            return model_list.all()

    def get_all_vthunder_by_address(self, session, ip_address):
        model_list = session.query(self.model_class).filter(
            self.model_class.ip_address == ip_address).filter(
            self.model_class.status == "ACTIVE")
        model_list = model_list.options(noload('*'))
        return model_list.all()

    def get_delete_compute_flag(self, session, compute_id):
        if compute_id:
            count = session.query(self.model_class).filter(
                and_(self.model_class.compute_id == compute_id,
                     self.model_class.status != "DELETED")).count()
            if count < 2:
                return True

            else:
                return False
        else:
            return False

    def get_vthunder_from_src_addr(self, session, srcaddr):
        model = session.query(self.model_class).filter(
            self.model_class.status != "DELETED").filter(
            self.model_class.ip_address == srcaddr).first()

        if not model:
            return None
        return model.id

    def get_spare_vthunder(self, session):
        model = session.query(self.model_class).filter(
            self.model_class.status == "READY").first()

        if not model:
            return None

        return model.to_data_model()

    def set_spare_vthunder_status(self, session, id, new_status):
        self.update(session, id, status=new_status)

    def get_spare_vthunder_count(self, session):
        with session.begin(subtransactions=True):
            count = session.query(self.model_class).filter_by(
                status="READY", loadbalancer_id=None).count()

        return count

    def get_busy_spare_vthunder_count(self, session):
        with session.begin(subtransactions=True):
            count = session.query(self.model_class).filter_by(
                status="BUSY", loadbalancer_id=None).count()

        return count

    def get_all_deleted_expiring(self, session, exp_age):

        expiry_time = datetime.datetime.utcnow() - exp_age

        query = session.query(self.model_class).filter(
            self.model_class.updated_at < expiry_time)
        if hasattr(self.model_class, 'status'):
            query = query.filter(or_(self.model_class.status == "USED_SPARE",
                                     self.model_class.status == consts.DELETED))
        else:
            query = query.filter_by(operating_status=consts.DELETED)
        # Do not load any relationship
        query = query.options(noload('*'))
        model_list = query.all()

        id_list = [model.id for model in model_list]
        return id_list

    def get_project_list_using_partition(self, session, partition_name, ip_address):
        queryset_vthunders = session.query(self.model_class.project_id.distinct()).filter(
            and_(self.model_class.partition_name == partition_name,
                 self.model_class.ip_address == ip_address,
                 or_(self.model_class.role == "STANDALONE",
                     self.model_class.role == "MASTER",
                     self.model_class.role == "BACKUP")))
        list_projects = [project[0] for project in queryset_vthunders]
        return list_projects

    def update_last_write_mem(self, session, ip_address, partition, **model_kwargs):
        with session.begin(subtransactions=True):
            session.query(self.model_class).filter_by(
                    ip_address=ip_address, partition_name=partition).update(model_kwargs)

    def get_vthunder_by_project_id_and_role(self, session, project_id, role):
        model = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.status == "ACTIVE",
                 self.model_class.role == role)).first()

        if not model:
            return None

        return model.to_data_model()

    def get_vthunders_by_project_id_and_role(self, session, project_id, role):
        model_list = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.status == "ACTIVE",
                 self.model_class.role == role))

        id_list = [model.id for model in model_list]
        return id_list

    def get_rack_vthunders_by_ip_address(self, session, ip_address):
        model_list = session.query(self.model_class).filter(
            self.model_class.ip_address == ip_address).filter(
            and_(self.model_class.role == "MASTER",
                 or_(self.model_class.status == "ACTIVE",
                     self.model_class.status == "PENDING_DELETE")))

        id_list = [model.id for model in model_list]
        return id_list

    def get_lb_count_vthunder_partition(self, session, ip_address, partition):
        status_list = ["ACTIVE", "PENDING_UPDATE"]
        return session.query(self.model_class).filter(self.model_class.ip_address == ip_address,
                                                    self.model_class.partition_name == partition,
                                                    self.model_class.status.in_(status_list)).count()


class LoadBalancerRepository(repo.LoadBalancerRepository):
    thunder_model_class = models.VThunder

    def get_active_lbs_by_thunder(self, session, vthunder):
        lb_list = []
        query = session.query(self.model_class).filter(
            and_(vthunder.loadbalancer_id == self.model_class.id,
                 self.model_class.provisioning_status == consts.ACTIVE))
        model_list = query.all()
        for data in model_list:
            lb_list.append(data.to_data_model())
        return lb_list

    def get_lb_count_by_subnet(self, session, project_ids, subnet_id):
        return session.query(self.model_class).join(base_models.Vip).filter(
            and_(self.model_class.project_id.in_(project_ids),
                 base_models.Vip.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE,
                     self.model_class.provisioning_status == consts.PENDING_UPDATE))).count()

    def get_lb_count_by_flavor(self, session, project_ids, flavor_id):
        return session.query(self.model_class).filter(
            self.model_class.project_id.in_(project_ids)).filter(
                self.model_class.flavor_id == flavor_id,
                or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                    self.model_class.provisioning_status == consts.ACTIVE)).count()

    def get_lbs_by_subnet(self,session, project_ids, subnet_id):
        lb_list = []
        query = session.query(self.model_class).join(base_models.Vip).filter(
            and_(self.model_class.project_id.in_(project_ids),
                 base_models.Vip.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE,
                     self.model_class.provisioning_status == consts.PENDING_UPDATE)))
        model_list = query.all()
        for data in model_list:
            lb_list.append(data.to_data_model())
        return lb_list

    def check_lb_exists_in_project(self, session, project_id):
        lb = session.query(self.model_class).join(base_models.Vip).filter(
            and_(self.model_class.project_id == project_id,
                 self.model_class.provisioning_status == consts.ACTIVE)).count()
        if lb == 0:
            return False
        else:
            return True

    def get_lbs_by_project_id(self, session, project_id):
        lb_list = []
        query = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            or_(self.model_class.provisioning_status == "ACTIVE",
                self.model_class.provisioning_status == "PENDING_UPDATE",
                self.model_class.provisioning_status == "PENDING_CREATE"))

        model_list = query.all()
        for data in model_list:
            lb_list.append(data.to_data_model())
        return lb_list

    def get_all_other_lbs_in_project(self, session, project_id, id):
        lb_list = []
        query = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.id != id,
                 self.model_class.provisioning_status != "ERROR",
                 self.model_class.provisioning_status != "DELETED"))

        model_list = query.all()
        for data in model_list:
            lb_list.append(data.to_data_model())
        return lb_list

    def get_lb_excluding_deleted(self, session, lb_id):
        query = session.query(self.model_class).filter(
            and_(self.model_class.id == lb_id,
                 self.model_class.provisioning_status != "DELETED"))
        model = query.first()
        if model is None:
            return None
        return model.to_data_model()

    def get_lbs_on_thunder_by_subnet(self, session, loadbalancer_id, subnet_id):
        model = session.query(self.model_class).join(base_models.Vip).filter(
            and_(self.model_class.id == loadbalancer_id,
                 base_models.Vip.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE,
                     self.model_class.provisioning_status == consts.PENDING_UPDATE))).first()
        if not model:
            return None
        return model.to_data_model()


class VRIDRepository(BaseRepository):
    model_class = models.VRID

    # A project can have multiple VRIDs, so need to convert each vrid object through
    # "to_data_model"
    def get_vrid_from_owner(self, session, owner, project_ids):
        vrid_obj_list = []

        model = session.query(self.model_class).filter(
            or_(self.model_class.owner.in_(owner),
                self.model_class.owner.in_(project_ids)))
        for data in model:
            vrid_obj_list.append(data.to_data_model())

        return vrid_obj_list


class MemberRepository(repo.MemberRepository):

    def get_member_count(self, session, project_id):
        count = session.query(self.model_class).filter(
            and_(self.model_class.project_id == project_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()
        return count

    def get_member_count_by_subnet(self, session, project_ids, subnet_id):
        return session.query(self.model_class).filter(
            and_(self.model_class.project_id.in_(project_ids),
                 self.model_class.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_member_count_by_ip_address(self, session, ip_address, project_id):
        return session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.ip_address == ip_address,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_member_count_by_ip_address_port_protocol(self, session, ip_address, project_id, port,
                                                     protocol):
        return session.query(self.model_class).join(base_models.Pool).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.ip_address == ip_address,
                 self.model_class.protocol_port == port,
                 base_models.Pool.protocol == protocol,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_pool_count_by_ip(self, session, ip_address, project_id):
        return session.query(self.model_class.pool_id.distinct()).filter(
            self.model_class.project_id == project_id).filter(
            and_(self.model_class.ip_address == ip_address,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_pool_count_by_ip_on_thunder(self, session, ip_address, pool_ids):
        return session.query(self.model_class.pool_id.distinct()).filter(
            self.model_class.pool_id.in_(pool_ids)).filter(
            and_(self.model_class.ip_address == ip_address,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_pool_count_subnet(self, session, project_ids, subnet_id):
        return session.query(self.model_class.pool_id.distinct()).filter(
            self.model_class.project_id.in_(project_ids)).filter(
            and_(self.model_class.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_pool_count_subnet_on_thunder(self, session, pool_ids, subnet_id):
        return session.query(self.model_class.pool_id.distinct()).filter(
            self.model_class.pool_id.in_(pool_ids)).filter(
            and_(self.model_class.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).count()

    def get_members_on_thunder_by_subnet(self, session, ip_address, subnet_id):
        model = session.query(self.model_class).filter(
            and_(self.model_class.ip_address == ip_address,
                 self.model_class.subnet_id == subnet_id,
                 or_(self.model_class.provisioning_status == consts.PENDING_DELETE,
                     self.model_class.provisioning_status == consts.ACTIVE))).first()

        if not model:
            return None
        return model.to_data_model()

    def get_members_by_project_id(self, session, project_id):
        member_list = []
        query = session.query(self.model_class).filter(
            self.model_class.project_id == project_id).filter(
            or_(self.model_class.provisioning_status == "ACTIVE",
                self.model_class.provisioning_status == "PENDING_UPDATE",
                self.model_class.provisioning_status == "PENDING_CREATE"))

        model_list = query.all()
        for data in model_list:
            member_list.append(data.to_data_model())
        return member_list


class NatPoolRepository(BaseRepository):
    model_class = models.NATPool


class VrrpSetRepository(BaseRepository):
    model_class = models.VrrpSet

    def get_subnet_set_ids(self, session, subnet):
        query = session.query(self.model_class).filter(
            self.model_class.mgmt_subnet == subnet)
        query = query.options(noload('*'))
        model_list = query.all()
        set_id_list = [model.set_id for model in model_list]
        return set_id_list

class ListenerStatisticsRepository(repo.ListenerStatisticsRepository):

    def delete_multiple(self, session, **filters):
        """Deletes entities from the database."""

        models = session.query(self.model_class).filter_by(**filters).all()
        for model in models:
            with session.begin(subtransactions=True):
                session.delete(model)
                session.flush()
