import logging
from collections import defaultdict
from datetime import datetime, timezone
from typing import AsyncGenerator, Dict, List, Optional, Set, Tuple

from maps_adv.geosmb.clients.bvm import BvmClient

from .data_managers import BaseDataManager
from .enums import (
    CallEvent,
    ClientGender,
    OrderByField,
    OrderDirection,
    SegmentType,
    Source,
)
from .exceptions import (
    InvalidEventParams,
    NoClientIdFieldsPassed,
    UnknownClient,
    UnsupportedEventType,
)


class Domain:
    __slots__ = ["_dm", "_bvm_client"]

    _dm: BaseDataManager
    _bvm_client: BvmClient

    def __init__(self, dm: BaseDataManager, bvm_client: BvmClient):
        self._dm = dm
        self._bvm_client = bvm_client

    async def create_client(
        self,
        *,
        biz_id: int,
        source: Source,
        metadata: Optional[dict] = None,
        phone: Optional[int] = None,
        email: Optional[str] = None,
        passport_uid: Optional[int] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        gender: Optional[ClientGender] = None,
        comment: Optional[str] = None,
        initiator_id: Optional[int] = None,
    ) -> dict:
        client_to_merge = await self._find_client_for_merge(
            biz_id, phone, email, passport_uid
        )

        if client_to_merge:
            return await self._dm.merge_client(
                client_id=client_to_merge["id"],
                biz_id=biz_id,
                source=source,
                metadata=metadata,
                phone=phone,
                email=email,
                passport_uid=passport_uid,
                first_name=first_name,
                last_name=last_name,
                gender=gender,
                comment=comment,
                initiator_id=initiator_id,
            )

        return await self._dm.create_client(
            biz_id=biz_id,
            source=source,
            metadata=metadata,
            phone=phone,
            email=email,
            passport_uid=passport_uid,
            first_name=first_name,
            last_name=last_name,
            gender=gender,
            comment=comment,
            initiator_id=initiator_id,
        )

    async def create_clients(
        self,
        *,
        biz_id: int,
        source: Source,
        label: Optional[str] = None,
        clients: List[dict],
    ) -> dict:
        return await self._dm.create_clients(
            biz_id=biz_id, source=source, label=label, clients=clients
        )

    async def retrieve_client(self, *, biz_id: int, client_id: int) -> dict:
        return await self._dm.retrieve_client(biz_id=biz_id, client_id=client_id)

    async def list_clients(
        self,
        *,
        biz_id: int,
        client_ids: Optional[List[int]] = None,
        search_string: Optional[str] = None,
        segment_type: Optional[SegmentType] = None,
        label: Optional[str] = None,
        order_by_field: Optional[OrderByField] = None,
        order_direction: Optional[OrderDirection] = None,
        limit: int,
        offset: int,
    ) -> Tuple[int, List[dict]]:
        return await self._dm.list_clients(
            biz_id=biz_id,
            client_ids=client_ids,
            search_string=search_string,
            segment_type=segment_type,
            label=label,
            order_by_field=order_by_field,
            order_direction=order_direction,
            limit=limit,
            offset=offset,
        )

    async def list_contacts(self, client_ids: List[int]) -> List[dict]:
        return await self._dm.list_contacts(client_ids)

    async def list_suggest_clients(
        self, *, biz_id: int, search_field: OrderByField, search_string: str, limit: int
    ) -> List[dict]:
        return await self._dm.list_suggest_clients(
            biz_id=biz_id,
            search_field=search_field,
            search_string=search_string,
            limit=limit,
        )

    async def update_client(
        self,
        *,
        client_id: int,
        biz_id: int,
        source: Source,
        metadata: Optional[dict] = None,
        phone: Optional[int] = None,
        email: Optional[str] = None,
        passport_uid: Optional[int] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        gender: Optional[ClientGender] = None,
        comment: Optional[str] = None,
        initiator_id: Optional[int] = None,
    ) -> dict:
        return await self._dm.update_client(
            client_id=client_id,
            biz_id=biz_id,
            source=source,
            metadata=metadata,
            phone=phone,
            email=email,
            passport_uid=passport_uid,
            first_name=first_name,
            last_name=last_name,
            gender=gender,
            comment=comment,
            initiator_id=initiator_id,
        )

    async def add_event(
        self,
        *,
        client_id: int,
        biz_id: int,
        source: Source,
        event_timestamp: datetime,
        order_event: Optional[dict] = None,
        call_event: Optional[dict] = None,
    ):
        client_exists = await self._dm.client_exists(client_id=client_id, biz_id=biz_id)

        if not client_exists:
            raise UnknownClient(id=client_id, biz_id=biz_id)

        if order_event is not None:
            return await self._dm.add_order_event(
                client_id=client_id,
                biz_id=biz_id,
                source=source,
                event_timestamp=event_timestamp,
                **order_event,
            )
        elif call_event is not None:
            if call_event["event_type"] != CallEvent.INITIATED:
                raise UnsupportedEventType(
                    f"Event of type {call_event['event_type'].name} "
                    "can't be added via API."
                )

            return await self._dm.add_call_event(
                client_id=client_id,
                biz_id=biz_id,
                source=source,
                event_timestamp=event_timestamp,
                **call_event,
            )
        else:
            raise InvalidEventParams()

    async def list_segments(self, biz_id: int) -> dict:
        total_clients, segments, labels = await self._dm.list_segments(biz_id)

        segments_for_schema = [
            dict(segment_type=segment, **sizes) for segment, sizes in segments.items()
        ]
        segments_for_schema.sort(key=lambda i: i["segment_type"].name)

        labels_for_schema = [
            dict(name=label, size=size) for label, size in labels.items()
        ]
        labels_for_schema.sort(key=lambda i: i["name"])

        return dict(
            total_clients_current=total_clients["current"],
            total_clients_previous=total_clients["previous"],
            segments=segments_for_schema,
            labels=labels_for_schema,
        )

    async def segment_statistics(
        self, biz_id: int, segment_type: Optional[SegmentType]
    ) -> Dict[datetime, int]:
        return await self._dm.segment_statistics(
            biz_id=biz_id,
            segment_type=segment_type,
            on_datetime=datetime.now(timezone.utc),
        )

    async def list_clients_by_segment(
        self,
        biz_id: int,
        segment: Optional[SegmentType] = None,
        label: Optional[str] = None,
    ) -> List[dict]:
        return await self._dm.list_clients_by_segment(
            biz_id=biz_id, segment=segment, label=label
        )

    async def list_client_segments(
        self,
        *,
        passport_uid: Optional[int] = None,
        phone: Optional[int] = None,
        email: Optional[str] = None,
        biz_id: Optional[int] = None,
    ) -> List[dict]:
        if not any([passport_uid, phone, email]):
            raise NoClientIdFieldsPassed()

        return await self._dm.list_client_segments(
            passport_uid=passport_uid, phone=phone, email=email, biz_id=biz_id
        )

    async def list_client_events(
        self,
        client_id: int,
        biz_id: int,
        datetime_from: Optional[datetime] = None,
        datetime_to: Optional[datetime] = None,
    ) -> dict:
        return await self._dm.list_client_events(
            client_id=client_id,
            biz_id=biz_id,
            datetime_from=datetime_from,
            datetime_to=datetime_to,
        )

    async def import_call_events(
        self, events_gen: AsyncGenerator[List[list], None]
    ) -> None:
        logger = logging.getLogger("domain.import_call_events")
        await self._dm.create_table_import_call_events_tmp()

        logger.info("Starting copy call events")
        orgs = defaultdict(set)
        async for records in events_gen:
            for record in records:
                permalink, client_phone = record[1], record[2]
                orgs[permalink].add(client_phone)

            await self._dm.store_imported_call_events(records)

        logger.info("Starting resolve biz_id/clients")
        clients = await self._resolve_org_clients(orgs)

        logger.info("Starting upload call events")
        await self._dm.upload_imported_call_events(clients)
        logger.info("Finish upload call events")

    async def clear_clients_for_gdpr(self, passport_uid: int) -> List[dict]:
        return await self._dm.clear_clients_by_passport(passport_uid)

    async def search_clients_for_gdpr(self, passport_uid: int) -> bool:
        return await self._dm.check_clients_existence_by_passport(passport_uid)

    async def _resolve_org_clients(self, orgs: Dict[int, Set[int]]) -> List[list]:
        clients_data = []
        for permalink, phones in orgs.items():
            try:
                biz_id = await self._bvm_client.fetch_biz_id_by_permalink(permalink)
            except Exception as exc:
                logging.getLogger(__name__).warning(
                    f"Failed to resolve permalink {permalink} to biz_id. {exc}"
                )
                continue

            # todo (shpak_vadim): bulk create_client
            for phone in phones:
                try:
                    client = await self.create_client(
                        biz_id=biz_id, source=Source.GEOADV_PHONE_CALL, phone=int(phone)
                    )
                except Exception as exc:
                    logging.getLogger(__name__).warning(
                        f"Failed to create client biz_id={biz_id}, phone={phone}. {exc}"
                    )
                    continue
                clients_data.append([permalink, phone, biz_id, client["id"]])

        return clients_data

    async def _find_client_for_merge(
        self,
        biz_id: int,
        phone: Optional[int],
        email: Optional[str],
        passport_uid: Optional[int],
    ) -> dict:
        candidates_for_merge = await self._dm.find_clients(
            biz_id=biz_id, phone=phone, email=email, passport_uid=passport_uid
        )

        if candidates_for_merge:
            return self._select_perfect_merging_candidate(
                candidates_for_merge, phone, email, passport_uid
            )

    @staticmethod
    def _select_perfect_merging_candidate(
        candidates: List[dict],
        phone: Optional[int],
        email: Optional[str],
        passport_uid: Optional[int],
    ) -> dict:
        if passport_uid:
            # leave candidates with the same passport_uid or without passport_uid
            candidates = [
                c for c in candidates if c["passport_uid"] in (None, passport_uid)
            ]

        id_fields_values = {
            "passport_uid": passport_uid,
            "phone": phone,
            "email": email,
        }

        for field_id in ("passport_uid", "phone", "email"):
            if id_fields_values[field_id] is None:
                continue

            merge_candidates = [
                c for c in candidates if c[field_id] == id_fields_values[field_id]
            ]

            if merge_candidates:
                merge_candidates_with_passport = [
                    c for c in merge_candidates if c["passport_uid"]
                ]

                return (
                    merge_candidates_with_passport[0]
                    if merge_candidates_with_passport
                    else merge_candidates[0]
                )
