#    Copyright (c) 2014 Rackspace
#    Copyright (c) 2016 Blue Box, an IBM Company
#    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 datetime
import re
import typing as tp

from oslo_log import log as logging
from sqlalchemy.orm import collections

from octavia.common import constants

LOG = logging.getLogger(__name__)


class BaseDataModel:
    def _to_dict(self, value, calling_classes=None, recurse=False):
        calling_classes = calling_classes or []
        # We need to have json convertible data for storing it in
        # persistence jobboard backend.
        if isinstance(value, datetime.datetime):
            ret = value.isoformat()
        elif isinstance(value, bytes):
            ret = value.decode()
        elif recurse:
            if isinstance(value, list):
                ret = []
                for item in value:
                    if isinstance(item, BaseDataModel):
                        if type(self) not in calling_classes:
                            ret.append(
                                item.to_dict(calling_classes=(
                                    calling_classes + [type(self)]),
                                    recurse=recurse))
                        else:
                            # TODO(rm_work): Is the idea that if this list
                            #  contains ANY BaseDataModel, that all of them
                            #  are data models, and we may as well quit?
                            #  Or, were we supposed to append a `None` for
                            #  each one? I assume the former?
                            ret = None
                            break
                    else:
                        ret.append(
                            self._to_dict(item,
                                          calling_classes=calling_classes,
                                          recurse=recurse))
            elif isinstance(value, BaseDataModel):
                if type(self) not in calling_classes:
                    ret = value.to_dict(
                        calling_classes=calling_classes + [type(self)],
                        recurse=recurse)
                else:
                    ret = None
            else:
                ret = value
        else:
            if isinstance(value, BaseDataModel):
                ret = None
            elif isinstance(value, list):
                ret = []
            else:
                ret = value
        return ret

    def to_dict(self, calling_classes=None, recurse=False, **kwargs):
        """Converts a data model to a dictionary."""
        calling_classes = calling_classes or []
        ret = {}
        for attr, value in self.__dict__.items():
            if attr.startswith('_') or not kwargs.get(attr, True):
                continue
            if attr == 'tags':
                # tags is a list, it doesn't need recurse
                ret[attr] = value
                continue
            ret[attr] = self._to_dict(value, calling_classes=calling_classes,
                                      recurse=recurse)

        return ret

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.to_dict() == other.to_dict()
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    @classmethod
    def from_dict(cls, dict):
        return cls(**dict)

    @classmethod
    def _name(cls):
        """Returns class name in a more human readable form."""
        # Split the class name up by capitalized words
        return ' '.join(re.findall('[A-Z][^A-Z]*', cls.__name__))

    def _get_unique_key(self, obj=None):
        """Returns a unique key for passed object for data model building."""
        obj = obj or self
        # First handle all objects with their own ID, then handle subordinate
        # objects.
        if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer',
                                      'Listener', 'Amphora', 'L7Policy',
                                      'L7Rule']:
            return obj.__class__.__name__ + obj.id
        if obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']:
            return obj.__class__.__name__ + obj.pool_id
        if obj.__class__.__name__ in ['ListenerStatistics']:
            return obj.__class__.__name__ + obj.listener_id + obj.amphora_id
        if obj.__class__.__name__ in ['ListenerCidr']:
            return obj.__class__.__name__ + obj.listener_id + obj.cidr
        if obj.__class__.__name__ in ['VRRPGroup', 'Vip']:
            return obj.__class__.__name__ + obj.load_balancer_id
        if obj.__class__.__name__ in ['AmphoraHealth']:
            return obj.__class__.__name__ + obj.amphora_id
        if obj.__class__.__name__ in ['SNI']:
            return (obj.__class__.__name__ +
                    obj.listener_id + obj.tls_container_id)
        raise NotImplementedError

    def _find_in_graph(self, key, _visited_nodes=None):
        """Locates an object with the given unique key in the current

        object graph and returns a reference to it.
        """
        _visited_nodes = _visited_nodes or []
        mykey = self._get_unique_key()
        if mykey in _visited_nodes:
            # Seen this node already, don't traverse further
            return None
        if mykey == key:
            return self
        _visited_nodes.append(mykey)
        attr_names = [attr_name for attr_name in dir(self)
                      if not attr_name.startswith('_')]
        for attr_name in attr_names:
            attr = getattr(self, attr_name)
            if isinstance(attr, BaseDataModel):
                result = attr._find_in_graph(
                    key, _visited_nodes=_visited_nodes)
                if result is not None:
                    return result
            elif isinstance(attr, (collections.InstrumentedList, list)):
                for item in attr:
                    if isinstance(item, BaseDataModel):
                        result = item._find_in_graph(
                            key, _visited_nodes=_visited_nodes)
                        if result is not None:
                            return result
        # If we are here we didn't find it.
        return None

    def update(self, update_dict):
        """Generic update method which works for simple,

        non-relational attributes.
        """
        for key, value in update_dict.items():
            setattr(self, key, value)


