from enum import Enum, auto, unique
from typing import List

import marshmallow
from marshmallow import fields, validate
from smb.common.http_client import collect_errors

from smb.common.aiotvm import HttpClientWithTvm
from maps_adv.common.protomallow import PbEnumField, ProtobufSchema
from maps_adv.geosmb.clients.loyalty.proto import loyalty_pb2

__all__ = [
    "LoyaltyIntClient",
    "BadLoyaltyResponse",
    "CouponReviewResolution",
    "LoyaltyIntException",
]


@unique
class CouponReviewResolution(Enum):
    APPROVED = auto()
    REJECTED = auto()


PROTO_TO_ENUM_MAP = {
    "coupon_review_resolution": [
        (loyalty_pb2.CouponReviewResolution.APPROVED, CouponReviewResolution.APPROVED),
        (loyalty_pb2.CouponReviewResolution.REJECTED, CouponReviewResolution.REJECTED),
    ]
}


class LoyaltyIntException(Exception):
    pass


class BadLoyaltyResponse(LoyaltyIntException):
    pass


class EmptyCouponsList(LoyaltyIntException):
    pass


class IncorrectCouponData(LoyaltyIntException):
    pass


class CouponReviewCorrectedSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            loyalty_pb2.SubmitCouponsReviewsListRequest.CouponReviewCorrected
        )

    title = fields.String()
    products_description = fields.String()
    conditions = fields.String()


class CouponReviewResultSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            loyalty_pb2.SubmitCouponsReviewsListRequest.CouponReviewResult
        )

    biz_id = fields.String(required=True)
    item_id = fields.String(required=True)
    revision_id = fields.Integer(required=True)
    resolution = PbEnumField(
        enum=CouponReviewResolution,
        pb_enum=loyalty_pb2.CouponReviewResolution,
        values_map=PROTO_TO_ENUM_MAP["coupon_review_resolution"],
        required=True,
    )
    corrected = fields.Nested(CouponReviewCorrectedSchema, required=False)
    reason_codes = fields.List(fields.Integer(), required=True)


class SubmitCouponsReviewsListRequestSchema(ProtobufSchema):
    class Meta:
        pb_message_class = loyalty_pb2.SubmitCouponsReviewsListRequest

    results_list = fields.Nested(CouponReviewResultSchema, many=True, required=True)


class PagingOutSchema(ProtobufSchema):
    class Meta:
        pb_message_class = loyalty_pb2.PagingOutput

    total = fields.Integer(required=True)
    offset = fields.Integer(required=True)
    limit = fields.Integer(required=True)


class CouponForReviewSchema(ProtobufSchema):
    class Meta:
        pb_message_class = loyalty_pb2.CouponForReview

    biz_id = fields.Integer(required=True, validate=validate.Range(min=1))
    item_id = fields.Integer(required=True, validate=validate.Range(min=1))
    revision_id = fields.Integer(required=True)
    title = fields.String(required=True)
    cover_url = fields.String(required=False)
    products_description = fields.String(required=True)
    conditions = fields.String(required=False)


class CouponsListForReviewResponseSchema(ProtobufSchema):
    class Meta:
        pb_message_class = loyalty_pb2.CouponsListForReviewResponse

    coupons_list = fields.Nested(CouponForReviewSchema, many=True)
    paging = fields.Nested(PagingOutSchema, required=True)


class CouponSentToModerationSchema(ProtobufSchema):
    class Meta:
        pb_message_class = (
            loyalty_pb2.ConfirmCouponsSentToModerationRequest.CouponSentToModeration
        )

    biz_id = fields.String(required=True)
    item_id = fields.String(required=True)
    revision_id = fields.Integer(required=True)


class ConfirmCouponsSentToModerationRequestSchema(ProtobufSchema):
    class Meta:
        pb_message_class = loyalty_pb2.ConfirmCouponsSentToModerationRequest

    coupons_list = fields.Nested(CouponSentToModerationSchema, many=True, required=True)


class LoyaltyIntClient(HttpClientWithTvm):
    DEFAULT_LIMIT = 500

    @collect_errors
    async def get_coupons_list_for_review(self) -> List[dict]:
        offset = 0
        while True:
            coupons_list = await self._get_coupons_list_for_review(
                limit=self.DEFAULT_LIMIT, offset=offset
            )

            if not coupons_list:
                break

            offset += self.DEFAULT_LIMIT
            yield coupons_list

    async def _get_coupons_list_for_review(self, limit: int, offset: int) -> List[dict]:
        got = await self.request(
            method="POST",
            uri="/v0/get_coupons_list_for_review",
            expected_statuses=[200],
            headers=await self._make_headers(),
            data=loyalty_pb2.CouponsListForReviewRequest(
                paging=loyalty_pb2.Paging(limit=limit, offset=offset)
            ).SerializeToString(),
            metric_name="/v0/get_coupons_list_for_review",
        )

        try:
            result = CouponsListForReviewResponseSchema().from_bytes(got)
            return result["coupons_list"]
        except marshmallow.ValidationError as e:
            raise BadLoyaltyResponse(e.messages)

    @collect_errors
    async def submit_coupons_reviews_list(
        self, coupons_review_results: List[dict]
    ) -> None:
        if not coupons_review_results:
            raise EmptyCouponsList()

        try:
            coupons_reviews = SubmitCouponsReviewsListRequestSchema().to_bytes(
                dict(results_list=coupons_review_results)
            )
        except marshmallow.ValidationError as e:
            raise IncorrectCouponData(e.messages)

        await self.request(
            method="POST",
            uri="/v0/submit_coupons_reviews_list",
            expected_statuses=[204],
            headers=await self._make_headers(),
            data=coupons_reviews,
            metric_name="/v0/submit_coupons_reviews_list",
        )

    @collect_errors
    async def confirm_coupons_sent_to_review(self, sent_coupons: List[dict]) -> None:
        if not sent_coupons:
            raise EmptyCouponsList()

        try:
            coupon_confirmations = (
                ConfirmCouponsSentToModerationRequestSchema().to_bytes(  # noqa
                    dict(coupons_list=sent_coupons)
                )
            )
        except marshmallow.ValidationError as e:
            raise IncorrectCouponData(e.messages)

        await self.request(
            method="POST",
            uri="/v0/confirm_coupons_sent_to_moderation",
            expected_statuses=[204],
            headers=await self._make_headers(),
            data=coupon_confirmations,
            metric_name="/v0/confirm_coupons_sent_to_moderation",
        )

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