"""Schema for the rate card — types only, no rates.

Rates live in :mod:`genesyscalc.ratecard` as an immutable verbatim anchor.
This module defines what a metered feature *is*, so the anchor can be typed
and the arithmetic can be tested without either knowing the other's
business.

The invariants in ``__post_init__`` are the point: a meter flagged
:attr:`Confidence.CONFIRMED` that carries no source URL and date cannot be
constructed. That is what stops an unsourced number wearing a green tick on
a client-facing stage.
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum


class Confidence(Enum):
    """How much weight a number carries. Rendered on stage as an icon."""

    CONFIRMED = "confirmed"   # published by the vendor, with a source URL + date
    ESTIMATED = "estimated"   # working assumption, stated
    UNKNOWN = "unknown"       # not sourced; bound it with sensitivity if material

    @property
    def icon(self) -> str:
        return {"confirmed": "🟢", "estimated": "🟡", "unknown": "🔴"}[self.value]


class LicenceModel(Enum):
    """Genesys prices per-user AI features differently by licence model."""

    NAMED = "named"
    CONCURRENT = "concurrent"

    @property
    def label(self) -> str:
        return self.value.capitalize()


class MeterBasis(Enum):
    """How a feature converts activity into tokens."""

    PER_USER_PER_MONTH = "per_user_per_month"  # Copilot 40/60, STA 30/45
    UNITS_PER_TOKEN = "units_per_token"        # "51 sessions per token"
    TOKENS_PER_UNIT = "tokens_per_unit"        # "0.5 tokens per interaction"
    VOICE_MINUTES = "voice_minutes"            # 17 min/token, 15s per-call round-up
    ZERO_RATED = "zero_rated"                  # "no charge for token usage"


class Tier(Enum):
    """Ordering for the published highest-tier rule.

    "Genesys bases charges on the highest tier (price) resource that Genesys
    Cloud uses during the interaction." Higher value = higher tier = wins.
    An interaction is charged once, at the highest tier it touched — see
    :class:`genesyscalc.usage.DeflectionMix`.
    """

    BOT = 1
    VIRTUAL_AGENT = 2
    AGENTIC_VIRTUAL_AGENT = 3


@dataclass(frozen=True)
class Meter:
    """One line of the vendor's published rate card.

    ``feature`` and ``published_rate`` hold the article's *exact* wording;
    ``rate`` holds the parsed number the arithmetic uses. Keeping both is
    what makes a transcription slip catchable — ``test_rate_card`` pins the
    strings, and a separate test proves the float agrees with the string.
    """

    key: str
    feature: str                      # the article's exact feature name
    published_rate: str               # the article's exact rate wording
    basis: MeterBasis
    rate: float = 0.0                 # tokens per unit (0.0 for per-user/zero-rated)
    per_user_named: float | None = None
    per_user_concurrent: float | None = None
    tier: Tier | None = None
    confidence: Confidence = Confidence.CONFIRMED
    source_url: str | None = None
    source_date: str | None = None
    unit_label: str = "units"
    note: str = ""

    def __post_init__(self) -> None:
        if self.rate < 0:
            raise ValueError(f"{self.key}: rate must not be negative")
        if self.confidence is Confidence.CONFIRMED and not (
            self.source_url and self.source_date
        ):
            raise ValueError(
                f"{self.key}: a confirmed (🟢) meter must carry a source URL and date"
            )
        per_user = self.basis is MeterBasis.PER_USER_PER_MONTH
        has_pair = (
            self.per_user_named is not None and self.per_user_concurrent is not None
        )
        if per_user != has_pair:
            raise ValueError(
                f"{self.key}: per-user rates are required for "
                f"{MeterBasis.PER_USER_PER_MONTH.value} and forbidden otherwise"
            )
        if self.basis is MeterBasis.ZERO_RATED and self.rate != 0.0:
            raise ValueError(f"{self.key}: a zero-rated meter must have rate 0.0")

    def tokens_per_user_month(self, licence: LicenceModel) -> float:
        """Subscription tokens for one user for one month."""
        if self.basis is not MeterBasis.PER_USER_PER_MONTH:
            raise ValueError(f"{self.key} is not a per-user meter")
        rate = (
            self.per_user_named
            if licence is LicenceModel.NAMED
            else self.per_user_concurrent
        )
        assert rate is not None  # guaranteed by __post_init__
        return rate

    def tokens_for(self, units: float) -> float:
        """Consumption tokens for ``units`` of activity.

        Voice-bot minutes are NOT priced here — they carry a per-call
        round-up that needs call counts, so
        :func:`genesyscalc.usage.voice_bot_tokens` owns that path.
        """
        if self.basis in (MeterBasis.ZERO_RATED,):
            return 0.0
        if self.basis is MeterBasis.PER_USER_PER_MONTH:
            raise ValueError(f"{self.key}: use tokens_per_user_month()")
        return units * self.rate


@dataclass(frozen=True)
class TokenPrice:
    """What one token costs, list versus negotiated.

    ``contracted_rate`` (a signed per-token rate) wins outright when set.
    Otherwise the concession is a percentage off list — the lever a
    negotiation actually moves. Both are client data and arrive from the
    notebook's ``engagement-data`` cell, never from the anchor.
    """

    currency: str = "USD"
    list_rate: float = 1.00
    contracted_rate: float | None = None
    concession_pct: float = 0.0

    def __post_init__(self) -> None:
        if self.list_rate <= 0:
            raise ValueError("list_rate must be positive")
        if not 0.0 <= self.concession_pct < 1.0:
            raise ValueError("concession_pct must be in [0, 1)")
        if self.contracted_rate is not None and self.contracted_rate < 0:
            raise ValueError("contracted_rate must not be negative")

    def effective_rate(self) -> float:
        """The rate actually billed per token."""
        if self.contracted_rate is not None:
            return self.contracted_rate
        return self.list_rate * (1.0 - self.concession_pct)
