import json
from enum import Enum, auto, unique
from json import JSONDecodeError
from typing import AsyncIterator, Dict, List

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

from smb.common.aiotvm import HttpClientWithTvm
from maps_adv.common.protomallow import PbDateTimeField, PbEnumField, ProtobufSchema
from maps_adv.geosmb.clients.facade.proto import facade_pb2

__all__ = [
    "BadFacadeResponse",
    "CouponCoverType",
    "CouponDistribution",
    "CouponModerationStatus",
    "CouponStatus",
    "CouponType",
    "Currency",
    "FacadeIntClient",
]


@unique
class CouponStatus(Enum):
    NOT_RUNNING_YET = auto()
    RUNNING = auto()
    FINISHED = auto()


@unique
class Currency(Enum):
    RUB = auto()
    KZT = auto()
    BYN = auto()
    TRY = auto()
    USD = auto()
    EUR = auto()


@unique
class CouponDistribution(Enum):
    PUBLIC = auto()
    PRIVATE_FOR_SEGMENTS = auto()
    PRIVATE_BY_LINK = auto()


@unique
class CouponModerationStatus(Enum):
    IN_REVIEW = auto()
    IN_APPROVED = auto()
    IN_REJECTED = auto()


@unique
class CouponCoverType(Enum):
    DEFAULT = auto()
    SQUARE = auto()
    SMALL_SQUARE = auto()
    HORIZONTAL = auto()
    VERTICAL = auto()
    LOGO = auto()


@unique
class CouponType(Enum):
    FREE = auto()
    SERVICE = auto()


@unique
class SegmentType(Enum):
    UNKNOWN = auto()
    REGULAR = auto()
    ACTIVE = auto()
    LOST = auto()
    NO_ORDERS = auto()
    UNPROCESSED_ORDERS = auto()
    PROSPECTIVE = auto()
    LOYAL = auto()
    DISLOYAL = auto()
    MISSED_LAST_CALL = auto()
    SHORT_LAST_CALL = auto()


PROTO_TO_ENUM_MAP = {
    "coupon_status": [
        (facade_pb2.CouponStatus.NOT_RUNNING_YET, CouponStatus.NOT_RUNNING_YET),
        (facade_pb2.CouponStatus.RUNNING, CouponStatus.RUNNING),
        (facade_pb2.CouponStatus.FINISHED, CouponStatus.FINISHED),
    ],
    "currency": [
        (facade_pb2.Currency.RUB, Currency.RUB),
        (facade_pb2.Currency.KZT, Currency.KZT),
        (facade_pb2.Currency.BYN, Currency.BYN),
        (facade_pb2.Currency.TRY, Currency.TRY),
        (facade_pb2.Currency.USD, Currency.USD),
        (facade_pb2.Currency.EUR, Currency.EUR),
    ],
    "coupon_distribution": [
        (facade_pb2.CouponDistribution.PUBLIC, CouponDistribution.PUBLIC),
        (
            facade_pb2.CouponDistribution.PRIVATE_FOR_SEGMENTS,
            CouponDistribution.PRIVATE_FOR_SEGMENTS,
        ),
        (
            facade_pb2.CouponDistribution.PRIVATE_BY_LINK,
            CouponDistribution.PRIVATE_BY_LINK,
        ),
    ],
    "coupon_moderation_status": [
        (
            facade_pb2.CouponModerationStatus.IN_REVIEW,
            CouponModerationStatus.IN_REVIEW,
        ),
        (
            facade_pb2.CouponModerationStatus.IN_APPROVED,
            CouponModerationStatus.IN_APPROVED,
        ),
        (
            facade_pb2.CouponModerationStatus.IN_REJECTED,
            CouponModerationStatus.IN_REJECTED,
        ),
    ],
    "coupon_cover_type": [
        (facade_pb2.CouponCoverType.DEFAULT, CouponCoverType.DEFAULT),
        (facade_pb2.CouponCoverType.SQUARE, CouponCoverType.SQUARE),
        (facade_pb2.CouponCoverType.SMALL_SQUARE, CouponCoverType.SMALL_SQUARE),
        (facade_pb2.CouponCoverType.HORIZONTAL, CouponCoverType.HORIZONTAL),
        (facade_pb2.CouponCoverType.VERTICAL, CouponCoverType.VERTICAL),
        (facade_pb2.CouponCoverType.LOGO, CouponCoverType.LOGO),
    ],
    "coupon_type": [
        (facade_pb2.CouponsWithSegmentsOutput.CouponType.FREE, CouponType.FREE),
        (facade_pb2.CouponsWithSegmentsOutput.CouponType.SERVICE, CouponType.SERVICE),
    ],
    "segment_type": [
        (facade_pb2.SegmentType.UNKNOWN, SegmentType.UNKNOWN),
        (facade_pb2.SegmentType.REGULAR, SegmentType.REGULAR),
        (facade_pb2.SegmentType.ACTIVE, SegmentType.ACTIVE),
        (facade_pb2.SegmentType.LOST, SegmentType.LOST),
        (facade_pb2.SegmentType.NO_ORDERS, SegmentType.NO_ORDERS),
        (facade_pb2.SegmentType.UNPROCESSED_ORDERS, SegmentType.UNPROCESSED_ORDERS),
        (facade_pb2.SegmentType.PROSPECTIVE, SegmentType.PROSPECTIVE),
        (facade_pb2.SegmentType.LOYAL, SegmentType.LOYAL),
        (facade_pb2.SegmentType.DISLOYAL, SegmentType.DISLOYAL),
        (facade_pb2.SegmentType.MISSED_LAST_CALL, SegmentType.MISSED_LAST_CALL),
        (facade_pb2.SegmentType.SHORT_LAST_CALL, SegmentType.SHORT_LAST_CALL),
    ],
}


