from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List
from collections import defaultdict
import logging

from maps_adv.adv_store.client.lib.client import Client as AdvStoreClient

from maps_adv.common.third_party_clients.juggler import JugglerClient
from maps_adv.common.helpers.enums import CampaignTypeEnum
from maps_adv.statistics.dashboard.server.lib.data_manager import (
    AbstractDataManager,
    NothingFound,
)

OVERDRAFT_THRESHOLDS = {
    CampaignTypeEnum.BILLBOARD: 0.05,
    CampaignTypeEnum.OVERVIEW_BANNER: 0.05,
    CampaignTypeEnum.PIN_ON_ROUTE: 0.05,
    CampaignTypeEnum.ROUTE_BANNER: 0.05,
    CampaignTypeEnum.VIA_POINTS: 0.1,
    CampaignTypeEnum.ZERO_SPEED_BANNER: 0.1,
    CampaignTypeEnum.CATEGORY_SEARCH: 0.0,
    CampaignTypeEnum.PROMOCODE: 0.0,
}


class Domain:
    __slots__ = ("_dm", "_", "_adv_store_client", "_juggler_client")

    _dm: AbstractDataManager
    _adv_store_client: AdvStoreClient
    _juggler_client: JugglerClient

    def __init__(
        self,
        dm: AbstractDataManager,
        adv_store_client: AdvStoreClient,
        juggler_client: JugglerClient,
    ):
        self._dm = dm
        self._adv_store_client = adv_store_client
        self._juggler_client = juggler_client

    async def calculate_monitoring_data(
        self, now_dt: datetime, period_seconds: int
    ) -> List[Dict[str, Any]]:
        start_dt = now_dt - timedelta(seconds=period_seconds)

        campaign_ids = await self._dm.get_campaign_ids_for_period(start_dt, now_dt)

        async with self._adv_store_client as adv_store_client:
            campaigns = await adv_store_client.retrieve_campaign_data_for_monitorings(
                campaign_ids
            )

        zsb_ids = [
            campaign["id"]
            for campaign in campaigns
            if campaign["campaign_type"] == CampaignTypeEnum.ZERO_SPEED_BANNER
        ]

        pin_on_route_ids = [
            campaign["id"]
            for campaign in campaigns
            if campaign["campaign_type"] == CampaignTypeEnum.PIN_ON_ROUTE
        ]

        billboard_ids = [
            campaign["id"]
            for campaign in campaigns
            if campaign["campaign_type"] == CampaignTypeEnum.BILLBOARD
        ]

        overview_banner_ids = [
            campaign["id"]
            for campaign in campaigns
            if campaign["campaign_type"] == CampaignTypeEnum.OVERVIEW_BANNER
        ]

        route_banner_ids = [
            campaign["id"]
            for campaign in campaigns
            if campaign["campaign_type"] == CampaignTypeEnum.ROUTE_BANNER
        ]

        result_all = await self._dm.calculate_metrics(start_dt, now_dt)
        result_zsb = await self._dm.calculate_metrics(start_dt, now_dt, zsb_ids)
        result_route_banner = await self._dm.calculate_metrics(
            start_dt, now_dt, route_banner_ids
        )
        result_overview_banner = await self._dm.calculate_metrics(
            start_dt, now_dt, overview_banner_ids
        )
        result_pin_on_route = await self._dm.calculate_metrics(
            start_dt, now_dt, pin_on_route_ids
        )
        result_billboard = await self._dm.calculate_metrics(
            start_dt, now_dt, billboard_ids
        )
        tables_metrics = await self._dm.retrieve_tables_metrics(now_dt)

        metrics = [
            {
                "labels": {"name": "total_users"},
                "type": "COUNTER",
                "value": result_all["users"],
            },
            {
                "labels": {"name": "total_shows"},
                "type": "COUNTER",
                "value": result_all["shows"],
            },
            {
                "labels": {"name": "total_clicks"},
                "type": "COUNTER",
                "value": result_all["clicks"],
            },
            {
                "labels": {"name": "zsb_shows"},
                "type": "COUNTER",
                "value": result_zsb["shows"],
            },
            {
                "labels": {"name": "zsb_clicks"},
                "type": "COUNTER",
                "value": result_zsb["clicks"],
            },
            {
                "labels": {"name": "route_banner_shows"},
                "type": "COUNTER",
                "value": result_route_banner["shows"],
            },
            {
                "labels": {"name": "route_banner_clicks"},
                "type": "COUNTER",
                "value": result_route_banner["clicks"],
            },
            {
                "labels": {"name": "overview_banner_shows"},
                "type": "COUNTER",
                "value": result_overview_banner["shows"],
            },
            {
                "labels": {"name": "overview_banner_clicks"},
                "type": "COUNTER",
                "value": result_overview_banner["clicks"],
            },
            {
                "labels": {"name": "pin_on_route_shows"},
                "type": "COUNTER",
                "value": result_pin_on_route["shows"],
            },
            {
                "labels": {"name": "pin_on_route_clicks"},
                "type": "COUNTER",
                "value": result_pin_on_route["clicks"],
            },
            {
                "labels": {"name": "billboard_shows"},
                "type": "COUNTER",
                "value": result_billboard["shows"],
            },
            {
                "labels": {"name": "billboard_clicks"},
                "type": "COUNTER",
                "value": result_billboard["clicks"],
            },
            {
                "labels": {"timestamp": "now"},
                "type": "IGAUGE",
                "value": int(datetime.now(timezone.utc).timestamp()),
            },
            {
                "labels": {"timestamp": "request_from"},
                "type": "IGAUGE",
                "value": int(start_dt.timestamp()),
            },
            {
                "labels": {"timestamp": "request_to"},
                "type": "IGAUGE",
                "value": int(now_dt.timestamp()),
            },
        ]

        metrics.extend(
            [
                {
                    "labels": {
                        "table": table["table"],
                        "column": "max_receive_timestamp",
                    },
                    "type": "IGAUGE",
                    "value": table["max_receive_timestamp"],
                }
                for table in tables_metrics
            ]
        )

        return metrics

    async def check_not_spending_budget(self):
        async with self._adv_store_client as adv_store:
            adv_store_data = await adv_store.list_campaigns_for_budget_analysis()

        try:
            campaign_ids = list(map(lambda c: c["campaign_id"], adv_store_data))
            events_stat = await self._dm.calculate_campaigns_charged_sum(
                campaign_ids, None
            )
        except NothingFound:
            return

        campaigns_charged = {
            item["campaign_id"]: item["charged_sum"] for item in events_stat
        }

        campaign_ids_to_report = []
        for item in adv_store_data:
            daily_budget = Decimal(item.get("daily_budget", "Inf"))
            if daily_budget == Decimal("Inf"):
                # if daily budget is unlimited, it has a chance to spend whole budget
                continue

            campaign_id = item["campaign_id"]
            days_left = item.get("days_left")
            budget = Decimal(item.get("budget", "Inf"))
            charged = campaigns_charged.get(item["campaign_id"], Decimal(0))

            if days_left * daily_budget < budget - charged:
                campaign_ids_to_report.append(campaign_id)

        if campaign_ids_to_report:
            async with self._adv_store_client as adv_store:
                await adv_store.create_campaign_not_spending_budget_events(
                    campaign_ids_to_report
                )

    async def check_overdraft(self):
        to_utc = datetime.now(tz=timezone.utc)
        from_utc = to_utc - timedelta(days=1)
        mapkit_campaigns = await self._dm.get_aggregated_mapkit_events_by_campaign(
            from_utc, to_utc
        )
        normalized_campaigns = (
            await self._dm.get_aggregated_normalized_events_by_campaign(
                from_utc, to_utc
            )
        )
        processed_campaigns = (
            await self._dm.get_aggregated_processed_events_by_campaign(from_utc, to_utc)
        )
        campaigns_ids = (
            mapkit_campaigns.keys()
            | normalized_campaigns.keys()
            | processed_campaigns.keys()
        )

        async with self._adv_store_client as adv_store_client:
            campaigns = await adv_store_client.retrieve_campaign_data_for_monitorings(
                campaigns_ids
            )

        campaigns = {
            campaign["campaign_id"]: {
                "campaign_type": campaign["campaign_type"],
                "id": campaign["campaign_id"],
            }
            for campaign in campaigns
            if campaign["campaign_type"]
            not in [CampaignTypeEnum.CATEGORY_SEARCH, CampaignTypeEnum.PROMOCODE]
        }

        for counter_name, counter_values in [
            ("mapkit_events", mapkit_campaigns),
            ("normalized_events", normalized_campaigns),
            ("processed_events", processed_campaigns),
        ]:
            for campaign_id, campaign_data in counter_values.items():
                if campaign_id in campaigns:
                    campaigns[campaign_id][counter_name] = (
                        # TODO Use a lookup table to determine billed event type by campaign type
                        campaign_data["action_make_route"]
                        if campaigns[campaign_id]["campaign_type"]
                        == CampaignTypeEnum.VIA_POINTS
                        else campaign_data["billboard_show"]
                    )

        overdraft_campaigns = defaultdict(list)
        double_spent_campaigns = defaultdict(list)
        counters_by_campaign_type = defaultdict(lambda: defaultdict(lambda: 0))

        for campaign_data in campaigns.values():
            campaign_type = campaign_data["campaign_type"]
            mapkit_events = campaign_data.get("mapkit_events", 0)
            normalized_events = campaign_data.get("normalized_events", 0)
            processed_events = campaign_data.get("processed_events", 0)

            for counter_name in [
                "mapkit_events",
                "normalized_events",
                "processed_events",
            ]:
                counters_by_campaign_type[campaign_type][
                    counter_name
                ] += campaign_data.get(counter_name, 0)

            if processed_events < normalized_events * (
                1.0 - OVERDRAFT_THRESHOLDS[campaign_type]
            ):
                overdraft_campaigns[campaign_type].append(campaign_data)

            if (
                processed_events > normalized_events
                or normalized_events > mapkit_events
            ):
                double_spent_campaigns[campaign_type].append(campaign_data)

        overdraft_logger = logging.getLogger("dashboard.check_overdraft")
        double_spent_logger = logging.getLogger("dashboard.check_double_spent")

        for campaign_type in CampaignTypeEnum:
            normalized_events = counters_by_campaign_type[campaign_type][
                "normalized_events"
            ]
            processed_events = counters_by_campaign_type[campaign_type][
                "processed_events"
            ]

            overdraft_by_campaign = ", ".join(
                [
                    f"""{campaign["id"]}({100 * ((campaign.get("normalized_events", 0) - campaign.get("processed_events", 0))/campaign.get("normalized_events", 1))}%)"""
                    for campaign in overdraft_campaigns[campaign_type]
                ]
            )

            if overdraft_campaigns[campaign_type]:
                overdraft_logger.warn(
                    f"Campaign overdraft for type {campaign_type.name}, campaigns: {overdraft_by_campaign}"
                )

            if processed_events < normalized_events * (
                1.0 - OVERDRAFT_THRESHOLDS[campaign_type]
            ):
                await self._juggler_client(
                    description=f"Campaigns: {overdraft_by_campaign}",
                    service="campaign_overdraft:" + campaign_type.name,
                    status="CRIT",
                )
            else:
                await self._juggler_client(
                    description="",
                    service="campaign_overdraft:" + campaign_type.name,
                    status="OK",
                )

            if double_spent_campaigns[campaign_type]:
                double_spent_by_campaign = ", ".join(
                    [
                        f"""{campaign["id"]}(mapkit: {campaign.get("mapkit_events", 0)}, normalized: {campaign.get("normalized_events", 0)}, processed: {campaign.get("processed_events", 0)})"""
                        for campaign in double_spent_campaigns[campaign_type]
                    ]
                )
                await self._juggler_client(
                    description=f"Campaigns: {double_spent_by_campaign}",
                    service="campaign_double_spent:" + campaign_type.name,
                    status="CRIT",
                )
                double_spent_logger.warn(
                    f"Campaign double spent for type {campaign_type.name}, campaigns: {double_spent_by_campaign}"
                )
            else:
                await self._juggler_client(
                    description="",
                    service="campaign_double_spent:" + campaign_type.name,
                    status="OK",
                )
