import logging
from decimal import Decimal
from math import ceil, floor
from typing import Dict, Optional

logger = logging.getLogger(__name__)

__all__ = ["calculate_charges"]

"""Adds additional fields to received order data"""


class UnsupportedPrecision(Exception):
    pass


DECIMAL_SIGNIFICANT_FRACTION_PLACES = 6


def calculate_charges(order_data: Dict) -> Dict:
    charge_for_order = Decimal("0")
    order_id = order_data["order_id"]
    rest_of_balance = order_data["budget_balance"]
    campaign_list = order_data["campaigns"]

    for campaign in campaign_list:
        if campaign["events_count"] != 0:
            charge_limit = _calculate_charge_limit(
                rest_of_balance, charge_for_order, campaign, order_id
            )

            cost_per_event = campaign["cpm"] / 1000
            _validate_money_precision(
                cost_per_event, "cost_per_event", campaign["campaign_id"]
            )

            events_to_charge = _calculate_events(campaign, cost_per_event, charge_limit)

            _set_events_cost(campaign, events_to_charge, cost_per_event, charge_limit)
        else:
            _set_events_cost(campaign=campaign, events_to_charge=0, cost_per_event=None)

        charge_for_order += _calculate_charge(campaign)

    order_data["amount_to_bill"] = None if order_id is None else charge_for_order

    return order_data


def _validate_money_precision(money: Decimal, param_name: str, campaign_id: int):
    value_parts = str(money).split(".")
    if len(value_parts) > 1:
        fraction_digits = value_parts[-1].rstrip("0")
        if len(fraction_digits) > DECIMAL_SIGNIFICANT_FRACTION_PLACES:
            raise UnsupportedPrecision(
                f"Unsupported precision of {param_name}={money} "
                f"for campaign with id={campaign_id}. "
                f"Only {DECIMAL_SIGNIFICANT_FRACTION_PLACES} "
                f"decimal places are supported."
            )


def _calculate_charge_limit(
    rest_of_balance: Decimal, charge_for_order: Decimal, campaign: dict, order_id: int
):
    charge_limit = min(
        rest_of_balance - charge_for_order,
        campaign["budget"] - campaign["charged"],
        campaign["daily_budget"] - campaign["charged_daily"],
    )

    if charge_limit < 0:
        campaign_id = campaign["campaign_id"]
        logger.error(
            f"Negative charge limit detected: campaign_id={campaign_id}, "
            "order_id=%s, budget_balance=%s, campaign=%s",
            order_id,
            rest_of_balance,
            campaign,
        )

    return charge_limit if charge_limit >= 0 else Decimal("0")


def _calculate_events(
    campaign: Dict, cost_per_event: Decimal, charge_limit: Decimal
) -> Decimal:
    if charge_limit == 0:
        return Decimal("0")

    available_events_amount = charge_limit / cost_per_event

    events_to_charge = min(available_events_amount, campaign["events_count"])

    return events_to_charge


def _calculate_charge(campaign: Dict) -> Decimal:
    if campaign["events_to_charge"] == 0:
        return Decimal("0")

    return (
        campaign["cost_per_event"] * (campaign["events_to_charge"] - 1)
        + campaign["cost_per_last_event"]
    )


def _set_events_cost(
    campaign: Dict,
    events_to_charge: Decimal,
    cost_per_event: Decimal,
    charge_limit: Optional[Decimal] = 0,
):
    if events_to_charge == 0:
        campaign["events_to_charge"] = 0
        campaign["cost_per_event"] = None
        campaign["cost_per_last_event"] = None
    # check if Decimal is integer
    elif events_to_charge % 1 == 0:
        campaign["events_to_charge"] = int(events_to_charge)
        campaign["cost_per_event"] = cost_per_event
        campaign["cost_per_last_event"] = cost_per_event
    else:
        campaign["events_to_charge"] = ceil(events_to_charge)
        campaign["cost_per_event"] = cost_per_event
        # last event with special cost
        campaign["cost_per_last_event"] = (
            charge_limit - floor(events_to_charge) * cost_per_event
        )