class FacadeIntException(Exception):
    pass


class InvalidParams(FacadeIntException):
    pass


class BadFacadeResponse(FacadeIntException):
    pass


class ValidationException(FacadeIntException):
    pass


class OrganizationWithCouponsSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            facade_pb2.OrganizationsWithCouponsResponse.OrganizationWithCoupons
        )

    biz_id = fields.String(required=True)
    permalink = fields.String(required=True)
    showcase_url = fields.String(required=True)


class OrganizationsWithCouponsResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.OrganizationsWithCouponsResponse

    organizations_list = fields.Nested(OrganizationWithCouponsSchema, many=True)


class OrganizationWithBookingSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            facade_pb2.OrganizationsWithBookingResponse.OrganizationWithBooking
        )

    biz_id = fields.String(required=True)
    permalink = fields.String(required=True)
    booking_url = fields.String(required=True)


class OrganizationsWithBookingResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.OrganizationsWithBookingResponse

    organizations_list = fields.Nested(OrganizationWithBookingSchema, many=True)


class CouponsStatusesListRequestSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsStatusesListRequest

    items_ids_list = fields.List(fields.String())

    @pre_dump
    def _normalize(self, data: dict) -> dict:
        return {"items_ids_list": [str(i) for i in data]}


class CouponStatusInfoSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsStatusesListResponse.CouponStatusInfo

    biz_id = fields.String(required=True)
    item_id = fields.String(required=True)
    status = PbEnumField(
        enum=CouponStatus,
        pb_enum=facade_pb2.CouponStatus,
        values_map=PROTO_TO_ENUM_MAP["coupon_status"],
        required=True,
    )

    @post_load
    def _normalize(self, data: dict) -> dict:
        data["biz_id"] = int(data["biz_id"])
        data["coupon_id"] = int(data.pop("item_id"))

        return data


class CouponsStatusesListResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsStatusesListResponse

    coupons_statuses_list = fields.Nested(CouponStatusInfoSchema, many=True)

    @post_load
    def _to_flat(self, data: dict) -> dict:
        return data["coupons_statuses_list"]


class LoyaltyItemForSnapshotSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            facade_pb2.LoyaltyItemsListForSnapshotResponse.LoyaltyItemForSnapshot
        )

    client_id = fields.Integer(required=True)
    issued_at = PbDateTimeField(required=True)
    id = fields.Integer(required=True)
    type = fields.String(required=True)
    data = fields.String(required=True)

    @post_load
    def _normalize_data(self, data: dict) -> dict:
        try:
            data["data"] = json.loads(data["data"])
        except JSONDecodeError:
            raise ValidationException(f"data field must be json-string: {data}")

        return data


class LoyaltyItemsListForSnapshotResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.LoyaltyItemsListForSnapshotResponse

    items_list = fields.Nested(LoyaltyItemForSnapshotSchema, many=True)


class CostSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.Cost

    currency = PbEnumField(
        enum=Currency,
        pb_enum=facade_pb2.Currency,
        values_map=PROTO_TO_ENUM_MAP["currency"],
        required=True,
    )
    cost = fields.String(required=True)


class CouponServiceSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.BusinessCouponsForSnapshotResponse.CouponService

    service_id = fields.String(required=True)
    level = fields.Integer(required=True)
    price = fields.Nested(CostSchema, required=True)
    name = fields.String(required=True)
    duration = fields.Integer(required=True)


class CouponCoverTemplateSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponCoverTemplate

    url_template = fields.String(required=True)
    aliases = fields.List(fields.String())
    type = PbEnumField(
        enum=CouponCoverType,
        pb_enum=facade_pb2.CouponCoverType,
        values_map=PROTO_TO_ENUM_MAP["coupon_cover_type"],
        required=True,
    )


class CouponItemForSnapshotSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            facade_pb2.BusinessCouponsForSnapshotResponse.CouponItemForSnapshot
        )

    biz_id = fields.Integer(required=True)
    item_id = fields.Integer(required=True)
    title = fields.String(required=True)
    services = fields.Nested(CouponServiceSchema, many=True)
    products_description = fields.String(required=False)
    price = fields.Nested(CostSchema, required=True)
    discount = fields.Integer(required=True)
    discounted_price = fields.Nested(CostSchema, required=True)
    start_date = PbDateTimeField(required=True)
    get_until_date = PbDateTimeField(required=True)
    end_date = PbDateTimeField(required=True)
    distribution = PbEnumField(
        enum=CouponDistribution,
        pb_enum=facade_pb2.CouponDistribution,
        values_map=PROTO_TO_ENUM_MAP["coupon_distribution"],
        required=True,
    )
    moderation_status = PbEnumField(
        enum=CouponModerationStatus,
        pb_enum=facade_pb2.CouponModerationStatus,
        values_map=PROTO_TO_ENUM_MAP["coupon_moderation_status"],
        required=True,
    )
    published = fields.Boolean(required=True)
    payments_enabled = fields.Boolean(required=True)
    conditions = fields.String(required=False)
    creator_login = fields.String(required=False)
    creator_uid = fields.String(required=False)
    created_at = PbDateTimeField(required=True)
    cover_templates = fields.Nested(CouponCoverTemplateSchema, many=True)
    coupon_showcase_url = fields.String()
    meta = fields.String()

    @post_load()
    def meta_json(self, data: dict) -> dict:
        try:
            data["meta"] = json.loads(data["meta"])
        except ValueError:
            raise marshmallow.ValidationError("meta must be a valid JSON")

        return data


class BusinessCouponsForSnapshotResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.BusinessCouponsForSnapshotResponse

    items_list = fields.Nested(CouponItemForSnapshotSchema, many=True)


class CouponSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsWithSegmentsOutput.Coupon

    coupon_id = fields.Integer(required=True, load_from="id")
    type = PbEnumField(
        enum=CouponType,
        pb_enum=facade_pb2.CouponsWithSegmentsOutput.CouponType,
        values_map=PROTO_TO_ENUM_MAP["coupon_type"],
        required=True,
    )
    segments = fields.List(
        PbEnumField(
            enum=SegmentType,
            pb_enum=facade_pb2.SegmentType,
            values_map=PROTO_TO_ENUM_MAP["segment_type"],
        )
    )
    cost_discount = fields.Nested(CostSchema())
    percent_discount = fields.Integer()

    @validates_schema
    def _validate(self, data: dict):
        if not data["segments"]:
            raise marshmallow.ValidationError(
                f"Coupon coupon_id={data['coupon_id']} has no segments"
            )

    @post_load
    def _to_flat(self, data: dict):
        cost_discount = data.pop("cost_discount", {})
        if cost_discount:
            data["currency"] = cost_discount["currency"]
            data["cost_discount"] = cost_discount["cost"]


class BusinessSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsWithSegmentsOutput.Business

    biz_id = fields.Integer(required=True)
    coupons = fields.Nested(CouponSchema, many=True)

    @validates_schema
    def _validate(self, data: dict):
        if not data["coupons"]:
            raise marshmallow.ValidationError(
                f"Business biz_id={data['biz_id']} has no coupons"
            )


class CouponsWithSegmentsOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponsWithSegmentsOutput

    businesses = fields.Nested(BusinessSchema, many=True)


class CouponPromotionItemSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.CouponPromotionItem

    advert_id = fields.Integer(required=True)
    coupon_id = fields.Integer(required=True)
    biz_id = fields.Integer()
    name = fields.String(required=True)
    date_from = PbDateTimeField(required=True)
    date_to = PbDateTimeField(required=True)
    description = fields.String(required=True)
    announcement = fields.String(required=True)
    image_url = fields.String(required=True)
    coupon_url = fields.String(required=True)


class FetchCouponPromotionsOutputSchema(ProtobufSchema):
    class Meta:
        pb_message_class = facade_pb2.FetchCouponPromotionsOutput

    promotions = fields.Nested(CouponPromotionItemSchema, many=True)

    @post_load
    def _to_flat(self, data: dict) -> dict:
        return data["promotions"]