class SessionPersistence(BaseDataModel):

    def __init__(self, pool_id=None, type=None, cookie_name=None,
                 pool=None, persistence_timeout=None,
                 persistence_granularity=None):
        self.pool_id = pool_id
        self.type = type
        self.cookie_name = cookie_name
        self.pool = pool
        self.persistence_timeout = persistence_timeout
        self.persistence_granularity = persistence_granularity

    def delete(self):
        self.pool.session_persistence = None


class ListenerStatistics(BaseDataModel):

    def __init__(self, listener_id=None, amphora_id=None, bytes_in=0,
                 bytes_out=0, active_connections=0,
                 total_connections=0, request_errors=0, received_time=0.0):
        self.listener_id = listener_id
        self.amphora_id = amphora_id
        self.bytes_in = bytes_in
        self.bytes_out = bytes_out
        self.active_connections = active_connections
        self.total_connections = total_connections
        self.request_errors = request_errors
        self.received_time = received_time

    def get_stats(self):
        stats = {
            'bytes_in': self.bytes_in,
            'bytes_out': self.bytes_out,
            'active_connections': self.active_connections,
            'total_connections': self.total_connections,
            'request_errors': self.request_errors,
        }
        return stats

    def db_fields(self):
        fields = self.to_dict()
        fields.pop('received_time')
        return fields

    def __iadd__(self, other):
        if isinstance(other, ListenerStatistics):
            self.bytes_in += other.bytes_in
            self.bytes_out += other.bytes_out
            self.request_errors += other.request_errors
            self.total_connections += other.total_connections
        else:
            raise TypeError(  # noqa: O342
                "unsupported operand type(s) for +=: '{}' and '{}'".format(
                    type(self), type(other)))

        return self


class LoadBalancerStatistics(BaseDataModel):

    def __init__(self, bytes_in=0, bytes_out=0, active_connections=0,
                 total_connections=0, request_errors=0, listeners=None):
        self.bytes_in = bytes_in
        self.bytes_out = bytes_out
        self.active_connections = active_connections
        self.total_connections = total_connections
        self.request_errors = request_errors
        self.listeners = listeners or []

    def get_stats(self):
        stats = {
            'bytes_in': self.bytes_in,
            'bytes_out': self.bytes_out,
            'active_connections': self.active_connections,
            'total_connections': self.total_connections,
            'request_errors': self.request_errors,
        }
        return stats


class HealthMonitor(BaseDataModel):

    def __init__(self, id=None, project_id=None, pool_id=None, type=None,
                 delay=None, timeout=None, fall_threshold=None,
                 rise_threshold=None, http_method=None, url_path=None,
                 expected_codes=None, enabled=None, pool=None, name=None,
                 provisioning_status=None, operating_status=None,
                 created_at=None, updated_at=None, tags=None,
                 http_version=None, domain_name=None):
        self.id = id
        self.project_id = project_id
        self.pool_id = pool_id
        self.type = type
        self.delay = delay
        self.timeout = timeout
        self.fall_threshold = fall_threshold
        self.rise_threshold = rise_threshold
        self.http_method = http_method
        self.url_path = url_path
        self.expected_codes = expected_codes
        self.enabled = enabled
        self.pool = pool
        self.provisioning_status = provisioning_status
        self.operating_status = operating_status
        self.name = name
        self.created_at = created_at
        self.updated_at = updated_at
        self.tags = tags
        self.http_version = http_version
        self.domain_name = domain_name

    def delete(self):
        self.pool.health_monitor = None


