import asyncio
import itertools
import logging
from datetime import datetime, timezone
from typing import List, Optional

import pytz
from smb.common.http_client import BaseHttpClientException

from maps_adv.common.geoproduct import GeoproductClient
from maps_adv.common.yasms import YasmsClient
from maps_adv.geosmb.booking_yang.server.lib.clients import YangClient, templates_env
from maps_adv.geosmb.booking_yang.server.lib.data_managers import OrdersDataManager
from maps_adv.geosmb.clients.geosearch import GeoSearchClient
from maps_adv.geosmb.doorman.client import DoormanClient, OrderEvent, Source

from .exceptions import InsufficientTimeToConfirm, OrgNotFound, OrgWithoutPhone
from .time_to_call_calcultator import TimeToCallCalculator


class OrdersDomain:
    __slots__ = (
        "_dm",
        "_geoproduct",
        "_geosearch",
        "_yang",
        "_yasms",
        "_doorman",
        "_new_yang_format",
        "_disconnect_orgs",
    )

    YANG_TASK_PHONE_CNT: int = 5
    CLIENT_CREATION_RESERVE_SEC: int = 60

    _dm: OrdersDataManager
    _geoproduct: GeoproductClient
    _geosearch: GeoSearchClient
    _yang: YangClient
    _yasms: Optional[YasmsClient]
    _doorman: Optional[DoormanClient]

    _new_yang_format: bool
    _disconnect_orgs: bool

    def __init__(
        self,
        *,
        dm: OrdersDataManager,
        geoproduct: GeoproductClient,
        geosearch: GeoSearchClient,
        yang: YangClient,
        yasms: Optional[YasmsClient],
        doorman: Optional[DoormanClient],
        new_yang_format: bool,
        disconnect_orgs: bool,
    ):
        self._dm = dm
        self._geoproduct = geoproduct
        self._geosearch = geosearch
        self._yang = yang
        self._yasms = yasms
        self._doorman = doorman

        self._new_yang_format = new_yang_format
        self._disconnect_orgs = disconnect_orgs

    async def make_order(
        self,
        permalink: int,
        reservation_datetime: datetime,
        reservation_timezone: str,
        person_count: int,
        customer_name: str,
        customer_phone: str,
        comment: str,
        call_agreement_accepted: bool,
        biz_id: int,
        customer_passport_uid: Optional[int] = None,
    ) -> datetime:
        org = await self._retrieve_org_data(permalink)

        order = await self._create_order(
            permalink=permalink,
            reservation_datetime=reservation_datetime,
            reservation_timezone=reservation_timezone,
            person_count=person_count,
            customer_name=customer_name,
            customer_phone=customer_phone,
            comment=comment,
            call_agreement_accepted=call_agreement_accepted,
            biz_id=biz_id,
            org=org,
            customer_passport_uid=customer_passport_uid,
        )

        if self._doorman:
            if order["time_to_call"] is not None:
                asyncio.create_task(
                    self._register_order_in_doorman(
                        order_id=order["id"],
                        biz_id=biz_id,
                        phone=customer_phone,
                        name=customer_name,
                        passport_uid=customer_passport_uid,
                        order_created_at=datetime.now(timezone.utc),
                    )
                )
            else:
                asyncio.create_task(
                    self._register_rejected_order_in_doorman(
                        order_id=order["id"],
                        biz_id=biz_id,
                        phone=customer_phone,
                        name=customer_name,
                        passport_uid=customer_passport_uid,
                    )
                )

        if order["time_to_call"] is None:
            raise InsufficientTimeToConfirm()

        if order["time_to_call"] <= datetime.now(tz=timezone.utc):
            asyncio.create_task(self._create_yang_task_suite(org=org, **order))

        return order["time_to_call"]

    async def fetch_pending_yang_tasks_count(self) -> int:
        return await self._dm.fetch_pending_yang_tasks_count()

    async def _create_order(
        self,
        permalink: int,
        reservation_datetime: datetime,
        reservation_timezone: str,
        person_count: int,
        customer_name: str,
        customer_phone: str,
        comment: str,
        call_agreement_accepted: bool,
        biz_id: int,
        org: dict,
        customer_passport_uid: int,
    ) -> dict:
        order = {
            "permalink": permalink,
            "reservation_datetime": reservation_datetime,
            "reservation_timezone": reservation_timezone,
            "person_count": person_count,
            "customer_name": customer_name,
            "customer_phone": customer_phone,
            "customer_passport_uid": customer_passport_uid,
            "comment": comment,
            "call_agreement_accepted": call_agreement_accepted,
            "biz_id": biz_id,
            "time_to_call": TimeToCallCalculator(
                org_open_hours=org["open_hours"],
                reservation_datetime=reservation_datetime,
                reservation_timezone=reservation_timezone,
            ).calculate(),
            "created_at": datetime.now(timezone.utc),
        }

        order["id"] = await self._dm.create_order(**order)

        return order

    async def upload_orders(self) -> None:
        for order in await self._dm.list_pending_orders():
            org = await self._retrieve_org_data(order["permalink"])

            await self._create_yang_task_suite(org=org, **order)

    async def create_missed_clients(self) -> None:
        if not self._doorman:
            return

        for order in await self._dm.list_orders_without_client(
            self.CLIENT_CREATION_RESERVE_SEC
        ):
            try:
                await self._register_order_in_doorman(
                    order_id=order["id"],
                    biz_id=order["biz_id"],
                    phone=order["customer_phone"],
                    name=order["customer_name"],
                    order_created_at=order["created_at"],
                    passport_uid=order["customer_passport_uid"],
                )
            except BaseHttpClientException as exc:
                logging.getLogger(__name__).warning(
                    f"Fails to create client for order_id={order['id']}: {exc}"
                )

    async def send_missed_result_events(self) -> None:
        if not self._doorman:
            return

        for order in await self._dm.list_orders_for_sending_result_event():
            try:
                await self._register_order_result_in_doorman(
                    order_id=order["id"],
                    biz_id=order["biz_id"],
                    client_id=order["client_id"],
                    event_type=self._resolve_event_type_by_verdict(order["verdict"]),
                    # todo (shpak_vadim): must be time when assessor completed the task
                    result_timestamp=datetime.now(timezone.utc),
                )
            except BaseHttpClientException as exc:
                logging.getLogger(__name__).warning(
                    f"Fails to send order result event "
                    f"for order_id={order['id']}: {exc}"
                )

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

    async def import_processed_tasks(self):
        min_created = await self._dm.retrieve_earliest_unprocessed_order_time()

        if min_created is None:
            return

        async for assignment in self._yang.list_accepted_assignments(min_created):
            order = await self._dm.retrieve_order_by_suite(assignment["task_suite_id"])
            if order is None:
                continue

            if self._disconnect_orgs:
                await self._disconnect_org_if_necessary(
                    order["permalink"], assignment["solution"]
                )

            verdict = self._parse_verdict(assignment["solution"])

            await self._dm.update_orders(
                order_ids=[order["id"]],
                task_result_got_at=datetime.now(tz=timezone.utc),
                booking_verdict=verdict,
            )

            if self._doorman:
                asyncio.create_task(
                    self._register_order_result_in_doorman(
                        order_id=order["id"],
                        biz_id=order["biz_id"],
                        client_id=order["client_id"],
                        event_type=self._resolve_event_type_by_verdict(verdict),
                        # todo (shpak_vadim): must be time when assessor completed task
                        #  ("submitted" field in Yang response)
                        result_timestamp=datetime.now(timezone.utc),
                    )
                )

    async def notify_about_processed_tasks(self):
        orders = await self._dm.retrieve_orders_for_sending_sms()
        for order in orders:
            try:
                await self._send_sms_with_verdict(order=order)
            except BaseHttpClientException as exc:
                logging.getLogger(__name__).exception(f"Fails to send sms: {exc}")
            else:
                await self._dm.update_orders(
                    order_ids=[order["id"]], sms_sent_at=datetime.now(timezone.utc)
                )

    async def clear_orders_for_gdpr(self, passport_uid: int) -> List[int]:
        return await self._dm.clear_orders_by_passport(passport_uid)

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

    async def _send_sms_with_verdict(self, order: dict) -> None:
        logging.getLogger(__name__).info(
            "%s sms for order %d, with verdict %s",
            ("sending" if self._yasms is not None else "would send"),
            order["id"],
            order["booking_verdict"],
        )
        if self._yasms is not None:
            await self._yasms.send_sms(
                phone=self._normalize_phone(order["customer_phone"]),
                text=self._build_sms_text(order),
            )

    async def _disconnect_org_if_necessary(
        self, permalink: int, solution: dict
    ) -> None:
        if solution.get("disconnect") or solution.get("not_booked") in (
            "not_cafe",
            "not_by_phone",
        ):
            asyncio.create_task(
                self._geoproduct.delete_organization_reservations(permalink=permalink)
            )

    async def _create_yang_task_suite(self, org: dict, **order) -> None:
        task_suite_data = self._build_yang_input_data(org=org, **order)

        async with self._dm.lock_order(order["id"]) as con:
            if not con:
                return

            try:
                created_suite = await self._yang.create_task_suite(task_suite_data)
            except BaseHttpClientException as exc:
                logging.getLogger(__name__).warning(
                    f"Failed to create task suite. Will try later. order={order}. {exc}"
                )
            else:
                logging.getLogger(__name__).info(
                    f"Created task suite {created_suite['id']}."
                )

                await self._dm.update_orders(
                    order_ids=[order["id"]],
                    yang_suite_id=created_suite["id"],
                    yang_task_created_at=created_suite["created_at"],
                    task_created_at=datetime.now(timezone.utc),
                    booking_meta={
                        "org_name": org["name"],
                        "org_phone": org["phones"][0],
                    },
                    con=con,
                )

    @staticmethod
    def _parse_verdict(result: dict) -> str:
        if result.get("booking") == "booked":
            return "booked"

        elif result.get("not_booked") == "no_place":
            return (
                "no_place_contacts_transfered"
                if result.get("customer_contacts_transfer")
                or result.get("alternative") == "customer_contacts_transfer"
                else "no_place"
            )

        elif result.get("not_booked") == "not_enough_information":
            return (
                "not_enough_information_contacts_transfered"
                if result.get("customer_contacts_transfer")
                or result.get("alternative") == "customer_contacts_transfer"
                else "not_enough_information"
            )

        elif result.get("call_status") == "cant":
            return "call_failed"

        else:
            return "generic_failure"

    @staticmethod
    def _build_sms_text(order: dict) -> str:
        local_datetime = (
            order["reservation_datetime"]
            .astimezone(pytz.timezone(order["reservation_timezone"]))
            .strftime("%H:%M %d.%m.%Y")
        )
        template_name = "sms_{}.j2".format(order["booking_verdict"])

        return templates_env.get_template(template_name).render(
            org_name=order["booking_meta"]["org_name"],
            reservation_datetime=local_datetime,
            customer_name=order["customer_name"],
            phone=order["booking_meta"]["org_phone"],
        )

    def _build_yang_input_data(self, *, org: dict, **order) -> dict:
        local_datetime = order["reservation_datetime"].astimezone(
            pytz.timezone(order["reservation_timezone"])
        )

        input_values = {
            "reservation_date": local_datetime.strftime("%d.%m.%Y"),
            "reservation_time": local_datetime.strftime("%H:%M"),
            "cafe_name": org["name"],
            "customer_fio": order["customer_name"],
            "comment": order["comment"],
            "person_cnt": str(order["person_count"]),
            "rubric_name": org["rubric"],
            "meta": {
                "ts": int(order["created_at"].timestamp() * 1000),
                "permalink": order["permalink"],
                "cafe_adress": org["formatted_address"],
                "pipeline": "maps-adv-bookings-yang",
            },
            "customer_phone": order["customer_phone"],
        }

        if self._new_yang_format:
            input_values["phones"] = org["phones"]
        else:
            for ind, phone in enumerate(
                itertools.islice(
                    itertools.chain(org["phones"], itertools.repeat(None)),
                    self.YANG_TASK_PHONE_CNT,
                ),
                start=1,
            ):
                input_values["phone{}".format(ind)] = phone

        return input_values

    async def _retrieve_org_data(self, permalink: int) -> dict:
        org = await self._geosearch.resolve_org(permalink)
        if org is None:
            raise OrgNotFound("Failed to resolve org {}.".format(permalink))

        reservation_phones = await self._get_reservation_phones(int(org.permalink))

        all_phones = reservation_phones + org.formatted_callable_phones
        if not all_phones:
            raise OrgWithoutPhone(f"Organization {permalink} resolves without phone")

        _phones = []
        [_phones.append(el) for el in all_phones if el not in _phones]

        return dict(
            name=org.name,
            rubric=org.categories_names[0],
            formatted_address=org.formatted_address,
            phones=_phones,
            open_hours=org.open_hours,
        )

    async def _get_reservation_phones(self, permalink: int) -> List[str]:
        reservations = await self._geoproduct.list_reservations(permalink)

        return [reservation["data"]["phone_number"] for reservation in reservations]

    @staticmethod
    def _normalize_phone(phone: str) -> int:
        return int("".join(ch for ch in phone if ch.isdigit()))

    async def _register_order_in_doorman(
        self,
        order_id: int,
        biz_id: int,
        phone: str,
        name: str,
        order_created_at: datetime,
        passport_uid: int,
    ) -> int:
        client = await self._doorman.create_client(
            biz_id=biz_id,
            source=Source.BOOKING_YANG,
            phone=self._normalize_phone(phone),
            first_name=name,
            passport_uid=passport_uid,
        )
        async with self._dm.acquire_con_with_tx() as con:
            await self._dm.update_orders(
                con=con, order_ids=[order_id], client_id=client["id"]
            )

            await self._doorman.add_order_event(
                order_id=order_id,
                biz_id=biz_id,
                client_id=client["id"],
                event_type=OrderEvent.CREATED,
                event_timestamp=order_created_at,
                source=Source.BOOKING_YANG,
            )

        return client["id"]

    async def _register_rejected_order_in_doorman(
        self, order_id: int, biz_id: int, phone: str, name: str, passport_uid: int
    ) -> None:
        now = datetime.now(timezone.utc)
        client_id = await self._register_order_in_doorman(
            order_id=order_id,
            biz_id=biz_id,
            phone=phone,
            name=name,
            order_created_at=now,
            passport_uid=passport_uid,
        )
        await self._register_order_result_in_doorman(
            order_id=order_id,
            biz_id=biz_id,
            client_id=client_id,
            event_type=OrderEvent.REJECTED,
            result_timestamp=now,
        )

    async def _register_order_result_in_doorman(
        self,
        order_id: int,
        biz_id: int,
        client_id: int,
        event_type: OrderEvent,
        result_timestamp: datetime,
    ) -> None:
        async with self._dm.acquire_con_with_tx() as con:
            await self._dm.update_orders(
                con=con,
                order_ids=[order_id],
                sent_result_event_at=datetime.now(tz=timezone.utc),
            )
            await self._doorman.add_order_event(
                order_id=order_id,
                biz_id=biz_id,
                client_id=client_id,
                event_type=event_type,
                event_timestamp=result_timestamp,
                source=Source.BOOKING_YANG,
            )

    @staticmethod
    def _resolve_event_type_by_verdict(verdict: str) -> OrderEvent:
        return OrderEvent.ACCEPTED if verdict == "booked" else OrderEvent.REJECTED

    async def fetch_actual_orders(self, actual_on: datetime) -> List[dict]:
        return await self._dm.fetch_actual_orders(actual_on=actual_on)
