import dataclasses
from typing import List, Optional

import aiohttp
from marshmallow import fields, post_load, pre_dump
from smb.common.http_client import BaseHttpClientException, collect_errors

from smb.common.aiotvm import HttpClientWithTvm
from maps_adv.common.protomallow import ProtobufSchema
from maps_adv.geosmb.tuner.proto import errors_pb2, settings_pb2

__all__ = [
    "TunerClient",
    "BusinessEmailNotificationSettings",
    "BusinessSmsNotificationSettings",
    "BadRequest",
    "BadParams",
    "UnknownBizId",
    "BookingSettings",
    "OnlineBookingSettings",
    "RequestsSettings",
]


class TunerClientException(BaseHttpClientException):
    pass


class BadRequest(TunerClientException):
    pass


class BadParams(TunerClientException):
    pass


class UnknownBizId(TunerClientException):
    pass


@dataclasses.dataclass
class BusinessEmailNotificationSettings:
    order_created: bool = True
    order_cancelled: bool = True
    order_changed: bool = True
    certificate_notifications: bool = True
    request_created: bool = True


@dataclasses.dataclass
class BusinessSmsNotificationSettings:
    request_created: bool = True


@dataclasses.dataclass
class OnlineBookingSettings:
    export_to_ya_services: bool = False


@dataclasses.dataclass
class BookingSettings:
    enabled: bool = True
    slot_interval: int = 30
    online_settings: OnlineBookingSettings = dataclasses.field(
        default_factory=OnlineBookingSettings
    )


@dataclasses.dataclass
class RequestsSettings:
    enabled: bool = True
    button_text: str = "Отправить заявку"


@dataclasses.dataclass
class Contacts:
    emails: List[str]
    phones: List[int]
    telegram_logins: List[str]


class EmailNotificationsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.EmailNotifications

    order_created = fields.Bool(required=True)
    order_cancelled = fields.Bool(required=True)
    order_changed = fields.Bool(required=True)
    certificate_notifications = fields.Bool(required=True)
    request_created = fields.Bool()


class SmsNotificationsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.SmsNotifications

    request_created = fields.Bool(required=True)


class OnlineBookingSettingsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.OnlineBookingSettings

    export_to_ya_services = fields.Bool(required=True)


class BookingSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.Booking

    enabled = fields.Bool(required=True)
    slot_interval = fields.Integer(required=True)
    online_settings = fields.Nested(OnlineBookingSettingsSchema)

    @post_load
    def _load_data(self, data: dict) -> dict:
        data["online_settings"] = OnlineBookingSettings(**data["online_settings"])

        return data


class RequestsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.Requests

    enabled = fields.Bool(required=True)
    button_text = fields.String(required=True)


class SettingsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.Settings

    emails = fields.List(fields.String())
    phone = fields.Integer()
    notifications = fields.Nested(EmailNotificationsSchema, required=True)
    sms_notifications = fields.Nested(SmsNotificationsSchema)
    booking = fields.Nested(BookingSchema)
    requests = fields.Nested(RequestsSchema)

    @pre_dump
    def _dump_data(self, data: dict) -> dict:
        data["notifications"] = dataclasses.asdict(data["notifications"])
        data["sms_notifications"] = dataclasses.asdict(data["sms_notifications"])
        if data.get("booking"):
            data["booking"] = dataclasses.asdict(data["booking"])
        if data.get("requests"):
            data["requests"] = dataclasses.asdict(data["requests"])

        return data

    @post_load
    def _load_data(self, data: dict) -> dict:
        data["notifications"] = BusinessEmailNotificationSettings(
            **data["notifications"]
        )
        data["sms_notifications"] = BusinessSmsNotificationSettings(
            **data["sms_notifications"]
        )
        data["booking"] = BookingSettings(**data["booking"])
        data["requests"] = RequestsSettings(**data["requests"])

        return data


class UpdateSettingsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.UpdateSettings

    biz_id = fields.Integer(required=True)
    settings = fields.Nested(SettingsSchema, required=True)


class ContactsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.Contacts

    emails = fields.List(
        fields.String(), required=True
    )
    phones = fields.List(
        fields.Integer()
    )
    telegram_logins = fields.List(
        fields.String()
    )


class SettingsV2Schema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.SettingsV2

    contacts = fields.Nested(ContactsSchema, required=True)
    notifications = fields.Nested(EmailNotificationsSchema, required=True)
    sms_notifications = fields.Nested(SmsNotificationsSchema)
    booking = fields.Nested(BookingSchema)
    requests = fields.Nested(RequestsSchema)

    @pre_dump
    def _dump_data(self, data: dict) -> dict:
        data["contacts"] = dataclasses.asdict(data["contacts"])
        data["notifications"] = dataclasses.asdict(data["notifications"])
        data["sms_notifications"] = dataclasses.asdict(data["sms_notifications"])
        if data.get("booking"):
            data["booking"] = dataclasses.asdict(data["booking"])
        if data.get("requests"):
            data["requests"] = dataclasses.asdict(data["requests"])

        return data

    @post_load
    def _load_data(self, data: dict) -> dict:
        data["contacts"] = Contacts(
            **data["contacts"]
        )
        data["notifications"] = BusinessEmailNotificationSettings(
            **data["notifications"]
        )
        data["sms_notifications"] = BusinessSmsNotificationSettings(
            **data["sms_notifications"]
        )
        data["booking"] = BookingSettings(**data["booking"])
        data["requests"] = RequestsSettings(**data["requests"])

        return data