class Pool(BaseDataModel):
    def __init__(self, id=None, project_id=None, name=None, description=None,
                 protocol=None, lb_algorithm=None, enabled=None,
                 operating_status=None, members=None, health_monitor=None,
                 session_persistence=None, load_balancer_id=None,
                 load_balancer=None, listeners=None, l7policies=None,
                 created_at=None, updated_at=None, provisioning_status=None,
                 tags=None, tls_certificate_id=None,
                 ca_tls_certificate_id=None, crl_container_id=None,
                 tls_enabled=None, tls_ciphers=None, tls_versions=None,
                 alpn_protocols=None):
        self.id = id
        self.project_id = project_id
        self.name = name
        self.description = description
        self.load_balancer_id = load_balancer_id
        self.load_balancer = load_balancer
        self.protocol = protocol
        self.lb_algorithm = lb_algorithm
        self.enabled = enabled
        self.operating_status = operating_status
        self.members = members or []
        self.health_monitor = health_monitor
        self.session_persistence = session_persistence
        self.listeners = listeners or []
        self.l7policies = l7policies or []
        self.created_at = created_at
        self.updated_at = updated_at
        self.provisioning_status = provisioning_status
        self.tags = tags
        self.tls_certificate_id = tls_certificate_id
        self.ca_tls_certificate_id = ca_tls_certificate_id
        self.crl_container_id = crl_container_id
        self.tls_enabled = tls_enabled
        self.tls_ciphers = tls_ciphers
        self.tls_versions = tls_versions
        self.alpn_protocols = alpn_protocols

    def update(self, update_dict):
        for key, value in update_dict.items():
            if key == 'session_persistence':
                if value is None or value == {}:
                    if self.session_persistence is not None:
                        self.session_persistence.delete()
                elif self.session_persistence is not None:
                    self.session_persistence.update(value)
                else:
                    value.update({'pool_id': self.id})
                    self.session_persistence = SessionPersistence(**value)
            else:
                setattr(self, key, value)

    def delete(self):
        for listener in self.listeners:
            if listener.default_pool_id == self.id:
                listener.default_pool = None
                listener.default_pool_id = None
            for pool in listener.pools:
                if pool.id == self.id:
                    listener.pools.remove(pool)
                    break
        for pool in self.load_balancer.pools:
            if pool.id == self.id:
                try:
                    self.load_balancer.pools.remove(pool)
                except ValueError:
                    LOG.debug("Pool %s has already been removed from load "
                              "balancer pools list.", pool.id)
                break
        for l7policy in self.l7policies:
            if l7policy.redirect_pool_id == self.id:
                # Technically this should never happen, as we block deletion
                # of pools in use by L7Policies at the API. However, we should
                # probably keep this here in case the data model gets
                # manipulated in some other way in the future.
                l7policy.action = constants.L7POLICY_ACTION_REJECT
                l7policy.redirect_pool = None
                l7policy.redirect_pool_id = None


class Member(BaseDataModel):

    def __init__(self, id=None, project_id=None, pool_id=None, ip_address=None,
                 protocol_port=None, weight=None, backup=None, enabled=None,
                 subnet_id=None, operating_status=None, pool=None,
                 created_at=None, updated_at=None, provisioning_status=None,
                 name=None, monitor_address=None, monitor_port=None,
                 tags=None, vnic_type=None):
        self.id = id
        self.project_id = project_id
        self.pool_id = pool_id
        self.ip_address = ip_address
        self.protocol_port = protocol_port
        self.weight = weight
        self.backup = backup
        self.enabled = enabled
        self.subnet_id = subnet_id
        self.operating_status = operating_status
        self.pool = pool
        self.created_at = created_at
        self.updated_at = updated_at
        self.provisioning_status = provisioning_status
        self.name = name
        self.monitor_address = monitor_address
        self.monitor_port = monitor_port
        self.tags = tags
        self.vnic_type = vnic_type

    def delete(self):
        for mem in self.pool.members:
            if mem.id == self.id:
                self.pool.members.remove(mem)
                break


