import asyncio
import logging
from datetime import datetime, timezone
from typing import AsyncIterator, Iterable, List, Optional

import pytz
from babel import dates as babel_dates

from maps_adv.geosmb.booking_yang.client import BookingYangClient
from maps_adv.geosmb.clients.bvm import BvmClient
from maps_adv.geosmb.clients.geosearch import GeoSearchClient
from maps_adv.geosmb.clients.market import MarketIntClient
from maps_adv.geosmb.doorman.client import DoormanClient


class PoiDomain:
    __slots__ = [
        "_booking_yang_client",
        "_doorman_client",
        "_market_client",
        "_bvm_client",
        "_booking_yang_poi_experiment",
        "_market_poi_experiment",
        "_geosearch_client",
    ]

    _booking_yang_client: BookingYangClient
    _doorman_client: DoormanClient
    _market_client: MarketIntClient
    _bvm_client: BvmClient
    _booking_yang_poi_experiment: str
    _geosearch_client: GeoSearchClient
    _market_poi_experiment: str

    def __init__(
        self,
        booking_yang: BookingYangClient,
        doorman_client: DoormanClient,
        market_client: MarketIntClient,
        bvm_client: BvmClient,
        geosearch_client: GeoSearchClient,
        booking_yang_poi_experiment: str,
        market_poi_experiment: str,
    ):
        self._booking_yang_client = booking_yang
        self._doorman_client = doorman_client
        self._market_client = market_client
        self._bvm_client = bvm_client
        self._geosearch_client = geosearch_client
        self._booking_yang_poi_experiment = booking_yang_poi_experiment
        self._market_poi_experiment = market_poi_experiment

    async def iter_orders_poi_data_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        utc_now = datetime.now(tz=timezone.utc)
        yang_orders = await self._fetch_booking_yang_poi_data(actual_on=utc_now)
        market_orders = await self._fetch_market_poi_data(actual_on=utc_now)

        await self._resolve_passports(
            yang_orders=yang_orders, market_orders=market_orders
        )
        await self._resolve_permalinks(market_orders)
        await asyncio.gather(
            *[
                self._resolve_tz_offset(order)
                for order in market_orders
                if order.get("permalink")
            ]
        )

        async for chunk in self._yield_export_data(
            iter_size=iter_size,
            yang_orders=yang_orders,
            market_orders=market_orders,
            utc_now=utc_now,
        ):
            yield chunk

    async def _fetch_booking_yang_poi_data(self, actual_on: datetime) -> List[dict]:
        data = []
        try:
            data = await self._booking_yang_client.fetch_actual_orders(
                actual_on=actual_on
            )
        except Exception as exc:
            logging.getLogger(__name__).exception(
                "Unhandled exception while fetching data from booking_yang",
                exc_info=exc,
            )

        return data

    async def _fetch_market_poi_data(self, actual_on: datetime) -> List[dict]:
        data = []
        try:
            data = await self._market_client.fetch_actual_orders(actual_on=actual_on)
        except Exception as exc:
            logging.getLogger(__name__).exception(
                "Unhandled exception while fetching data from market-int",
                exc_info=exc,
            )

        return data

    async def _resolve_passports(
        self, yang_orders: List[dict], market_orders: List[dict]
    ) -> None:
        yang_need_passports = [
            order
            for order in yang_orders
            if not order.get("passport_uid") and order.get("client_id")
        ]

        need_passport_clients = set().union(
            [order["client_id"] for order in yang_need_passports],
            [order["client_id"] for order in market_orders],
        )

        if need_passport_clients:
            with_passports = await self._fetch_passports(need_passport_clients)

            if with_passports:
                for order in yang_need_passports:
                    order["passport_uid"] = with_passports.get(
                        order["client_id"], {}
                    ).get("passport_uid")

                for order in market_orders:
                    order["passport_uid"] = with_passports.get(
                        order["client_id"], {}
                    ).get("passport_uid")

    async def _fetch_passports(self, need_passport_clients: Iterable[int]):
        client_contacts = []
        try:
            client_contacts = await self._doorman_client.list_contacts(
                client_ids=need_passport_clients
            )
        except Exception as exc:
            logging.getLogger(__name__).exception(
                "Unhandled exception while fetching data from doorman",
                exc_info=exc,
            )
        else:
            unknown_clients = need_passport_clients - client_contacts.keys()
            if unknown_clients:
                logging.getLogger(__name__).error(
                    f"Failed resolve passports for unknown "
                    f"client_ids={unknown_clients}."
                )

        return client_contacts

    async def _resolve_permalinks(self, orders: List[dict]) -> None:
        orders_for_resolve = [order for order in orders if order.get("passport_uid")]
        biz_ids = [order["biz_id"] for order in orders_for_resolve]

        if not biz_ids:
            return

        try:
            biz_permalinks = await self._bvm_client.fetch_permalinks_by_biz_ids(
                biz_ids=biz_ids
            )
        except Exception as exc:
            logging.getLogger(__name__).exception(
                f"Unhandled exception while fetching permalinks from bvm "
                f"for biz_ids={biz_ids}",
                exc_info=exc,
            )
        else:
            for order in orders_for_resolve:
                if order["biz_id"] in biz_permalinks:
                    order["permalink"] = biz_permalinks[order["biz_id"]][0]

    async def _resolve_tz_offset(self, order: dict) -> None:
        try:
            org_info = await self._geosearch_client.resolve_org(
                permalink=order["permalink"]
            )
            order["tz_offset"] = org_info.tz_offset
        except Exception as exc:
            logging.getLogger(__name__).exception(
                f"Unhandled exception while fetching tz_offset from geosearch "
                f"for biz_id={order['biz_id']}, permalink={order['permalink']}",
                exc_info=exc,
            )

    async def _yield_export_data(
        self,
        iter_size: Optional[int],
        yang_orders: List[dict],
        market_orders: List[dict],
        utc_now: datetime,
    ) -> AsyncIterator[List[dict]]:
        export_chunk = []
        for order in yang_orders:
            if order["passport_uid"]:
                export_chunk.append(
                    self._convert_order_to_yang_poi_format(order=order, utc_now=utc_now)
                )

            if iter_size and len(export_chunk) == iter_size:
                yield export_chunk

                export_chunk = []

        for order in market_orders:
            if (
                order.get("passport_uid")
                and order.get("permalink")
                and order.get("tz_offset") is not None
            ):
                export_chunk.append(
                    self._convert_order_to_market_poi_format(
                        order=order, utc_now=utc_now
                    )
                )

            if iter_size and len(export_chunk) == iter_size:
                yield export_chunk

                export_chunk = []

        if export_chunk:
            yield export_chunk

    def _convert_order_to_yang_poi_format(self, order: dict, utc_now: datetime) -> dict:
        shifted_reservation_dt = order["reservation_datetime"].astimezone(
            pytz.timezone(order["reservation_timezone"])
        )
        subscript_dt = self._make_subscript_dt(
            shifted_reservation_dt=shifted_reservation_dt,
            utc_reservation_dt=order["reservation_datetime"],
            utc_now=utc_now,
        )

        return {
            "permalink": order["permalink"],
            "passport_uid": order["passport_uid"],
            "experiment_tag": self._booking_yang_poi_experiment,
            "subscript": [
                ("RU", f"Стол на {subscript_dt['RU']}"),
                ("EN", f"Reservation for {subscript_dt['EN']}"),
            ],
        }

    def _convert_order_to_market_poi_format(
        self, order: dict, utc_now: datetime
    ) -> dict:
        shifted_reservation_dt = order["reservation_datetime"] + order["tz_offset"]

        subscript_dt = self._make_subscript_dt(
            shifted_reservation_dt=shifted_reservation_dt,
            utc_reservation_dt=order["reservation_datetime"],
            utc_now=utc_now,
        )

        return {
            "permalink": order["permalink"],
            "passport_uid": order["passport_uid"],
            "experiment_tag": self._market_poi_experiment,
            "subscript": [
                ("RU", f"Запись на {subscript_dt['RU']}"),
                ("EN", f"Appointment at {subscript_dt['EN']}"),
            ],
        }

    @staticmethod
    def _make_subscript_dt(
        *,
        shifted_reservation_dt: datetime,
        utc_reservation_dt: datetime,
        utc_now: datetime,
    ) -> dict:
        def _format_subscript_dt(value: datetime, format: str) -> dict:
            return {
                "RU": babel_dates.format_datetime(value, format=format, locale="ru_RU"),
                "EN": babel_dates.format_datetime(value, format=format, locale="EN"),
            }

        if utc_reservation_dt.date() == utc_now.date():
            return _format_subscript_dt(value=shifted_reservation_dt, format="HH:mm")
        else:
            return _format_subscript_dt(
                value=shifted_reservation_dt, format="HH:mm, d MMM"
            )