class UpdateSettingsV2Schema(ProtobufSchema):
    class Meta:
        pb_message_class = settings_pb2.UpdateSettingsV2

    biz_id = fields.Integer(required=True)
    settings = fields.Nested(SettingsV2Schema, required=True)


class TunerClient(HttpClientWithTvm):
    @collect_errors
    async def fetch_settings(self, biz_id: int) -> dict:
        response_body = await self.request(
            method="POST",
            uri="/v1/fetch_settings/",
            expected_statuses=[200],
            data=settings_pb2.FetchSettings(biz_id=biz_id).SerializeToString(),
            metric_name="/v1/fetch_settings/",
        )

        return SettingsSchema().from_bytes(response_body)

    @collect_errors
    async def update_settings(
        self,
        biz_id: int,
        emails: List[str],
        phone: Optional[int],
        notification_settings: BusinessEmailNotificationSettings,
        sms_notification_settings: BusinessSmsNotificationSettings,
        booking_settings: Optional[BookingSettings] = None,
        requests_settings: Optional[RequestsSettings] = None,
    ) -> dict:
        if not emails:
            raise BadParams("Emails list can't be empty.")

        data = {
            "biz_id": biz_id,
            "settings": {
                "emails": emails,
                "phone": phone,
                "notifications": notification_settings,
                "sms_notifications": sms_notification_settings,
                "booking": booking_settings,
                "requests": requests_settings,
            },
        }

        response_body = await self.request(
            method="POST",
            uri="/v1/update_settings/",
            expected_statuses=[200],
            data=UpdateSettingsSchema().to_bytes(data),
            metric_name="/v1/update_settings/",
        )

        return SettingsSchema().from_bytes(response_body)

    @collect_errors
    async def fetch_settings_v2(self, biz_id: int) -> dict:
        response_body = await self.request(
            method="POST",
            uri="/v2/fetch_settings/",
            expected_statuses=[200],
            data=settings_pb2.FetchSettings(biz_id=biz_id).SerializeToString(),
            metric_name="/v2/fetch_settings/",
        )

        return SettingsV2Schema().from_bytes(response_body)

    @collect_errors
    async def update_settings_v2(
        self,
        biz_id: int,
        contacts: Contacts,
        notification_settings: BusinessEmailNotificationSettings,
        sms_notification_settings: BusinessSmsNotificationSettings,
        booking_settings: Optional[BookingSettings] = None,
        requests_settings: Optional[RequestsSettings] = None,
    ) -> dict:
        if not contacts or not contacts.emails:
            raise BadParams("Emails list can't be empty.")

        data = {
            "biz_id": biz_id,
            "settings": {
                "contacts": contacts,
                "notifications": notification_settings,
                "sms_notifications": sms_notification_settings,
                "booking": booking_settings,
                "requests": requests_settings,
            },
        }

        response_body = await self.request(
            method="POST",
            uri="/v2/update_settings/",
            expected_statuses=[200],
            data=UpdateSettingsV2Schema().to_bytes(data),
            metric_name="/v2/update_settings/",
        )

        return SettingsV2Schema().from_bytes(response_body)

    @collect_errors
    async def update_telegram_user(self, user_id: int, user_login: str) -> dict:
        return await self.request(
            method="POST",
            uri="/v2/update_telegram_user/",
            expected_statuses=[200],
            json={
                "user_id": user_id,
                "user_login": user_login
            },
            metric_name="/v2/update_telegram_user/",
        )

    @collect_errors
    async def delete_telegram_user(self, user_id: int) -> None:
        await self.request(
            method="POST",
            uri="/v2/delete_telegram_user/",
            expected_statuses=[200],
            json={
                "user_id": user_id
            },
            metric_name="/v2/delete_telegram_user/",
        )

    async def _handle_custom_errors(self, response: aiohttp.ClientResponse) -> None:
        self._raise_for_matched_exception(
            response.status,
            await response.content.read(),
        )
        await self._raise_unknown_response(response)

    @staticmethod
    def _raise_for_matched_exception(status: int, exception_body: bytes) -> None:
        if status != 400:
            return

        error_pb = errors_pb2.Error.FromString(exception_body)
        description = error_pb.description
        if error_pb.code == errors_pb2.Error.UNKNOWN_BIZ_ID:
            raise UnknownBizId(description)
        else:
            raise BadRequest(description)