class Listener(BaseDataModel):

    def __init__(self, id=None, project_id=None, name=None, description=None,
                 default_pool_id=None, load_balancer_id=None, protocol=None,
                 protocol_port=None, connection_limit=None,
                 enabled=None, provisioning_status=None, operating_status=None,
                 tls_certificate_id=None, stats=None, default_pool=None,
                 load_balancer=None, sni_containers=None, peer_port=None,
                 l7policies=None, pools=None, insert_headers=None,
                 created_at=None, updated_at=None,
                 timeout_client_data=None, timeout_member_connect=None,
                 timeout_member_data=None, timeout_tcp_inspect=None,
                 tags=None, client_ca_tls_certificate_id=None,
                 client_authentication=None, client_crl_container_id=None,
                 allowed_cidrs=None, tls_ciphers=None, tls_versions=None,
                 alpn_protocols=None, hsts_max_age=None,
                 hsts_include_subdomains=None, hsts_preload=None):
        self.id = id
        self.project_id = project_id
        self.name = name
        self.description = description
        self.default_pool_id = default_pool_id
        self.load_balancer_id = load_balancer_id
        self.protocol = protocol
        self.protocol_port = protocol_port
        self.connection_limit = connection_limit
        self.enabled = enabled
        self.provisioning_status = provisioning_status
        self.operating_status = operating_status
        self.tls_certificate_id = tls_certificate_id
        self.stats = stats
        self.default_pool = default_pool
        self.load_balancer = load_balancer
        self.sni_containers = sni_containers or []
        self.peer_port = peer_port
        self.l7policies = l7policies or []
        self.insert_headers = insert_headers or {}
        self.pools = pools or []
        self.created_at = created_at
        self.updated_at = updated_at
        self.timeout_client_data = timeout_client_data
        self.timeout_member_connect = timeout_member_connect
        self.timeout_member_data = timeout_member_data
        self.timeout_tcp_inspect = timeout_tcp_inspect
        self.tags = tags
        self.client_ca_tls_certificate_id = client_ca_tls_certificate_id
        self.client_authentication = client_authentication
        self.client_crl_container_id = client_crl_container_id
        self.allowed_cidrs = allowed_cidrs or []
        self.tls_ciphers = tls_ciphers
        self.tls_versions = tls_versions
        self.alpn_protocols = alpn_protocols
        self.hsts_max_age: tp.Optional[int] = hsts_max_age
        self.hsts_include_subdomains: tp.Optional[bool] = (
            hsts_include_subdomains)
        self.hsts_preload: tp.Optional[bool] = hsts_preload

    def update(self, update_dict):
        for key, value in update_dict.items():
            setattr(self, key, value)
            if key == 'default_pool_id':
                if self.default_pool is not None:
                    l7_pool_ids = [p.redirect_pool_id for p in self.l7policies
                                   if p.redirect_pool_id is not None and
                                   p.l7rules and p.enabled is True]
                    old_pool = self.default_pool
                    if old_pool.id not in l7_pool_ids:
                        if old_pool in self.pools:
                            self.pools.remove(old_pool)
                        if self in old_pool.listeners:
                            old_pool.listeners.remove(self)
                if value is not None:
                    pool = self._find_in_graph('Pool' + value)
                    if pool not in self.pools:
                        self.pools.append(pool)
                    if self not in pool.listeners:
                        pool.listeners.append(self)
                else:
                    pool = None
                setattr(self, 'default_pool', pool)

    def delete(self):
        for listener in self.load_balancer.listeners:
            if listener.id == self.id:
                self.load_balancer.listeners.remove(listener)
                break
        for pool in self.pools:
            pool.listeners.remove(self)


class LoadBalancer(BaseDataModel):

    def __init__(self, id=None, project_id=None, name=None, description=None,
                 provisioning_status=None, operating_status=None, enabled=None,
                 topology=None, vip=None, listeners=None, amphorae=None,
                 pools=None, vrrp_group=None, server_group_id=None,
                 created_at=None, updated_at=None, provider=None, tags=None,
                 flavor_id=None, availability_zone=None,
                 additional_vips=None):

        self.id = id
        self.project_id = project_id
        self.name = name
        self.description = description
        self.provisioning_status = provisioning_status
        self.operating_status = operating_status
        self.enabled = enabled
        self.vip = vip
        self.vrrp_group = vrrp_group
        self.topology = topology
        self.listeners = listeners or []
        self.amphorae = amphorae or []
        self.pools = pools or []
        self.server_group_id = server_group_id
        self.created_at = created_at
        self.updated_at = updated_at
        self.provider = provider
        self.tags = tags or []
        self.flavor_id = flavor_id
        self.availability_zone = availability_zone
        self.additional_vips = additional_vips or []

    def update(self, update_dict):
        for key, value in update_dict.items():
            if key == 'vip':
                if self.vip is not None:
                    self.vip.update(value)
                else:
                    value.update({'load_balancer_id': self.id})
                    self.vip = Vip(**value)
            else:
                setattr(self, key, value)


