import asyncio
import logging
from copy import deepcopy
from typing import Callable, Dict, List, Union

import emoji

from maps_adv.common import aiosup
from maps_adv.common.aioyav import YavClient
from maps_adv.common.email_sender import Client as EmailClient
from maps_adv.common.yasms import YasmsClient
from maps_adv.geosmb.doorman.client import DoormanClient
from maps_adv.geosmb.telegraphist.server.lib.enums import NotificationType, Transport
from maps_adv.geosmb.telegraphist.server.lib.exceptions import (
    UnsupportedTransport,
    UnsupportedNotificationType,
    TransportException,
    NoAddress,
    AddressNotAllowed,
    SendFailed,
    NoOrderLink,
    NotificationDisabledInSettings,
)
from maps_adv.geosmb.telegraphist.server.lib.templates import (
    email_context_processor,
    env as sms_templates,
    push_env as push_templates,
)
from maps_adv.geosmb.tuner.client import (
    BusinessEmailNotificationSettings,
    BusinessSmsNotificationSettings,
    TunerClient,
)

supported_notifications = {
    "business": {
        NotificationType.CERTIFICATE_PURCHASED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_purchased", ctx
                ),
                "subject_generator": lambda ctx: emoji.emojize(
                    ":tada: Клиент купил ваш сертификат"
                ),
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.CERTIFICATE_CREATED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_created", ctx
                ),
                "subject_generator": lambda ctx: emoji.emojize(
                    ":tada: Сертификат создан!"
                ),
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.CERTIFICATE_CONNECT_PAYMENT: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_connect_payment", ctx
                ),
                "subject_generator": lambda ctx: emoji.emojize(
                    ":moneybag: Как подключить оплату для сертификата?"
                ),
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.CERTIFICATE_REJECTED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_rejected", ctx
                ),
                "subject_generator": lambda ctx: "Сертификат не прошёл модерацию",
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.CERTIFICATE_EXPIRED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_expired", ctx
                ),
                "subject_generator": lambda ctx: "Ваш сертификат больше не продаётся",
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.CERTIFICATE_EXPIRING: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "certificate_expiring", ctx
                ),
                "subject_generator": lambda ctx: "Через 7 дней продажа вашего сертификата остановится",  # noqa
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.SUBSEQUENT_CERTIFICATE_APPROVED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "subsequent_certificate_approved", ctx
                ),
                "subject_generator": lambda ctx: emoji.emojize(
                    ":wink: Сертификат прошёл модерацию"
                ),
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.FIRST_CERTIFICATE_APPROVED: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "first_certificate_approved", ctx
                ),
                "subject_generator": lambda ctx: emoji.emojize(
                    ":wink: Сертификат прошёл модерацию"
                ),
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_CREATED_FOR_BUSINESS: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_created_for_business", ctx
                ),
                "subject_generator": lambda ctx: f"Новая запись на {ctx['order']['earliest_order_booking']}",  # noqa
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_CHANGED_FOR_BUSINESS: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_changed_for_business", ctx
                ),
                "subject_generator": lambda ctx: f"Клиент перенес запись на {ctx['order']['earliest_order_booking']}",  # noqa
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_CANCELLED_FOR_BUSINESS: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_cancelled_for_business", ctx
                ),
                "subject_generator": lambda ctx: f"Клиент отменил запись на {ctx['order']['earliest_order_booking']}",  # noqa
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
        },
        NotificationType.REQUEST_CREATED_FOR_BUSINESS: {
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "request_created_for_business", ctx
                ),
                "subject_generator": lambda ctx: "Вам пришла новая заявка, откройте",
                "from_generator": lambda ctx: (
                    "Яндекс.Бизнес",
                    "orders@maps.yandex.ru",
                ),
            },
            Transport.SMS: {"template_name": "request_created_for_business_sms"},
        },
    },
    "client": {
        NotificationType.ORDER_CREATED: {
            Transport.SMS: {"template_name": "order_created_sms"},
            Transport.PUSH: {
                "title": "Вы записаны!",
                "template_name": "order_created_push",
            },
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_created", ctx
                ),
                "subject_generator": lambda ctx: f"Вы записались на {ctx['order']['earliest_order_booking']}",  # noqa
                "from_generator": lambda ctx: (
                    ctx["org"]["name"],
                    "booking@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_REMINDER: {
            Transport.SMS: {"template_name": "order_reminder_sms"},
            Transport.PUSH: {
                "title": "Напоминаем о записи",
                "template_name": "order_reminder_push",
            },
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_reminder", ctx
                ),
                "subject_generator": lambda ctx: f"Напоминаем, что вы записаны на {ctx['order']['earliest_order_booking']}",  # noqa
                "from_generator": lambda ctx: (
                    ctx["org"]["name"],
                    "booking@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_CHANGED: {
            Transport.SMS: {"template_name": "order_changed_sms"},
            Transport.PUSH: {
                "title": "Запись изменена",
                "template_name": "order_changed_push",
            },
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_changed", ctx
                ),
                "subject_generator": lambda ctx: "Время вашей записи было изменено",
                "from_generator": lambda ctx: (
                    ctx["org"]["name"],
                    "booking@maps.yandex.ru",
                ),
            },
        },
        NotificationType.ORDER_CANCELLED: {
            Transport.SMS: {"template_name": "order_cancelled_sms"},
            Transport.PUSH: {
                "title": "Запись отменена",
                "template_name": "order_cancelled_push",
            },
            Transport.EMAIL: {
                "context_processor": lambda ctx: email_context_processor(
                    "order_cancelled", ctx
                ),
                "subject_generator": lambda ctx: f"Ваша запись на {ctx['order']['earliest_order_booking']} отменена",  # noqa
                "from_generator": lambda ctx: (
                    ctx["org"]["name"],
                    "booking@maps.yandex.ru",
                ),
            },
        },
    },
}


