import asyncio
import gzip
import itertools
import json
import logging
from copy import deepcopy
from datetime import date, datetime, timezone
from typing import AsyncIterator, Dict, List, Optional, Tuple

from maps_adv.geosmb.clients.facade import CouponType, Currency, FacadeIntClient
from maps_adv.geosmb.clients.loyalty import CouponReviewResolution, LoyaltyIntClient
from maps_adv.geosmb.clients.logbroker.logbroker import (
    LogbrokerClient,
    LogbrokerReadTimeout,
    TopicReader,
)


class CouponsDomain:
    __slots__ = [
        "_facade_client",
        "_loyalty_client",
        "_coupons_to_review_topic_out",
        "_coupons_reviewed_topic_in",
        "_logbroker_consumer_id",
        "_logbroker_writer_client",
        "_logbroker_reader_clients",
        "_logbroker_message_count_threshold",
    ]

    _facade_client: FacadeIntClient
    _loyalty_client: LoyaltyIntClient
    _logbroker_writer_client: LogbrokerClient
    _logbroker_reader_clients: List[LogbrokerClient]
    _coupons_to_review_topic_out: str
    _coupons_reviewed_topic_in: str
    _logbroker_consumer_id: str
    _logbroker_message_count_threshold: int

    def __init__(
        self,
        facade_client: FacadeIntClient,
        loyalty_client: LoyaltyIntClient,
        logbroker_writer_client: LogbrokerClient,
        logbroker_reader_clients: List[LogbrokerClient],
        coupons_to_review_topic_out: str,
        coupons_reviewed_topic_in: str,
        logbroker_consumer_id: str,
        logbroker_message_count_threshold: int,
    ):
        self._facade_client = facade_client
        self._loyalty_client = loyalty_client
        self._logbroker_writer_client = logbroker_writer_client
        self._logbroker_reader_clients = logbroker_reader_clients
        self._coupons_to_review_topic_out = coupons_to_review_topic_out
        self._coupons_reviewed_topic_in = coupons_reviewed_topic_in
        self._logbroker_consumer_id = logbroker_consumer_id
        self._logbroker_message_count_threshold = logbroker_message_count_threshold

    async def iter_organizations_with_coupons_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        export_chunk = []

        async for orgs in self._facade_client.get_organizations_with_coupons():
            data = [
                dict(
                    permalink=int(org["permalink"]),
                    showcase={"type": "BOOKING", "value": org["showcase_url"]},
                )
                for org in orgs
            ]

            export_chunk.extend(data)
            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    async def iter_organizations_with_booking_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        export_chunk = []

        async for orgs in self._facade_client.get_organizations_with_booking():
            data = [
                dict(permalink=int(org["permalink"]), booking_url=org["booking_url"])
                for org in orgs
            ]

            export_chunk.extend(data)
            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    async def iter_loyalty_items_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        def _convert_to_yt_format(item: dict):
            item = deepcopy(item)
            item["issued_at"] = self._encode_dt_to_yt_timestamp(item["issued_at"])
            return item

        export_chunk = []

        async for items in self._facade_client.get_loyalty_items_list_for_snapshot():
            items = list(map(_convert_to_yt_format, items))
            export_chunk.extend(items)

            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    async def iter_business_coupons_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        def _convert_to_yt_format(item: dict):
            item = deepcopy(item)
            for service in item["services"]:
                service["price"]["currency"] = service["price"]["currency"].name
            item["currency"] = item["price"]["currency"].name
            item["price"] = item["price"]["cost"]
            item["discounted_price"] = item["discounted_price"]["cost"]
            item["start_date"] = self._encode_dt_to_yt_timestamp(item["start_date"])
            item["get_until_date"] = self._encode_dt_to_yt_timestamp(
                item["get_until_date"]
            )
            item["end_date"] = self._encode_dt_to_yt_timestamp(item["end_date"])
            item["distribution"] = item["distribution"].name
            item["moderation_status"] = item["moderation_status"].name
            item["created_at"] = self._encode_dt_to_yt_timestamp(item["created_at"])
            for cover_template in item["cover_templates"]:
                cover_template["type"] = cover_template["type"].name

            return item

        export_chunk = []

        async for coupons in self._facade_client.get_business_coupons_for_snapshot():
            coupons = list(map(_convert_to_yt_format, coupons))
            export_chunk.extend(coupons)

            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    async def iter_coupon_promotions_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[List[dict]]:
        def _convert_to_yt_format(item: dict):
            item = deepcopy(item)
            item["date_from"] = date.isoformat(item["date_from"])
            item["date_to"] = date.isoformat(item["date_to"])

            return item

        export_chunk = []

        async for promotions in self._facade_client.fetch_coupon_promotions():
            promotions = list(map(_convert_to_yt_format, promotions))
            export_chunk.extend(promotions)

            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    async def export_coupons_to_review(self):
        processed_coupons = []

        try:
            async with self._logbroker_writer_client.create_writer(
                self._coupons_to_review_topic_out
            ) as topic_writer:
                async for coupons_chunk in self._loyalty_client.get_coupons_list_for_review():  # noqa
                    for coupon in coupons_chunk:
                        lb_message = self._convert_coupon_for_review_to_lb_format(
                            coupon
                        )
                        await topic_writer.write_one(str.encode(json.dumps(lb_message)))
                        processed_coupons.append(coupon)
        finally:
            if processed_coupons:
                await self._confirm_export_coupons_to_review(processed_coupons)

    @staticmethod
    def _convert_coupon_for_review_to_lb_format(coupon: dict) -> dict:
        return {
            "unixtime": int(datetime.now(timezone.utc).timestamp() * 1000),
            "workflow": "common",
            "service": "geosmb",
            "type": "coupons",
            "meta": {
                "biz_id": coupon["biz_id"],
                "id": coupon["item_id"],
                "version_id": coupon["revision_id"],
            },
            "data": {
                "title": coupon["title"],
                "cover_url": coupon.get("cover_url"),
                "conditions": coupon.get("conditions"),
                "products_description": coupon["products_description"],
            },
        }

    async def _confirm_export_coupons_to_review(self, processed_coupons: List[dict]):
        await self._loyalty_client.confirm_coupons_sent_to_review(
            [
                {k: coupon[k] for k in ("biz_id", "item_id", "revision_id")}
                for coupon in processed_coupons
            ]
        )

    async def export_coupons_reviewed_to_loyalty(self) -> None:
        await asyncio.gather(
            *[
                self._fetch_review_results_from_logbroker(logbroker_client)
                for logbroker_client in self._logbroker_reader_clients
            ],
            return_exceptions=True,
        )

    async def iter_coupons_with_segments_for_export(
        self, iter_size: Optional[int] = None
    ) -> AsyncIterator[Dict[str, List[dict]]]:
        def _convert_business_to_yt_format(business: Tuple[str, List[dict]]):
            return [
                dict(
                    biz_id=business[0],
                    coupon_id=coupon["coupon_id"],
                    type=coupon["type"].name,
                    segments=[segment.name for segment in coupon["segments"]],
                    percent_discount=coupon.get("percent_discount"),
                    cost_discount=coupon.get("cost_discount"),
                    currency=coupon["currency"].name if "currency" in coupon else None,
                    poi_subscript=self._make_subscript_for_coupons_poi(
                        coupon_type=coupon["type"],
                        percent_discount=coupon.get("percent_discount"),
                        cost_discount=coupon.get("cost_discount"),
                    ),
                )
                for coupon in business[1]
            ]

        def _filter_by_currency(
            biz_chunk: Dict[str, List[dict]]
        ) -> Dict[str, List[dict]]:
            filtered_by_rub = dict()
            for biz_id, coupons in biz_chunk.items():
                filtered_coupons = [
                    coupon
                    for coupon in coupons
                    if coupon.get("currency", Currency.RUB) == Currency.RUB
                ]
                if filtered_coupons:
                    filtered_by_rub[biz_id] = filtered_coupons

            return filtered_by_rub

        export_chunk = []

        async for biz_chunk in self._facade_client.list_coupons_with_segments():
            biz_chunk = _filter_by_currency(biz_chunk)
            biz_chunk = map(_convert_business_to_yt_format, biz_chunk.items())
            export_chunk.extend(itertools.chain.from_iterable(biz_chunk))

            if iter_size:
                while len(export_chunk) > iter_size:
                    yield export_chunk[:iter_size]

                    export_chunk = export_chunk[iter_size:]

        if export_chunk:
            yield export_chunk

    @staticmethod
    def _make_subscript_for_coupons_poi(
        *,
        coupon_type: CouponType,
        percent_discount: Optional[int],
        cost_discount: Optional[str],
    ) -> list:
        if coupon_type == CouponType.SERVICE:
            return [
                ("RU", f"Сертификат на услугу: Получите скидку {percent_discount}%"),
                ("EN", f"Service certificate: Get discount {percent_discount}%"),
            ]
        elif percent_discount:
            return [
                (
                    "RU",
                    f"Бесплатный сертификат на %: Получите скидку {percent_discount}%",
                ),
                ("EN", f"Free % certificate: Get discount {percent_discount}%"),
            ]
        else:
            return [
                (
                    "RU",
                    f"Бесплатный сертификат на сумму: Получите скидку {cost_discount} руб.",  # noqa
                ),
                (
                    "EN",
                    f"Free certificate on amount: Get discount {cost_discount} rub.",
                ),
            ]

    async def _fetch_review_results_from_logbroker(
        self, logbroker_client: LogbrokerClient
    ) -> None:
        async with logbroker_client.create_reader(
            self._coupons_reviewed_topic_in, self._logbroker_consumer_id
        ) as topic_reader:
            while True:
                try:
                    async for message in topic_reader.read_batch(
                        self._logbroker_message_count_threshold
                    ):
                        review_results = self._decode_review_result(message)
                        if not review_results:
                            break

                        await self._submit_review_results(review_results, topic_reader)

                except LogbrokerReadTimeout:
                    break
                except Exception as exc:
                    logging.getLogger(__name__).error(
                        "Unhandled exception while reading from logbroker", exc_info=exc
                    )
                    break

                finally:
                    await topic_reader.finish_reading()

    async def _submit_review_results(
        self, review_results: List[dict], topic_reader: TopicReader
    ) -> None:
        if review_results:
            try:
                await self._loyalty_client.submit_coupons_reviews_list(review_results)
                topic_reader.commit()
            except Exception as exc:
                logging.getLogger(__name__).error(
                    "Unhandled exception while submiting review results", exc_info=exc
                )

    def _decode_review_result(self, review_result: bytes) -> List[dict]:
        return [
            self._convert_coupon_review_result_to_pb_format(json.loads(r))
            for r in gzip.decompress(review_result).decode().strip().split("\n")
        ]

    def _convert_coupon_review_result_to_pb_format(self, review_result: dict) -> dict:
        resolution = (
            CouponReviewResolution.APPROVED
            if review_result["result"]["verdict"] == "Yes"
            else CouponReviewResolution.REJECTED
        )

        corrected = (
            review_result["result"]["corrected"]["content"]
            if "corrected" in review_result["result"]
            else None
        )

        return dict(
            biz_id=review_result["meta"]["biz_id"],
            item_id=review_result["meta"]["id"],
            revision_id=review_result["meta"]["version_id"],
            corrected=corrected,
            reason_codes=review_result["result"]["reasons"],
            resolution=resolution,
        )

    def _encode_dt_to_yt_timestamp(self, dt: datetime) -> int:
        return int(dt.timestamp() * 1_000_000)