class VRRPGroup(BaseDataModel):

    def __init__(self, load_balancer_id=None, vrrp_group_name=None,
                 vrrp_auth_type=None, vrrp_auth_pass=None, advert_int=None,
                 smtp_server=None, smtp_connect_timeout=None,
                 load_balancer=None):
        self.load_balancer_id = load_balancer_id
        self.vrrp_group_name = vrrp_group_name
        self.vrrp_auth_type = vrrp_auth_type
        self.vrrp_auth_pass = vrrp_auth_pass
        self.advert_int = advert_int
        self.load_balancer = load_balancer


class Vip(BaseDataModel):

    def __init__(self, load_balancer_id=None, ip_address=None,
                 subnet_id=None, network_id=None, port_id=None,
                 load_balancer=None, qos_policy_id=None, octavia_owned=None,
                 vnic_type=None, sg_ids=None):
        self.load_balancer_id = load_balancer_id
        self.ip_address = ip_address
        self.subnet_id = subnet_id
        self.network_id = network_id
        self.port_id = port_id
        self.load_balancer = load_balancer
        self.qos_policy_id = qos_policy_id
        self.octavia_owned = octavia_owned
        self.vnic_type = vnic_type
        self.sg_ids = sg_ids or []

    def to_dict(self, **kwargs):
        ret = super().to_dict(**kwargs)
        if kwargs.get('recurse') is not True:
            # NOTE(gthiemonge) we need to return the associated SG IDs but as
            # sg_ids is a list, they are only added with recurse=True, which
            # does a full recursion on the Vip, adding the associated
            # LoadBalancer, its Listeners, etc...
            # Adding it directly here avoids unnecessary recursion
            ret[constants.SG_IDS] = self.sg_ids
        return ret


class AdditionalVip(BaseDataModel):

    def __init__(self, load_balancer_id=None, ip_address=None, subnet_id=None,
                 network_id=None, port_id=None, load_balancer=None):
        self.load_balancer_id = load_balancer_id
        self.ip_address = ip_address
        self.subnet_id = subnet_id
        self.network_id = network_id
        self.port_id = port_id
        self.load_balancer = load_balancer


class SNI(BaseDataModel):

    def __init__(self, listener_id=None, position=None, listener=None,
                 tls_container_id=None):
        self.listener_id = listener_id
        self.position = position
        self.listener = listener
        self.tls_container_id = tls_container_id

    # SQLAlchemy kindly attaches the whole listener object so
    # let's keep this simple by overriding the to_dict for this
    # object. Otherwise we recurse down the "ghost" listener object.
    def to_dict(self, **kwargs):
        return {'tls_container_id': self.tls_container_id,
                'listener_id': self.listener_id,
                'position': self.position}


class TLSContainer(BaseDataModel):

    def __init__(self, id=None, primary_cn=None, certificate=None,
                 private_key=None, passphrase=None, intermediates=None):
        self.id = id
        self.primary_cn = primary_cn
        self.certificate = certificate
        self.private_key = private_key
        self.passphrase = passphrase
        self.intermediates = intermediates or []


class Amphora(BaseDataModel):

    def __init__(self, id=None, load_balancer_id=None, compute_id=None,
                 status=None, lb_network_ip=None, vrrp_ip=None,
                 ha_ip=None, vrrp_port_id=None, ha_port_id=None,
                 load_balancer=None, role=None, cert_expiration=None,
                 cert_busy=False, vrrp_interface=None, vrrp_id=None,
                 vrrp_priority=None, cached_zone=None, created_at=None,
                 updated_at=None, image_id=None, compute_flavor=None):
        self.id = id
        self.load_balancer_id = load_balancer_id
        self.compute_id = compute_id
        self.status = status
        self.lb_network_ip = lb_network_ip
        self.vrrp_ip = vrrp_ip
        self.ha_ip = ha_ip
        self.vrrp_port_id = vrrp_port_id
        self.ha_port_id = ha_port_id
        self.role = role
        self.vrrp_interface = vrrp_interface
        self.vrrp_id = vrrp_id
        self.vrrp_priority = vrrp_priority
        self.load_balancer = load_balancer
        self.cert_expiration = cert_expiration
        self.cert_busy = cert_busy
        self.cached_zone = cached_zone
        self.created_at = created_at
        self.updated_at = updated_at
        self.image_id = image_id
        self.compute_flavor = compute_flavor

    def delete(self):
        for amphora in self.load_balancer.amphorae:
            if amphora.id == self.id:
                self.load_balancer.amphorae.remove(amphora)
                break


