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

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.clients.notify_me import NotifyMeClient

from maps_adv.geosmb.telegraphist.server.lib.enums import NotificationType, Transport
from maps_adv.geosmb.telegraphist.server.lib.templates import (
    email_context_processor,
    env as sms_templates,
)
from maps_adv.geosmb.telegraphist.server.lib.exceptions import (
    UnsupportedTransport,
    UnsupportedNotificationType,
    TransportException,
    NoAddress,
    AddressNotAllowed,
    SendFailed,
)


supported_notifications = {
    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"},
        Transport.TELEGRAM: {"template_name": "request_created_for_business_telegram"},
    },
    NotificationType.CART_ORDER_CREATED: {
        Transport.EMAIL: {
            "context_processor": lambda ctx: email_context_processor(
                "cart_order_created", ctx
            ),
            "subject_generator": lambda ctx: f"Новый заказ на {ctx['landing']['domain']}",
            "from_generator": lambda ctx: (
                "Яндекс Бизнес",
                "orders@maps.yandex.ru",
            ),
        },
        Transport.SMS: {"template_name": "request_created_for_business_sms"},
        Transport.TELEGRAM: {"template_name": "request_created_for_business_telegram"},
    },
}


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

    def __init__(
        self,
        *,
        email_client: EmailClient,
        yasms: YasmsClient,
        telegram_client: NotifyMeClient,
        email_template_codes: Dict[str, Dict[str, str]],
        yav_client: YavClient,
        yav_secret_id: str,
        limit_recipients: bool = False,
    ):
        self._email_client = email_client
        self._yasms = yasms
        self._telegram_client = telegram_client
        self._yav_client = yav_client
        self._yav_secret_id = yav_secret_id
        self._limit_recipients = limit_recipients

        self._transport_senders = {
            Transport.SMS: self._send_sms,
            Transport.EMAIL: self._send_email,
            Transport.TELEGRAM: self._send_telegram,
        }

        self._supported_notifications = deepcopy(supported_notifications)
        for notification, template_code in email_template_codes.items():
            notification_type = NotificationType(notification)
            notification_settings = self._supported_notifications[
                notification_type
            ][Transport.EMAIL]
            notification_settings["template_code"] = template_code

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

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

        self._check_transport_is_supported_for_notification(
            notification=notification,
            notification_type=notification_type,
            transport_type=transport["type"],
        )

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

    def _check_transport_is_supported_for_notification(
        self,
        notification: dict,
        notification_type: NotificationType,
        transport_type: Transport,
    ) -> None:
        supported_transports = set(notification.keys())
        if transport_type not in supported_transports:
            raise UnsupportedTransport(
                notification_type, [transport_type]
            )

    async def _send_sms(
        self,
        transport: dict,
        notification: dict,
        notification_details: dict,
    ) -> dict:
        if "phones" not in transport:
            raise NoAddress

        phones = transport["phones"]

        return await asyncio.gather(
            *[
                self.__send_sms(
                    phone=phone,
                    notification_details=notification_details,
                    notification=notification,
                )
                for phone in phones
            ],
            return_exceptions=True,
        )

    async def __send_sms(
        self,
        phone: int,
        notification: dict,
        notification_details: dict,
    ) -> dict:
        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,
        transport: dict,
        notification: dict,
        notification_details: dict,
    ) -> dict:
        if "emails" not in transport:
            raise NoAddress

        emails = transport["emails"]

        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_telegram(
            self,
            transport: dict,
            notification: dict,
            notification_details: dict,
    ) -> dict:
        if "telegram_uids" not in transport:
            raise NoAddress

        telegram_uids = transport["telegram_uids"]

        return await asyncio.gather(
            *[
                self.__send_telegram(
                    telegram_uid=telegram_uid,
                    notification_details=notification_details,
                    notification=notification,
                )
                for telegram_uid in telegram_uids
            ],
            return_exceptions=True,
        )

    async def __send_telegram(
            self,
            telegram_uid: int,
            notification: dict,
            notification_details: dict,
    ) -> dict:
        if self._limit_recipients and telegram_uid not in await self._retrieve_yav_whitelist(
                "TELEGRAM_UIDS_RECIPIENTS_WHITELIST", int
        ):
            raise AddressNotAllowed

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

        try:
            await self._telegram_client.send_message(telegram_uid=telegram_uid, text=text)
            return {"telegram_uid": telegram_uid}
        except Exception:
            logging.getLogger("telegraphist.notification_router").exception(
                f"Telegram send failed {telegram_uid}"
            )
            raise SendFailed

    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 []
