import asyncio
import logging
from functools import partial
from typing import List, Optional

from maps_adv.adv_store.v2.lib.core.direct_moderation import (
    Client as DirectModerationClient,
)
from maps_adv.adv_store.v2.lib.data_managers import (
    CampaignsDataManager,
    ModerationDataManager,
)
from maps_adv.adv_store.v2.lib.data_managers.campaigns import (
    CampaignsChangeLogActionName,
)
from maps_adv.adv_store.v2.lib.data_managers.exceptions import CampaignNotFound
from maps_adv.adv_store.api.schemas.enums import (
    CampaignDirectModerationStatusEnum,
    CampaignDirectModerationWorkflowEnum,
    CampaignStatusEnum,
    ModerationResolutionEnum,
    YesNoEnum,
)
from maps_adv.billing_proxy.client import Client as BillingProxyClient
from maps_adv.billing_proxy.client.lib.enums import Currency
from maps_adv.common.helpers.enums import CampaignTypeEnum

__all__ = ["CampaignNotInReview", "ModerationDomain", "ReviewCommentEmpty"]


logger = logging.getLogger("adv_store.domains.moderation")


class CampaignNotInReview(Exception):
    pass


class ReviewCommentEmpty(Exception):
    pass


def _map_currency_to_geo_id(currency):
    mapping = {
        Currency.RUB: 225,
        Currency.BYN: 149,
        Currency.KZT: 159,
        Currency.TRY: 983,
        Currency.EUR: 111,  # Europe
        Currency.USD: 84,
    }
    return mapping.get(currency) or 318  # Universal value


class ModerationDomain:
    __slots__ = (
        "_dm",
        "_campaigns_dm",
        "_direct_moderation_client",
        "_billing_proxy_client",
    )

    _dm: ModerationDataManager
    _campaigns_dm: CampaignsDataManager
    _direct_moderation_client: Optional[DirectModerationClient]
    _billing_proxy_client: Optional[BillingProxyClient]

    def __init__(
        self,
        dm: ModerationDataManager,
        campaigns_dm: CampaignsDataManager,
        direct_moderation_client: Optional[DirectModerationClient],
        billing_proxy_client: Optional[BillingProxyClient],
    ):
        self._dm = dm
        self._campaigns_dm = campaigns_dm
        self._direct_moderation_client = direct_moderation_client
        self._billing_proxy_client = billing_proxy_client

    async def list_campaigns(self) -> List[dict]:
        return await self._dm.list_campaigns()

    async def review_campaign(
        self,
        campaign_id: int,
        resolution: ModerationResolutionEnum,
        author_id: int,
        comment: str = "",
        verdicts: Optional[List[int]] = None,
    ) -> None:
        if not await self._campaigns_dm.campaign_exists(campaign_id):
            raise CampaignNotFound

        current_status = await self._campaigns_dm.get_status(campaign_id)

        if current_status is not CampaignStatusEnum.REVIEW:
            raise CampaignNotInReview

        if resolution is ModerationResolutionEnum.REJECT and not comment:
            raise ReviewCommentEmpty

        if resolution is ModerationResolutionEnum.APPROVE:
            new_status = CampaignStatusEnum.ACTIVE
            moderation_id = await self._dm.create_direct_moderation_for_campaign(
                campaign_id,
                author_id,
                workflow=CampaignDirectModerationWorkflowEnum.AUTO_ACCEPT,
            )
        else:
            new_status = CampaignStatusEnum.REJECTED
            moderation_id = await self._dm.create_direct_moderation_for_campaign(
                campaign_id,
                author_id,
                workflow=CampaignDirectModerationWorkflowEnum.AUTO_REJECT,
                verdicts=verdicts,
            )

        await self._campaigns_dm.set_status(
            campaign_id,
            author_id=author_id,
            status=new_status,
            change_log_action_name=CampaignsChangeLogActionName.CAMPAIGN_REVIEWED,
            metadata={"comment": comment},
        )

        await self._campaigns_dm.set_direct_moderation(campaign_id, moderation_id)

    async def _process_moderation(self, moderation_client, billing_client, moderation):
        campaign = await self._campaigns_dm.retrieve_campaign(moderation["campaign_id"])

        if (
            campaign["campaign_type"] == CampaignTypeEnum.ZERO_SPEED_BANNER
            and campaign.get("manul_order_id") is None
            # do not send moderation for manual orders
            # if a comment is present, we assume that campaign needs
            # manual handling and do not send moderation
            and campaign["comment"] == ""
        ):
            order = await billing_client.fetch_order(campaign["order_id"])
            geo_id = _map_currency_to_geo_id(order["currency"])
            await moderation_client.send_campaign_moderation(
                campaign, moderation, geo_id
            )

        # other types are not sent but set their state to PROCESSING
        await self._dm.update_direct_moderation(
            moderation["id"],
            CampaignDirectModerationStatusEnum.PROCESSING,
        )

    async def process_new_moderations(self) -> None:
        if not self._direct_moderation_client:
            logger.error("Direct client not found")
            return

        new_moderations = await self._dm.retrieve_direct_moderations_by_status(
            CampaignDirectModerationStatusEnum.NEW
        )

        async with (
            self._direct_moderation_client as client,
            self._billing_proxy_client as billing_client,
        ):
            exceptions = await asyncio.gather(
                *map(
                    partial(self._process_moderation, client, billing_client),
                    new_moderations,
                ),
                return_exceptions=True,
            )
            exceptions = list(filter(None, exceptions))

        if exceptions:
            raise exceptions[0]

    async def process_direct_moderation_responses(self) -> None:
        if not self._direct_moderation_client:
            logger.error("Direct client not found")
            return

        exception = None
        async for response in self._direct_moderation_client.retrieve_direct_responses():
            try:
                moderation = await self._dm.retrieve_direct_moderation(
                    response.meta.version_id
                )

                campaign = await self._campaigns_dm.retrieve_campaign(
                    response.meta.campaign_id
                )

                if (
                    response.verdict == YesNoEnum.YES
                    and moderation["status"]
                    == CampaignDirectModerationStatusEnum.PROCESSING
                ):
                    await self._dm.update_direct_moderation(
                        response.meta.version_id,
                        CampaignDirectModerationStatusEnum.ACCEPTED,
                    )
                    if campaign["direct_moderation_id"] == response.meta.version_id:
                        await self._campaigns_dm.set_status(
                            campaign_id=response.meta.campaign_id,
                            author_id=moderation["reviewer_uid"],
                            status=CampaignStatusEnum.ACTIVE,
                            change_log_action_name=CampaignsChangeLogActionName.CAMPAIGN_REVIEWED,  # noqa: E501
                            metadata={"direct_moderation": response.meta.version_id},
                        )
                elif response.verdict == YesNoEnum.NO:
                    await self._dm.update_direct_moderation(
                        response.meta.version_id,
                        CampaignDirectModerationStatusEnum.REJECTED,
                        response.reasons,
                    )
                    if campaign["direct_moderation_id"] == response.meta.version_id:
                        await self._campaigns_dm.set_status(
                            campaign_id=response.meta.campaign_id,
                            author_id=moderation["reviewer_uid"],
                            status=CampaignStatusEnum.REJECTED,
                            change_log_action_name=CampaignsChangeLogActionName.CAMPAIGN_REVIEWED,  # noqa: E501
                            metadata={"direct_moderation": response.meta.version_id},
                        )
            except BaseException as e:
                exception = e

        if exception:
            raise exception