class AmphoraHealth(BaseDataModel):

    def __init__(self, amphora_id=None, last_update=None, busy=False):
        self.amphora_id = amphora_id
        self.last_update = last_update
        self.busy = busy


class L7Rule(BaseDataModel):

    def __init__(self, id=None, l7policy_id=None, type=None, enabled=None,
                 compare_type=None, key=None, value=None, l7policy=None,
                 invert=False, provisioning_status=None, operating_status=None,
                 project_id=None, created_at=None, updated_at=None, tags=None):
        self.id = id
        self.l7policy_id = l7policy_id
        self.type = type
        self.compare_type = compare_type
        self.key = key
        self.value = value
        self.l7policy = l7policy
        self.invert = invert
        self.provisioning_status = provisioning_status
        self.operating_status = operating_status
        self.project_id = project_id
        self.created_at = created_at
        self.updated_at = updated_at
        self.enabled = enabled
        self.tags = tags

    def delete(self):
        if len(self.l7policy.l7rules) == 1:
            # l7policy should disappear from pool and listener lists. Since
            # we are operating only on the data model, we can fake this by
            # calling the policy's delete method.
            self.l7policy.delete()
        for r in self.l7policy.l7rules:
            if r.id == self.id:
                self.l7policy.l7rules.remove(r)
                break


class L7Policy(BaseDataModel):

    def __init__(self, id=None, name=None, description=None, listener_id=None,
                 action=None, redirect_pool_id=None, redirect_url=None,
                 position=None, listener=None, redirect_pool=None,
                 enabled=None, l7rules=None, provisioning_status=None,
                 operating_status=None, project_id=None, created_at=None,
                 updated_at=None, redirect_prefix=None, tags=None,
                 redirect_http_code=None):
        self.id = id
        self.name = name
        self.description = description
        self.listener_id = listener_id
        self.action = action
        self.redirect_pool_id = redirect_pool_id
        self.redirect_url = redirect_url
        self.position = position
        self.listener = listener
        self.redirect_pool = redirect_pool
        self.enabled = enabled
        self.l7rules = l7rules or []
        self.provisioning_status = provisioning_status
        self.operating_status = operating_status
        self.project_id = project_id
        self.created_at = created_at
        self.updated_at = updated_at
        self.redirect_prefix = redirect_prefix
        self.tags = tags
        self.redirect_http_code = redirect_http_code

    def _conditionally_remove_pool_links(self, pool):
        """Removes links to the given pool from parent objects.

        Note this only happens if our listener isn't referencing the pool
        via its default_pool or another active l7policy's redirect_pool_id.
        """
        if (self.listener.default_pool is not None and
                pool is not None and
                pool.id != self.listener.default_pool.id and
                pool in self.listener.pools):
            listener_l7pools = [
                p.redirect_pool for p in self.listener.l7policies
                if p.redirect_pool is not None and
                p.l7rules and p.enabled is True and
                p.id != self.id]
            if pool not in listener_l7pools:
                self.listener.pools.remove(pool)
                pool.listeners.remove(self.listener)

    def update(self, update_dict):
        for key, value in update_dict.items():
            if key == 'redirect_pool_id' and value is not None:
                self._conditionally_remove_pool_links(self.redirect_pool)
                self.action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL
                self.redirect_url = None
                self.redirect_http_code = None
                pool = self._find_in_graph('Pool' + value)
                self.redirect_pool = pool
                if self.l7rules and (self.enabled is True or (
                        'enabled' in update_dict.keys() and
                        update_dict['enabled'] is True)):
                    if pool not in self.listener.pools:
                        self.listener.pools.append(pool)
                    if self.listener not in pool.listeners:
                        pool.listeners.append(self.listener)
            elif key == 'redirect_url' and value is not None:
                self.action = constants.L7POLICY_ACTION_REDIRECT_TO_URL
                self._conditionally_remove_pool_links(self.redirect_pool)
                self.redirect_pool = None
                self.redirect_pool_id = None
            elif key == 'action' and value == constants.L7POLICY_ACTION_REJECT:
                self.redirect_url = None
                self._conditionally_remove_pool_links(self.redirect_pool)
                self.redirect_pool = None
                self.redirect_pool_id = None
                self.redirect_http_code = None
            elif key == 'position':
                self.listener.l7policies.remove(self)
                self.listener.l7policies.insert(value - 1, self)
            elif key == 'enabled':
                if (value is True and self.action ==
                        constants.L7POLICY_ACTION_REDIRECT_TO_POOL and
                        self.redirect_pool is not None and
                        self.l7rules and
                        self.redirect_pool not in self.listener.pools):
                    self.listener.pools.append(self.redirect_pool)
                    self.redirect_pool.listeners.append(self.listener)
                elif (value is False and self.action ==
                        constants.L7POLICY_ACTION_REDIRECT_TO_POOL and
                        self.redirect_pool is not None):
                    self._conditionally_remove_pool_links(
                        self.redirect_pool)
            setattr(self, key, value)

    def delete(self):
        self._conditionally_remove_pool_links(self.redirect_pool)
        if self.redirect_pool:
            for p in self.redirect_pool.l7policies:
                if p.id == self.id:
                    self.redirect_pool.l7policies.remove(p)
        for p in self.listener.l7policies:
            if p.id == self.id:
                self.listener.l7policies.remove(p)
                break