class FacadeIntClient(HttpClientWithTvm):
    DEFAULT_LIMIT = 500

    @collect_errors
    async def get_organizations_with_coupons(self) -> AsyncIterator[List[dict]]:
        offset = 0

        while True:
            organizations_list = await self._get_organizations_with_coupons(
                limit=self.DEFAULT_LIMIT, offset=offset
            )

            if not organizations_list:
                break

            offset += self.DEFAULT_LIMIT
            yield organizations_list

    async def _get_organizations_with_coupons(
        self, limit: int, offset: int
    ) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v1/get_organizations_with_coupons",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.OrganizationsWithCouponsRequest(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/get_organizations_with_coupons",
        )

        try:
            result = OrganizationsWithCouponsResponseSchema().from_bytes(got)
            return result["organizations_list"]
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def get_organizations_with_booking(self) -> AsyncIterator[List[dict]]:
        offset = 0

        while True:
            organizations_list = await self._get_organizations_with_booking(
                limit=self.DEFAULT_LIMIT, offset=offset
            )

            if not organizations_list:
                break

            offset += self.DEFAULT_LIMIT
            yield organizations_list

    async def _get_organizations_with_booking(
        self, limit: int, offset: int
    ) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v1/get_organizations_with_booking",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.OrganizationsWithBookingRequest(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/get_organizations_with_booking",
        )

        try:
            result = OrganizationsWithBookingResponseSchema().from_bytes(got)
            return result["organizations_list"]
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def list_coupons_statuses(self, coupon_ids: List[int]) -> List[dict]:
        if not coupon_ids:
            raise InvalidParams("coupon_ids param can't be empty")

        got = await self.request(
            method="POST",
            uri="/v1/get_coupons_statuses_list",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=CouponsStatusesListRequestSchema().to_bytes(coupon_ids),
            metric_name="/v1/get_coupons_statuses_list",
        )

        try:
            return CouponsStatusesListResponseSchema().from_bytes(got)
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def get_loyalty_items_list_for_snapshot(self) -> AsyncIterator[List[dict]]:
        offset = 0

        while True:
            items = await self._get_loyalty_items_list_for_snapshot(
                limit=self.DEFAULT_LIMIT, offset=offset
            )

            if not items:
                break

            offset += self.DEFAULT_LIMIT
            yield items

    async def _get_loyalty_items_list_for_snapshot(
        self, limit: int, offset: int
    ) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v1/get_loyalty_items_list_for_snapshot",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.LoyaltyItemsListForSnapshotRequest(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/get_loyalty_items_list_for_snapshot",
        )

        try:
            result = LoyaltyItemsListForSnapshotResponseSchema().from_bytes(got)
            return result["items_list"]
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def get_business_coupons_for_snapshot(self) -> AsyncIterator[List[dict]]:
        offset = 0
        limit = 1000

        while True:
            items = await self._get_business_coupons_for_snapshot(
                limit=limit, offset=offset
            )

            if not items:
                break

            offset += limit
            yield items

    async def _get_business_coupons_for_snapshot(
        self, limit: int, offset: int
    ) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v1/get_business_coupons_for_snapshot",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.BusinessCouponsForSnapshotRequest(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/get_business_coupons_for_snapshot",
        )

        try:
            result = BusinessCouponsForSnapshotResponseSchema().from_bytes(got)
            return result["items_list"]
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def list_coupons_with_segments(self) -> AsyncIterator[Dict[str, List[dict]]]:
        offset = 0
        # too complex request on Facade side, need small pagination for preventing timeouts
        limit = 200

        while True:
            items = await self._list_coupons_with_segments(limit=limit, offset=offset)

            if not items:
                break

            offset += limit
            yield items

    async def _list_coupons_with_segments(
        self, limit: int, offset: int
    ) -> Dict[str, List[dict]]:
        got = await self.request(
            method="POST",
            uri="/v1/get_coupons_with_segments",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.CouponsWithSegmentsInput(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/get_coupons_with_segments",
        )

        try:
            result = CouponsWithSegmentsOutputSchema().from_bytes(got)
            return {biz["biz_id"]: biz["coupons"] for biz in result["businesses"]}
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    @collect_errors
    async def fetch_coupon_promotions(self) -> AsyncIterator[List[dict]]:
        offset = 0

        while True:
            promotions = await self._fetch_coupon_promotions(
                limit=self.DEFAULT_LIMIT, offset=offset
            )

            if not promotions:
                break

            offset += self.DEFAULT_LIMIT
            yield promotions

    async def _fetch_coupon_promotions(self, limit: int, offset: int) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v1/fetch_coupon_promotions",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=facade_pb2.FetchCouponPromotionsInput(
                paging=facade_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v1/fetch_coupon_promotions",
        )

        try:
            return FetchCouponPromotionsOutputSchema().from_bytes(got)
        except marshmallow.ValidationError as e:
            raise BadFacadeResponse(e.messages)

    async def _make_headers(self) -> dict:
        return {"Content-Type": "application/x-protobuf"}
