import asyncio

from functools import partial
from itertools import zip_longest
from typing import List, Optional

from maps_adv.billing_proxy.lib.core.balance_client import (
    BalanceClient,
    UserIsAlreadyAssociatedError,
)
from maps_adv.billing_proxy.lib.data_manager import (
    exceptions as data_manager_exceptions,
)
from maps_adv.billing_proxy.lib.data_manager.clients import AbstractClientsDataManager

from .exceptions import (
    AgencyDoesNotExist,
    ClientByUidDoesNotExist,
    ClientDoesNotExist,
    ClientsDoNotExist,
    UserIsAssignedToAnotherClient,
    ClientsHaveOrdersWithAgency,
    ClientsAreAlreadyInAgency,
)


class ClientsDomain:
    def __init__(self, dm: AbstractClientsDataManager, balance_client: BalanceClient):
        self._dm = dm
        self._balance_client = balance_client

    async def _import_client_from_balance(
        self, client_id: int = None, uid: int = None, **overrides
    ) -> dict:
        if client_id is not None:
            client = await self._balance_client.find_client(client_id=client_id)
        elif uid is not None:
            client = await self._balance_client.find_client(uid=uid)
        if client is None or client["is_agency"]:
            if client_id is not None:
                raise ClientDoesNotExist(client_id=client_id)
            if uid is not None:
                raise ClientByUidDoesNotExist(uid=uid, is_agency=(client is not None))

        contracts = await self._balance_client.list_client_contracts(client["id"])
        reps = await self._balance_client.list_client_passports(client["id"])

        client.update(overrides)

        async with self._dm.connection() as con:
            client = await self._dm.upsert_client(
                con=con, representatives=reps, **client
            )
            await self._dm.sync_client_contracts(client["id"], contracts, con)
            return client

    async def retrieve_client(self, client_id: int):
        client = await self._dm.find_client_locally(client_id)
        if client is not None and client["is_agency"]:
            raise ClientDoesNotExist(client_id=client_id)

        if client is None:
            client = await self._import_client_from_balance(
                client_id=client_id, has_accepted_offer=False
            )

        if client is None:
            raise ClientDoesNotExist(client_id=client_id)

        del client["is_agency"]
        return client

    async def register_client(
        self,
        uid: Optional[int],
        name: str,
        email: str,
        phone: str,
        domain: str,
        account_manager_id: int = None,
        has_accepted_offer: bool = False,
    ):
        if uid is not None:
            try:
                client = await self._import_client_from_balance(
                    uid=uid, has_accepted_offer=has_accepted_offer
                )
                # update client in case it hasn't got service_id
                await self._balance_client.create_client(
                    client["name"], client["email"], client["phone"], client["id"]
                )
                return client
            except ClientByUidDoesNotExist as exc:
                if exc.is_agency:
                    raise

        client = await self.create_client(
            name,
            email,
            phone,
            domain,
            account_manager_id,
            has_accepted_offer,
        )

        if uid is not None:
            await self.add_user_to_client(client["id"], False, uid)

        return client

    async def create_client(
        self,
        name: str,
        email: str,
        phone: str,
        domain: str,
        account_manager_id: int = None,
        has_accepted_offer: bool = False,
    ):
        created_id = await self._balance_client.create_client(name, email, phone)

        return await self._dm.insert_client(
            created_id,
            name,
            email,
            phone,
            account_manager_id,
            domain,
            None,
            has_accepted_offer,
            True,
        )

    async def set_account_manager_for_client(
        self, client_id: int, account_manager_id: int
    ):
        try:
            await self._dm.set_account_manager_for_client(client_id, account_manager_id)
        except data_manager_exceptions.ClientsDoNotExist:
            raise ClientDoesNotExist(client_id=client_id)

    async def list_agencies(self):
        return await self._dm.list_agencies()

    async def list_agency_clients(self, agency_id: int):
        if not await self._dm.agency_exists(agency_id):
            raise AgencyDoesNotExist(agency_id=agency_id)

        return await self._dm.list_agency_clients(agency_id)

    async def list_internal_clients(self):
        return await self._dm.list_agency_clients(None)

    async def list_account_manager_clients(self, account_manager_id: int):
        return await self._dm.list_account_manager_clients(account_manager_id)

    async def add_clients_to_agency(self, client_ids: List[int], agency_id: int):
        clients_with_agencies = await self._dm.list_clients_with_agencies(client_ids)
        if clients_with_agencies:
            raise ClientsAreAlreadyInAgency(client_ids=clients_with_agencies)

        try:
            await self._dm.add_clients_to_agency(client_ids, agency_id)
        except data_manager_exceptions.AgencyDoesNotExist as exc:
            raise AgencyDoesNotExist(agency_id=exc.agency_id)
        except data_manager_exceptions.ClientsDoNotExist as exc:
            raise ClientsDoNotExist(client_ids=exc.client_ids)

    async def add_clients_to_internal(self, client_ids: List[int]):
        clients_with_agencies = await self._dm.list_clients_with_agencies(client_ids)
        if clients_with_agencies:
            raise ClientsAreAlreadyInAgency(client_ids=clients_with_agencies)

        try:
            await self._dm.add_clients_to_agency(client_ids, None)
        except data_manager_exceptions.ClientsDoNotExist as exc:
            raise ClientsDoNotExist(client_ids=exc.client_ids)

    async def remove_clients_from_agency(self, client_ids: List[int], agency_id: int):
        if not await self._dm.agency_exists(agency_id):
            raise AgencyDoesNotExist(agency_id=agency_id)

        clients_with_orders_with_agency = (
            await self._dm.list_clients_with_orders_with_agency(client_ids, agency_id)
        )
        if clients_with_orders_with_agency:
            raise ClientsHaveOrdersWithAgency(
                client_ids=clients_with_orders_with_agency
            )

        await self._dm.remove_clients_from_agency(client_ids, agency_id)

    async def remove_clients_from_internal(self, client_ids: List[int]):
        clients_with_orders_with_agency = (
            await self._dm.list_clients_with_orders_with_agency(client_ids, None)
        )
        if clients_with_orders_with_agency:
            raise ClientsHaveOrdersWithAgency(
                client_ids=clients_with_orders_with_agency
            )

        await self._dm.remove_clients_from_agency(client_ids, None)

    async def list_client_contracts(self, client_id: int) -> List[dict]:
        if not await self._dm.client_exists(client_id):
            raise ClientDoesNotExist(client_id=client_id)

        return await self._dm.list_contacts_by_client(client_id)

    async def list_agency_contracts(self, agency_id: int) -> List[dict]:
        if not await self._dm.agency_exists(agency_id):
            raise AgencyDoesNotExist(agency_id=agency_id)

        return await self._dm.list_contacts_by_client(agency_id)

    async def sync_clients_data_and_contracts_with_balance(self):
        limit = asyncio.Semaphore(10)

        async def sync_clients(semaphore, ids):
            async with semaphore:
                clients = await self._balance_client.find_clients(
                    list(filter(None, ids))  # None can come from zip_longest
                )
                async with self._dm.connection() as con:
                    for client in clients:
                        await self._dm.upsert_client(con=con, **client)

        async def sync_contract(semaphore, id):
            async with semaphore:
                contracts = await self._balance_client.list_client_contracts(id)
                await self._dm.sync_client_contracts(id, contracts)

        async def sync_representatives(semaphore, id):
            async with semaphore:
                reps = await self._balance_client.list_client_passports(id)
                await self._dm.set_representatives_for_client(id, reps)

        ids = await self._dm.list_client_ids()

        slices = [iter(ids)] * 300
        await asyncio.gather(
            *map(partial(sync_clients, limit), zip_longest(*slices)),
            return_exceptions=True
        )
        await asyncio.gather(
            *map(partial(sync_contract, limit), ids), return_exceptions=True
        )
        await asyncio.gather(
            *map(partial(sync_representatives, limit), ids), return_exceptions=True
        )

    async def find_client_by_uid(self, uid: int):
        return self._import_client_from_balance(uid=uid)

    async def set_client_has_accepted_offer(self, client_id: int, is_agency: bool):
        await self._dm.set_client_has_accepted_offer(client_id, is_agency)

    async def list_clients(self):
        return await self._dm.list_clients()

    @staticmethod
    def _ensure_valid_client(client, client_id, is_agency):
        if client is None or client["is_agency"] != is_agency:
            if is_agency:
                raise AgencyDoesNotExist(agency_id=client_id)
            else:
                raise ClientDoesNotExist(client_id=client_id)

    async def add_user_to_client(self, client_id: int, is_agency: bool, uid: int):
        client = await self._dm.find_client_locally(client_id)
        self._ensure_valid_client(client, client_id, is_agency)
        try:
            await self._balance_client.create_user_client_association(client_id, uid)
            reps = await self._balance_client.list_client_passports(client_id)
            await self._dm.set_representatives_for_client(client_id, reps)
        except UserIsAlreadyAssociatedError:
            raise UserIsAssignedToAnotherClient()

    async def list_client_representatives(
        self, client_id: int, is_agency: bool
    ) -> List[int]:
        client = await self._dm.find_client_locally(client_id)
        self._ensure_valid_client(client, client_id, is_agency)
        return client["representatives"]


__all__ = ["ClientsDomain"]