class NotificationRouter:
    _allowed_recipient_domain = "@yandex-team.ru"

    def __init__(
        self,
        *,
        doorman_client: DoormanClient,
        email_client: EmailClient,
        yasms: YasmsClient,
        yav_client: YavClient,
        sup_client: aiosup.SupClient,
        tuner_client: TunerClient,
        email_template_codes: Dict[str, Dict[str, str]],
        limit_recipients: bool = False,
        yav_secret_id: str,
    ):
        self._doorman_client = doorman_client
        self._email_client = email_client
        self._yasms = yasms
        self._yav_client = yav_client
        self._sup_client = sup_client
        self._tuner_client = tuner_client
        self._limit_recipients = limit_recipients
        self._yav_secret_id = yav_secret_id

        self._transport_senders = {
            Transport.SMS: self._send_sms,
            Transport.EMAIL: self._send_email,
            Transport.PUSH: self._send_push,
        }
        self._supported_notifications = deepcopy(supported_notifications)
        for group in self._supported_notifications.keys():
            for notification, template_code in email_template_codes[group].items():
                notification_type = NotificationType(notification)
                notification_settings = self._supported_notifications[group][
                    notification_type
                ][Transport.EMAIL]
                notification_settings["template_code"] = template_code

    async def send_client_notification(
        self,
        *,
        recipient: dict,
        transports: List[Transport],
        notification_type: NotificationType,
        notification_details: dict,
    ) -> List[Dict[Transport, Union[Union[int, str], TransportException]]]:
        try:
            notification = self._supported_notifications["client"][notification_type]
        except KeyError:
            raise UnsupportedNotificationType(notification_type)

        self._check_transports_are_supported_for_notification(
            notification=notification,
            notification_type=notification_type,
            transports=transports,
        )

        client_data = await self._doorman_client.retrieve_client(
            biz_id=recipient["biz_id"], client_id=recipient["client_id"]
        )

        return [
            result
            for transport in transports
            for result in await self._send_notification(
                transport=transport,
                recipient=recipient,
                notification=notification,
                notification_details=notification_details,
                additional_contact_details=client_data,
            )
        ]

    async def send_business_notification(
        self,
        *,
        recipient: dict,
        transports: List[Transport],
        notification_type: NotificationType,
        notification_details: dict,
    ) -> List[Dict[Transport, Union[Union[int, str], TransportException]]]:
        try:
            notification = self._supported_notifications["business"][notification_type]
        except KeyError:
            raise UnsupportedNotificationType(notification_type)

        self._check_transports_are_supported_for_notification(
            notification=notification,
            notification_type=notification_type,
            transports=transports,
        )

        business_settings = await self._tuner_client.fetch_settings(
            biz_id=recipient["biz_id"]
        )

        return [
            result
            for transport in transports
            for result in (
                await self._send_notification(
                    transport=transport,
                    recipient=recipient,
                    notification=notification,
                    notification_details=notification_details,
                    additional_contact_details=business_settings,
                )
                if self._check_notification_enabled_in_settings(
                    notification_type,
                    business_settings["notifications"],
                    business_settings["sms_notifications"],
                    transport,
                )
                else [{transport: NotificationDisabledInSettings()}]
            )
        ]

    @staticmethod
    def _check_notification_enabled_in_settings(
        notification_type: NotificationType,
        email_notification_settings: BusinessEmailNotificationSettings,
        sms_notification_settings: BusinessSmsNotificationSettings,
        transport: Transport,
    ) -> bool:
        if transport == Transport.SMS:
            return (
                notification_type == NotificationType.REQUEST_CREATED_FOR_BUSINESS
                and sms_notification_settings.request_created
            )
        elif transport == Transport.EMAIL:
            return (
                (
                    notification_type == NotificationType.ORDER_CREATED_FOR_BUSINESS
                    and email_notification_settings.order_created
                )
                or (
                    notification_type == NotificationType.ORDER_CANCELLED_FOR_BUSINESS
                    and email_notification_settings.order_cancelled
                )
                or (
                    notification_type == NotificationType.ORDER_CHANGED_FOR_BUSINESS
                    and email_notification_settings.order_changed
                )
                or (
                    notification_type
                    in (
                        NotificationType.CERTIFICATE_CREATED,
                        NotificationType.CERTIFICATE_PURCHASED,
                        NotificationType.FIRST_CERTIFICATE_APPROVED,
                        NotificationType.SUBSEQUENT_CERTIFICATE_APPROVED,
                        NotificationType.CERTIFICATE_EXPIRING,
                        NotificationType.CERTIFICATE_EXPIRED,
                        NotificationType.CERTIFICATE_REJECTED,
                        NotificationType.CERTIFICATE_CONNECT_PAYMENT,
                    )
                    and email_notification_settings.certificate_notifications
                )
                or (
                    notification_type == NotificationType.REQUEST_CREATED_FOR_BUSINESS
                    and email_notification_settings.request_created
                )
            )

    async def _send_notification(
        self,
        transport: Transport,
        recipient: dict,
        notification: dict,
        notification_details: dict,
        additional_contact_details: dict,
    ) -> list:
        sender_func = self._transport_senders[transport]
        try:
            send_result = await sender_func(
                recipient=recipient,
                notification=notification,
                notification_details=notification_details,
                additional_contact_details=additional_contact_details,
            )
        except TransportException as exc:
            return [{transport: exc}]
        else:
            if transport != Transport.EMAIL:
                return [{transport: send_result}]
            else:
                return [{transport: result} for result in send_result]

    def _check_transports_are_supported_for_notification(
        self,
        notification: dict,
        notification_type: NotificationType,
        transports: List[Transport],
    ) -> None:
        supported_transports = set(notification.keys())
        unsupported_transports = set(transports) - supported_transports
        if unsupported_transports:
            raise UnsupportedTransport(
                notification_type, sorted(unsupported_transports, key=lambda t: t.name)
            )

    async def _send_sms(
        self,
        recipient: dict,
        notification: dict,
        notification_details: dict,
        additional_contact_details: dict,
    ) -> dict:
        if "phone" in recipient:
            phone = recipient["phone"]
        else:
            try:
                phone = additional_contact_details["phone"]
            except KeyError:
                raise NoAddress

        if self._limit_recipients and phone not in await self._retrieve_yav_whitelist(
            "PHONE_RECIPIENTS_WHITELIST", int
        ):
            raise AddressNotAllowed

        template_name = notification[Transport.SMS]["template_name"]
        template = sms_templates.get_template(f"{template_name}.jinja")
        text = template.render(**notification_details)

        try:
            await self._yasms.send_sms(phone=phone, text=text)
            return {"phone": phone}
        except Exception:
            logging.getLogger("telegraphist.notification_router").exception(
                "sms send failed"
            )
            raise SendFailed

    async def _send_email(
        self,
        recipient: dict,
        notification: dict,
        notification_details: dict,
        additional_contact_details: dict,
    ) -> dict:
        if "email" in recipient:
            emails = [recipient["email"]]
        elif "email" in additional_contact_details:
            emails = [additional_contact_details["email"]]
        elif additional_contact_details.get("emails"):
            emails = additional_contact_details["emails"]
        else:
            raise NoAddress

        return await asyncio.gather(
            *[
                self.__send_email(
                    email=email,
                    notification_details=notification_details,
                    notification=notification,
                )
                for email in emails
            ],
            return_exceptions=True,
        )

    async def __send_email(
        self,
        email: str,
        notification: dict,
        notification_details: dict,
    ) -> dict:
        if self._limit_recipients and not email.endswith(
            self._allowed_recipient_domain
        ):
            raise AddressNotAllowed

        notification_settings = notification[Transport.EMAIL]
        if "context_processor" in notification_settings:
            notification_details = notification_settings["context_processor"](
                notification_details
            )

        subject, from_name, from_email = None, None, None
        if "subject_generator" in notification_settings:
            subject = notification_settings["subject_generator"](notification_details)

        if "from_generator" in notification_settings:
            from_name, from_email = notification_settings["from_generator"](
                notification_details
            )

        try:
            await self._email_client.send_message(
                to_email=email,
                template_code=notification_settings["template_code"],
                args=notification_details,
                subject=subject,
                from_email=from_email,
                from_name=from_name,
            )
            return {"email": email}
        except Exception:
            logging.getLogger("telegraphist.notification_router").exception(
                "email send failed"
            )
            raise SendFailed

    async def _send_push(
        self,
        recipient: dict,
        notification: dict,
        notification_details: dict,
        additional_contact_details: dict,
    ) -> dict:
        if "details_link" not in notification_details:
            raise NoOrderLink

        if "device_id" in recipient:
            receiver_data = {"device_id": recipient["device_id"]}
            if await self._forbid_to_be_pushed_by_device_id(receiver_data["device_id"]):
                raise AddressNotAllowed
        else:
            if "passport_uid" in recipient:
                receiver_data = {"passport_uid": recipient["passport_uid"]}
            else:
                try:
                    receiver_data = {
                        "passport_uid": additional_contact_details["passport_uid"]
                    }
                except KeyError:
                    raise NoAddress

            if await self._forbid_to_be_pushed_by_passport_uid(
                receiver_data["passport_uid"]
            ):
                raise AddressNotAllowed

        template_name = notification[Transport.PUSH]["template_name"]
        template = push_templates.get_template(f"{template_name}.jinja")

        try:
            await self._sup_client.send_push_notifications(
                receiver=[
                    aiosup.Receiver(
                        did=receiver_data.get("device_id"),
                        uid=str(receiver_data["passport_uid"])
                        if "passport_uid" in receiver_data
                        else None,
                    )
                ],
                notification=aiosup.Notification(
                    title=notification[Transport.PUSH]["title"],
                    body=template.render(**notification_details),
                    link=notification_details["details_link"],
                ),
            )
            return receiver_data
        except Exception:
            logging.getLogger("telegraphist.notification_router").exception(
                "push send failed"
            )
            raise SendFailed

    async def _forbid_to_be_pushed_by_device_id(self, device_id: str) -> bool:
        if not self._limit_recipients:
            return False

        return device_id not in await self._retrieve_yav_whitelist(
            "DEVICE_ID_RECIPIENTS_WHITELIST", str
        )

    async def _forbid_to_be_pushed_by_passport_uid(self, passport_uid: dict) -> bool:
        if not self._limit_recipients:
            return False

        return passport_uid not in await self._retrieve_yav_whitelist(
            "PASSPORT_UID_RECIPIENTS_WHITELIST", int
        )

    async def _retrieve_yav_whitelist(
        self, whitelist_key: str, converter: Callable
    ) -> List[int]:
        secret = await self._yav_client.retrieve_secret_head(self._yav_secret_id)

        whitelist = secret["value"].get(whitelist_key)
        if whitelist:
            return [converter(item.strip()) for item in whitelist.split(",")]
        else:
            return []
