import asyncio
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Tuple

from maps_adv.stat_tasks_starter.lib.charger.clients import (
    AdvStoreClient,
    BillingClient,
)

from .events_stat import Collector as StatCollector

__all__ = ["Collector"]


class Collector:
    __slots__ = (
        "billing_url",
        "adv_store_url",
        "stat_collector",
        "_ignore_campaigns_ids",
    )

    def __init__(
        self,
        billing_url: str,
        adv_store_url: str,
        stat_db_config: dict,
        ignore_campaign_ids: Optional[List[int]] = None,
    ):
        self.billing_url = billing_url
        self.adv_store_url = adv_store_url
        self.stat_collector = StatCollector(**stat_db_config)
        self._ignore_campaigns_ids = (
            set() if not ignore_campaign_ids else set(ignore_campaign_ids)
        )

    async def __call__(self, timing_from: datetime, timing_to: datetime):
        campaigns_data = await self._fetch_adv_store_data(timing_to)
        campaigns_data = [
            item
            for item in campaigns_data
            if item["campaign_id"] not in self._ignore_campaigns_ids
        ]

        if not campaigns_data:
            return []

        order_ids = {item["order_id"] for item in campaigns_data if "order_id" in item}

        orders_balance_data, event_stat = await asyncio.gather(
            self._fetch_billing_data(list(order_ids)),
            self._fetch_events_stat(timing_from, timing_to, campaigns_data),
        )

        return self._compose_data(
            billing_data=orders_balance_data,
            adv_store_data=campaigns_data,
            events_stat=event_stat,
        )

    async def _fetch_adv_store_data(self, active_at: datetime) -> List[dict]:
        async with AdvStoreClient(self.adv_store_url) as adv_store_client:
            return await adv_store_client.receive_active_campaigns(active_at)

    async def _fetch_billing_data(self, orders_ids: List[int]) -> List[dict]:
        if orders_ids:
            async with BillingClient(self.billing_url) as billing_client:
                return await billing_client.receive_orders(orders_ids)
        else:
            return []

    async def _fetch_events_stat(
        self, timing_from: datetime, timing_to: datetime, campaigns: List[dict]
    ) -> Tuple[tuple]:
        timezone_campaigns = dict()
        for campaign in campaigns:
            tz_name = campaign["timezone"]
            if tz_name not in timezone_campaigns:
                timezone_campaigns[tz_name] = []
            timezone_campaigns[tz_name].append(campaign["campaign_id"])

        return await self.stat_collector(timing_from, timing_to, timezone_campaigns)

    @classmethod
    def _compose_data(cls, billing_data, adv_store_data, events_stat):
        composed = []

        for order in billing_data:
            composed_order = cls._compose_order_data(
                order["order_id"], order["balance"], adv_store_data, events_stat
            )
            composed.append(composed_order)

        if [item for item in adv_store_data if "order_id" not in item]:
            missed_order = cls._compose_order_data(
                None, "Inf", adv_store_data, events_stat
            )
            composed.append(missed_order)

        return composed

    @staticmethod
    def _compose_order_data(order_id, order_balance, adv_store_data, events_stat):
        campaigns = []
        for el in filter(lambda el: el.get("order_id") == order_id, adv_store_data):
            stat = [s for s in events_stat if s[0] == el["campaign_id"]]
            stat = stat[0] if stat else []

            campaign = {
                "campaign_id": el["campaign_id"],
                "tz_name": el["timezone"],
                "cpm": Decimal(el["cost"]),
                "budget": Decimal(el.get("budget", "Inf")),
                "daily_budget": Decimal(el.get("daily_budget", "Inf")),
                "charged_daily": stat[1] if stat else Decimal(0),
                "charged": stat[2] if stat else Decimal(0),
                "events_count": stat[3] if stat else 0,
            }
            campaigns.append(campaign)

        return {
            "order_id": order_id,
            "budget_balance": Decimal(order_balance),
            "campaigns": campaigns,
        }
