"""Fake Preseem Model implementation."""
import time
from uuid import uuid4

import grpc
from ._apiutil import BaseModelObject
from preseem_protobuf.model.service_pb2 import SetAttachmentReq


def has_any_fields_set(message):
    for field_desc, value in message.ListFields():
        if value:
            return True
    return False


def _copy_obj(obj):
    """Copy a BaseModelObject, handling protobuf fields that can't be deepcopied."""
    new_dict = {}
    for key, value in obj.__dict__.items():
        if value is not None and hasattr(value, 'CopyFrom'):
            copy_val = type(value)()
            copy_val.CopyFrom(value)
            new_dict[key] = copy_val
        else:
            new_dict[key] = value
    return type(obj)(**new_dict)


class FakePreseemModel:
    _companies = {}  # company_uuid to Company object
    _elements = {}  # company_uuid to {element_uuid to Element object}
    _accounts = {}  # company_uuid to {account_uuid to Account object}
    _packages = {}  # company_uuid to {package_uuid to Package object}
    _services = {}  # company_uuid to {service_uuid to Service objects}
    _sites = {}  # company_uuid to {site_uuid to Site object}
    _elem_disc = {}  # company_uuid to {uuid to ElementDiscovery object}

    def _new_attrs(uuid=None):
        """Generate attributes for a new preseem-model object.
           By default this generates a random UUID, or a UUID can be passed in."""
        now = time.time()
        if not uuid:
            uuid = uuid4()
        return {
            'id': uuid.int & (1 << 64) - 1,
            'uuid': uuid.bytes,
            'created_at': now,
            'updated_at': now,
            'inactive': False,
            'inactive_time': None
        }

    def _set_inactive(obj):
        obj.inactive = True
        obj.updated_at = obj.inactive_time = time.time()

    def clear(self):
        FakePreseemModel._companies.clear()
        FakePreseemModel._elements.clear()
        FakePreseemModel._accounts.clear()
        FakePreseemModel._packages.clear()
        FakePreseemModel._services.clear()
        FakePreseemModel._sites.clear()
        FakePreseemModel._elem_disc.clear()

    async def init(self):
        pass

    async def copy_from(self, other, company_uuid):
        """Insert exact copies of entities from another model into this one."""
        try:
            for obj in await other.Company.List():
                FakePreseemModel._companies[obj.uuid] = obj
        except grpc.RpcError:  # likely permissions, fake this
            FakePreseemModel._companies[company_uuid] = other.Company(uuid=company_uuid)
        for obj in await other.Element.List(company_uuid=company_uuid):
            elems = FakePreseemModel._elements.get(company_uuid)
            if elems is None:
                elems = FakePreseemModel._elements[company_uuid] = {}
            elems[obj.uuid] = obj
        for obj in await other.Element.ListElementDiscovery(company_uuid=company_uuid):
            discs = FakePreseemModel._elem_disc.get(company_uuid)
            if discs is None:
                discs = FakePreseemModel._elem_disc[company_uuid] = {}
            discs[obj.uuid] = obj
        for obj in await other.Site.List(company_uuid=company_uuid):
            sites = FakePreseemModel._sites.get(company_uuid)
            if sites is None:
                sites = FakePreseemModel._sites[company_uuid] = {}
            sites[obj.uuid] = obj
        for obj in await other.Package.List(company_uuid=company_uuid):
            packages = FakePreseemModel._packages.get(company_uuid)
            if packages is None:
                packages = FakePreseemModel._packages[company_uuid] = {}
            packages[obj.uuid] = obj
        for obj in await other.Account.List(company_uuid=company_uuid):
            accounts = FakePreseemModel._accounts.get(company_uuid)
            if accounts is None:
                accounts = FakePreseemModel._accounts[company_uuid] = {}
            accounts[obj.uuid] = obj
        for obj in await other.Service.List(company_uuid=company_uuid,
                                            include_attachment=True):
            services = FakePreseemModel._services.get(company_uuid)
            if services is None:
                services = FakePreseemModel._services[company_uuid] = {}
            services[obj.uuid] = obj

    """Fake preseem model that implements the python API."""

    class Company(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            obj = cls(**FakePreseemModel._new_attrs(kwargs.get('uuid')),
                      name=kwargs.get('name') or '',
                      parent_uuid=kwargs.get('parent_uuid'),
                      old_id=kwargs.get('old_id'))
            FakePreseemModel._companies[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def Get(cls, **kwargs):
            uuid = kwargs.get('uuid')
            assert uuid
            return _copy_obj(FakePreseemModel._companies[uuid])

        @classmethod
        async def GetByOldId(cls, **kwargs):
            old_id = kwargs.get('old_id')
            assert old_id
            companies = [
                x for x in FakePreseemModel._companies.values() if x.old_id == old_id
            ]
            assert len(companies) < 2
            if companies:
                return _copy_obj(companies[0])
            # TODO error for empty?

        @classmethod
        async def List(cls, **kwargs):
            return [_copy_obj(x) for x in FakePreseemModel._companies.values()]

    class ElementDiscovery(BaseModelObject):
        pass

    class Element(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            obj = cls(**FakePreseemModel._new_attrs(),
                      company_uuid=company.uuid,
                      name=kwargs.get('name'),
                      serial_number=kwargs.get('serial_number'),
                      system_mac_address=kwargs.get('system_mac_address'),
                      imsi=kwargs.get('imsi'),
                      management_ip=kwargs.get('management_ip'),
                      site_uuid=kwargs.get('site_uuid'),
                      status=kwargs.get('status') or 0,
                      poller_hash=kwargs.get('poller_hash'),
                      intent_role_uuid=kwargs.get('intent_role_uuid'),
                      acting_role_uuid=kwargs.get('acting_role_uuid'),
                      source=kwargs.get('source'),
                      source_id=kwargs.get('source_id'))
            elems = FakePreseemModel._elements.get(company.uuid)
            if elems is None:
                elems = FakePreseemModel._elements[company_uuid] = {}
            elems[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def List(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._elements.get(company_uuid) or {}).values()
                if all([getattr(x, k) == v for k, v in kwargs.items()])
            ]

        @classmethod
        async def Delete(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            elems = FakePreseemModel._elements[company_uuid]
            if elems:
                elem = elems.get(uuid)
                if elem:
                    del elems[uuid]

        @classmethod
        async def Update(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            elems = FakePreseemModel._elements[company_uuid]
            if elems:
                elem = elems.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(elem)
                    else:
                        setattr(elem, attr, val)

        @classmethod
        async def CreateElementDiscovery(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            obj = FakePreseemModel.ElementDiscovery(
                **FakePreseemModel._new_attrs(),
                company_uuid=company.uuid,
                name=kwargs.get('name'),
                management_ip=kwargs.get('management_ip'),
                site_uuid=kwargs.get('site_uuid'),
                source=kwargs.get('source'),
                source_id=kwargs.get('source_id'),
                intent_role_uuid=kwargs.get('intent_role_uuid'),
                element_uuid=kwargs.get('element_uuid'),
                status=kwargs.get('status'))
            discs = FakePreseemModel._elem_disc.get(company.uuid)
            if discs is None:
                discs = FakePreseemModel._elem_disc[company_uuid] = {}
            discs[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def ListElementDiscovery(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            intent_role_uuid = kwargs.get('intent_role_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._elem_disc.get(company_uuid) or {}).values()
                if not intent_role_uuid or intent_role_uuid == x.intent_role_uuid
            ]

        @classmethod
        async def DeleteElementDiscovery(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            discs = FakePreseemModel._elem_disc[company_uuid]
            if discs:
                disc = discs.get(uuid)
                if disc:
                    del discs[uuid]

        @classmethod
        async def UpdateElementDiscovery(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            discs = FakePreseemModel._elem_disc[company_uuid]
            if discs:
                disc = discs.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(disc)
                    else:
                        if getattr(disc, attr) != val:
                            disc.updated_at = time.time()
                        setattr(disc, attr, val)

    class Account(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            obj = cls(**FakePreseemModel._new_attrs(),
                      company_uuid=company.uuid,
                      source=kwargs.get('source'),
                      source_id=kwargs.get('source_id'),
                      type=kwargs.get('type'),
                      status=kwargs.get('status'),
                      name=kwargs.get('name'),
                      first_name=kwargs.get('first_name'),
                      last_name=kwargs.get('last_name'))
            accounts = FakePreseemModel._accounts.get(company.uuid)
            if accounts is None:
                accounts = FakePreseemModel._accounts[company_uuid] = {}
            accounts[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def List(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._accounts.get(company_uuid) or {}).values()
            ]

        @classmethod
        async def Delete(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            accounts = FakePreseemModel._accounts[company_uuid]
            if accounts:
                account = accounts.get(uuid)
                if account:
                    del accounts[uuid]

        @classmethod
        async def Update(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            accounts = FakePreseemModel._accounts[company_uuid]
            if accounts:
                account = accounts.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(account)
                    else:
                        if getattr(account, attr) != val:
                            account.updated_at = time.time()
                        setattr(account, attr, val)

    class Package(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            obj = cls(**FakePreseemModel._new_attrs(),
                      company_uuid=company.uuid,
                      name=kwargs.get('name') or '',
                      source=kwargs.get('source'),
                      source_id=kwargs.get('source_id'),
                      download_rate=kwargs.get('download_rate'),
                      upload_rate=kwargs.get('upload_rate'))
            packages = FakePreseemModel._packages.get(company.uuid)
            if packages is None:
                packages = FakePreseemModel._packages[company_uuid] = {}
            packages[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def List(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._packages.get(company_uuid) or {}).values()
            ]

        @classmethod
        async def Delete(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            packages = FakePreseemModel._packages[company_uuid]
            if packages:
                package = packages.get(uuid)
                if package:
                    del packages[uuid]

        @classmethod
        async def Update(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            packages = FakePreseemModel._packages[company_uuid]
            if packages:
                package = packages.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(package)
                    else:
                        if getattr(package, attr) != val:
                            package.updated_at = time.time()
                        setattr(package, attr, val)

    class Site(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            obj = cls(**FakePreseemModel._new_attrs(),
                      company_uuid=company.uuid,
                      name=kwargs.get('name') or '')
            sites = FakePreseemModel._sites.get(company.uuid)
            if sites is None:
                sites = FakePreseemModel._sites[company_uuid] = {}
            sites[obj.uuid] = obj
            return _copy_obj(obj)

        @classmethod
        async def List(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._sites.get(company_uuid) or {}).values()
            ]

        @classmethod
        async def Delete(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            sites = FakePreseemModel._sites[company_uuid]
            if sites:
                site = sites.get(uuid)
                if site:
                    del sites[uuid]

        @classmethod
        async def Update(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            sites = FakePreseemModel._sites[company_uuid]
            if sites:
                site = sites.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(site)
                    else:
                        if getattr(site, attr) != val:
                            site.updated_at = time.time()
                        setattr(site, attr, val)

    class Service(BaseModelObject):

        @classmethod
        async def Create(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            company = FakePreseemModel._companies[company_uuid]
            attachment = kwargs.get('attachment')
            obj = cls(**FakePreseemModel._new_attrs(),
                      company_uuid=company.uuid,
                      source=kwargs.get('source'),
                      source_id=kwargs.get('source_id'),
                      source_url=kwargs.get('source_url'),
                      account_uuid=kwargs.get('account_uuid'),
                      package_uuid=kwargs.get('package_uuid'),
                      download_rate=kwargs.get('download_rate'),
                      upload_rate=kwargs.get('upload_rate'),
                      attachment=attachment if has_any_fields_set(attachment) else None,
                      subscriber_identifier=kwargs.get('subscriber_identifier'))
            services = FakePreseemModel._services.get(company.uuid)
            if services is None:
                services = FakePreseemModel._services[company_uuid] = {}
            services[obj.uuid] = _copy_obj(obj)
            return _copy_obj(obj)

        @classmethod
        async def List(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            assert company_uuid
            # the real impl returns empty list if the company doesn't exist
            return [
                _copy_obj(x)
                for x in (FakePreseemModel._services.get(company_uuid) or {}).values()
            ]

        @classmethod
        async def Delete(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            services = FakePreseemModel._services[company_uuid]
            if services:
                service = services.get(uuid)
                if service:
                    del services[uuid]

        @classmethod
        async def Update(cls, **kwargs):
            company_uuid = kwargs.get('company_uuid')
            uuid = kwargs.get('uuid')
            services = FakePreseemModel._services[company_uuid]
            if services:
                service = services.get(uuid)
                for attr, val in kwargs.items():
                    if attr == 'attachment_req':
                        if val:
                            assert isinstance(val, SetAttachmentReq)
                            attr = 'attachment'
                            if has_any_fields_set(val.attachment):
                                val = val.attachment
                            else:
                                val = None
                    if attr == 'company_uuid' or attr == 'uuid':
                        continue
                    elif attr == 'inactive' and val is True:
                        FakePreseemModel._set_inactive(service)
                    else:
                        service.updated_at = time.time()
                        setattr(service, attr, val)