class Quotas(BaseDataModel):

    def __init__(self,
                 project_id=None,
                 load_balancer=None,
                 listener=None,
                 pool=None,
                 health_monitor=None,
                 member=None,
                 l7policy=None,
                 l7rule=None,
                 in_use_health_monitor=None,
                 in_use_listener=None,
                 in_use_load_balancer=None,
                 in_use_member=None,
                 in_use_pool=None,
                 in_use_l7policy=None,
                 in_use_l7rule=None):
        self.project_id = project_id
        self.health_monitor = health_monitor
        self.listener = listener
        self.load_balancer = load_balancer
        self.pool = pool
        self.member = member
        self.l7policy = l7policy
        self.l7rule = l7rule
        self.in_use_health_monitor = in_use_health_monitor
        self.in_use_listener = in_use_listener
        self.in_use_load_balancer = in_use_load_balancer
        self.in_use_member = in_use_member
        self.in_use_pool = in_use_pool
        self.in_use_l7policy = in_use_l7policy
        self.in_use_l7rule = in_use_l7rule


class Flavor(BaseDataModel):

    def __init__(self, id=None, name=None,
                 description=None, enabled=None,
                 flavor_profile_id=None):
        self.id = id
        self.name = name
        self.description = description
        self.enabled = enabled
        self.flavor_profile_id = flavor_profile_id


class FlavorProfile(BaseDataModel):

    def __init__(self, id=None, name=None, provider_name=None,
                 flavor_data=None):
        self.id = id
        self.name = name
        self.provider_name = provider_name
        self.flavor_data = flavor_data


class AvailabilityZone(BaseDataModel):

    def __init__(self, name=None, description=None, enabled=None,
                 availability_zone_profile_id=None):
        self.name = name
        self.description = description
        self.enabled = enabled
        self.availability_zone_profile_id = availability_zone_profile_id


class AvailabilityZoneProfile(BaseDataModel):

    def __init__(self, id=None, name=None, provider_name=None,
                 availability_zone_data=None):
        self.id = id
        self.name = name
        self.provider_name = provider_name
        self.availability_zone_data = availability_zone_data


class ListenerCidr(BaseDataModel):

    def __init__(self, listener_id=None, cidr=None):
        self.listener_id = listener_id
        self.cidr = cidr

    # SQLAlchemy kindly attaches the whole listener object so
    # let's keep this simple by overriding the to_dict for this
    # object. Otherwise we recurse down the "ghost" listener object.
    def to_dict(self, **kwargs):
        return {'cidr': self.cidr, 'listener_id': self.listener_id}


class VipSecurityGroup(BaseDataModel):
    def __init__(self, load_balancer_id: str = None, sg_id: str = None):
        self.load_balancer_id = load_balancer_id
        self.sg_id = sg_id


class AmphoraMemberPort(BaseDataModel):

    def __init__(self, port_id=None, amphora_id=None, network_id=None,
                 created_at=None, updated_at=None):
        self.port_id = port_id
        self.amphora_id = amphora_id
        self.network_id = network_id
        self.created_at = created_at
        self.updated_at = updated_at
