from datetime import datetime
from enum import Enum, auto
from typing import List, Optional, Tuple

import aiohttp
from smb.common.http_client import HttpClient, collect_errors


class MailingListSource(Enum):
    YT = auto()
    IN_PLACE = auto()


class EmailSenderError(Exception):
    pass


class IncorrectParams(EmailSenderError):
    pass


class Client(HttpClient):
    def __init__(self, account_slug: str, account_token: str, **kwargs):
        self.account_slug = account_slug
        self._basic_auth = aiohttp.BasicAuth(account_token)
        super().__init__(**kwargs)

    @collect_errors
    async def schedule_promo_campaign(
        self,
        *,
        title: str,
        subject: str,
        from_email: str,
        from_name: str,
        body: str,
        mailing_list_source: MailingListSource,
        mailing_list_params: list,
        schedule_dt: datetime,
        tags: Tuple[str, ...] = tuple(),
        unsubscribe_list_slug: Optional[str],
        allowed_stat_domains: Optional[List[str]] = None,
    ) -> dict:
        """Создаёт и планирует промо-кампанию для одноразовой рассылки.

        https://github.yandex-team.ru/sendr/sendr/blob/master/docs/crm_integration.md#автоматизация-отправки-промо-кампаний # noqa

        :param title: название кампании (как она будет отображаться в интерфейсе Рассылятора).
        :param subject: тема письма.
        :param from_email: email отправителя.
        :param from_name: имя отправителя.
        :param body: содержание письма в виде текста с HTML разметкой.
        :param mailing_list_source: способ расчёта списка рассылки:
            YT -- через YT таблицу. Позволяет задать как email, так и passport_uid;
            IN_PLACE -- через параметр @mailing_list_params. Позволяет задать только email (но не passport_uid);
        :param mailing_list_params: зависит от @mailing_list_source
            * для YT: ["//path/to/yt/table/at/hahn"]
            * для IN_PLACE: [
                {
                    "email": "alice@example.com"
                },
                {
                    "email": "david@example.com",
                    "params": {"var": "val"}      # параметры для рендеринга шаблона письма
                },
                ...
            ]
        :param schedule_dt: дата и время старта кампании. Должно быть с явно заданной таймзоной (aware datetime).
            Если время указано в прошлом, то кампания будет запущена сразу.
        :param unsubscribe_list_slug: slug списка отписки, который нужно связать с рассылкой - на ящики из этого списка
            НЕ будут отправлены письма, а кликнувшие по Рассыляторской ссылке отписки из письма пользователи
            попадут в этот список. Если не задан, будет использован дефолтный self._default_unsubscribe_list_slug.
        :param tags: список тегов кампании. Объединяется с self._default_tags.
        :param allowed_stat_domains домены, на которые разрешен редирект при учете статистики
        :return: {
            "id": 1,
            "slug": "GUG2RFM2-MV51",
            "campaign_is_scheduled": true
        }
        """
        if not schedule_dt.tzinfo or schedule_dt.tzinfo.utcoffset(schedule_dt) is None:
            raise IncorrectParams("schedule_dt must be aware object")

        if not mailing_list_params:
            raise IncorrectParams("No params in mailing_list_params")

        segment_preparator = SEGMENT_PREPARATORS_MAP[mailing_list_source]

        data = dict(
            title=title,
            subject=subject,
            from_addr={"name": from_name, "email": from_email},
            letter_body=body,
            segment=segment_preparator(mailing_list_params),
            schedule_time=datetime.strftime(schedule_dt, "%Y-%m-%dT%H:%M:%S%z"),
            unsubscribe_list=unsubscribe_list_slug,
            tags=tags,
        )

        if allowed_stat_domains is not None:
            data["allowed_stat_domains"] = allowed_stat_domains

        return await self.request(
            method="POST",
            uri=f"/api/0/{self.account_slug}/automation/promoletter",
            expected_statuses=[200],
            auth=self._basic_auth,
            json=data,
            metric_name="/api/0/{account_slug}/automation/promoletter"
        )

    @staticmethod
    def _prepare_yt_segment(params: list) -> dict:
        if len(params) != 1 or not isinstance(params[0], str):
            raise IncorrectParams(
                "For YT mailing list source mailing_list_params "
                "must be single-element list with path to YT table."
            )

        return {"template": "ytlist", "params": {"path": params[0]}}

    @staticmethod
    def _prepare_singleuselist_segment(params: list) -> dict:
        for param in params:
            if not isinstance(param, dict) or not set(param.keys()) <= {
                "email",
                "params",
            }:
                raise IncorrectParams(
                    "Bad mailing_list_params format. "
                    "Must be list of dicts with only email/params keys."
                )

        return {"template": "singleuselist", "params": {"recipients": params}}

    @collect_errors
    async def send_message(
        self,
        *,
        args: dict = None,
        asynchronous: bool = True,
        for_testing: bool = False,
        template_code: str,
        to_email: str,
        subject: Optional[str] = None,
        from_email: Optional[str] = None,
        from_name: Optional[str] = None,
    ):
        """Отправляет транзакционное письмо. API поддержано частично.
        https://github.yandex-team.ru/sendr/sendr/blob/master/docs/transaction-api.md

        :param args: подстановочные параметры, которые будут подставлены в тело шаблона.
        :param asynchronous: при асинхронной отправке будет создана задача в бэкенд
        доставки и API вернет 200 ОК. Задача отправки будет выполнена в ближайшее
        возможное время, пропорциональное отношению загруженности очереди к скорости ее
        разгребания воркерами. Сервис не рекомендует синхронный режим.
        :param for_testing: позволяет отправить письмо не опубликованной кампании,
        но возможные получатели ограничены.
        :param template_code: код шаблона, который можно получить на вкладке "Параметры
        API", он же campaign_slug.
        :param to_email: получатель письма.
        :param subject: тема письма (если не передано или None, будет взять тема,
        введённая для этой рассылки в Рассыляторе).
        :param from_email: email, от которого будет отправлено сообщение (если
        не передано или None, будет использованы данные этой рассылки в Рассыляторе).
        :param from_name: имя, от которого будет отправлено сообщение (если
        не передано или None, будет использованы данные этой рассылки в Рассыляторе).
        Может быть использовано только при указании from_email
        """
        data = {"async": asynchronous, "to_email": to_email}
        if args:
            data["args"] = args
        if for_testing:
            data["for_testing"] = for_testing
        if from_email is not None:
            data["from_email"] = from_email
        if from_name is not None:
            if from_email is None:
                raise IncorrectParams
            data["from_name"] = from_name

        headers = {}
        if subject is not None:
            headers["Subject"] = subject

        await self.request(
            method="POST",
            uri=f"/api/0/{self.account_slug}/transactional/{template_code}/send",
            expected_statuses=[200],
            auth=self._basic_auth,
            json=data,
            metric_name="/api/0/{account_slug}/transactional/{template_code}/send",
        )


SEGMENT_PREPARATORS_MAP = {
    MailingListSource.YT: Client._prepare_yt_segment,
    MailingListSource.IN_PLACE: Client._prepare_singleuselist_segment,
}
