"""Tokens → dollars.

The order of operations is the whole content of this module, and it is
pinned by tests because getting it wrong moves the bill:

1. Round consumption up to whole tokens, monthly. Per-user subscription
   totals are exact and are NOT rounded — only metered consumption is.
2. Deduct the free monthly allowance (250 named / 350 concurrent).
3. Floor at zero. The allowance renews monthly and "do[es] not carry over
   to future months", so an unused remainder is discarded, never banked.

Everything commercial — the contracted rate, the concession — arrives via
:class:`genesyscalc.meters.TokenPrice` from the notebook's engagement-data
cell. This module knows arithmetic, not deals.
"""

from __future__ import annotations

import math
from dataclasses import dataclass

from .meters import Confidence, LicenceModel, TokenPrice
from .ratecard import FREE_TOKENS_PER_MONTH, meter

MONTHS_PER_YEAR: int = 12


@dataclass(frozen=True)
class CostLine:
    """One feature's monthly and annual cost, ready to render."""

    key: str
    feature: str
    units_monthly: float
    unit_label: str
    tokens_monthly: float
    cost_monthly: float
    cost_annual: float
    confidence: Confidence
    source_url: str | None
    note: str = ""


@dataclass(frozen=True)
class CostTotals:
    """The token bill, with the allowance and the MSRP walk both visible."""

    consumption_tokens_monthly: float
    subscription_tokens_monthly: float
    gross_tokens_monthly: float
    allowance_tokens_monthly: int
    billable_tokens_monthly: int
    effective_rate: float
    currency: str
    token_cost_monthly: float
    token_cost_annual: float
    list_cost_annual: float
    concession_saving_annual: float


def billable_tokens_monthly(
    consumption_tokens: float,
    subscription_tokens: float,
    licence: LicenceModel,
    apply_allowance: bool = True,
) -> int:
    """Whole tokens actually billed in a month.

    ``ceil`` lands on the consumption subtotal (Genesys bills whole tokens);
    subscription totals are already whole and exact. The allowance is then
    deducted and the result floored at zero — it does not carry over, so an
    unused remainder is simply lost.
    """
    if consumption_tokens < 0 or subscription_tokens < 0:
        raise ValueError("token counts must not be negative")
    gross = math.ceil(consumption_tokens) + subscription_tokens
    allowance = FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0
    return max(0, int(gross) - allowance)


def allowance_for(licence: LicenceModel, apply_allowance: bool = True) -> int:
    """The free monthly token allowance actually in play."""
    return FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0


def cost_lines(
    tokens_by_key: dict[str, float], units_by_key: dict[str, float], price: TokenPrice
) -> tuple[CostLine, ...]:
    """One :class:`CostLine` per meter, in published order, at the effective rate.

    Line costs are unrounded and un-allowanced: the allowance is an
    org-level deduction, not a per-feature one, so attributing it to a
    single line would misstate that feature's economics. It lands in
    :func:`total_cost`.
    """
    rate = price.effective_rate()
    lines: list[CostLine] = []
    for key in sorted(tokens_by_key, key=lambda k: meter(k).feature):
        m = meter(key)
        tokens = tokens_by_key[key]
        monthly = tokens * rate
        lines.append(
            CostLine(
                key=key,
                feature=m.feature,
                units_monthly=units_by_key.get(key, 0.0),
                unit_label=m.unit_label,
                tokens_monthly=tokens,
                cost_monthly=monthly,
                cost_annual=monthly * MONTHS_PER_YEAR,
                confidence=m.confidence,
                source_url=m.source_url,
                note=m.note,
            )
        )
    return tuple(lines)


def total_cost(
    consumption_tokens_monthly: float,
    subscription_tokens_monthly: float,
    licence: LicenceModel,
    price: TokenPrice,
    apply_allowance: bool = True,
) -> CostTotals:
    """The org-level bill: allowance deducted, MSRP walk carried alongside."""
    billable = billable_tokens_monthly(
        consumption_tokens_monthly,
        subscription_tokens_monthly,
        licence,
        apply_allowance,
    )
    rate = price.effective_rate()
    monthly = billable * rate
    annual = monthly * MONTHS_PER_YEAR
    list_annual = billable * price.list_rate * MONTHS_PER_YEAR
    return CostTotals(
        consumption_tokens_monthly=consumption_tokens_monthly,
        subscription_tokens_monthly=subscription_tokens_monthly,
        gross_tokens_monthly=(
            math.ceil(consumption_tokens_monthly) + subscription_tokens_monthly
        ),
        allowance_tokens_monthly=allowance_for(licence, apply_allowance),
        billable_tokens_monthly=billable,
        effective_rate=rate,
        currency=price.currency,
        token_cost_monthly=monthly,
        token_cost_annual=annual,
        list_cost_annual=list_annual,
        concession_saving_annual=list_annual - annual,
    )
