"""Pricing service implementing commodity revenue calculations.

This module exposes data models and helpers for computing product pricing
according to the formulas outlined in
``calminer-docs/specifications/price_calculation.md``. It focuses on the core
calculation steps (payable metal, penalties, net revenue) and is intended to be
composed within broader scenario evaluation workflows.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Mapping

from pydantic import BaseModel, Field, PositiveFloat, field_validator
from services.currency import require_currency


class PricingInput(BaseModel):
    """Normalized inputs for pricing calculations."""

    metal: str = Field(..., min_length=1)
    ore_tonnage: PositiveFloat = Field(
        ..., description="Total ore mass processed (metric tonnes)")
    head_grade_pct: PositiveFloat = Field(..., gt=0,
                                          le=100, description="Head grade as percent")
    recovery_pct: PositiveFloat = Field(..., gt=0,
                                        le=100, description="Recovery rate percent")
    payable_pct: float | None = Field(
        None, gt=0, le=100, description="Contractual payable percentage")
    reference_price: PositiveFloat = Field(
        ..., description="Reference price in base currency per unit")
    treatment_charge: float = Field(0, ge=0)
    smelting_charge: float = Field(0, ge=0)
    moisture_pct: float = Field(0, ge=0, le=100)
    moisture_threshold_pct: float | None = Field(None, ge=0, le=100)
    moisture_penalty_per_pct: float | None = Field(None)
    impurity_ppm: Mapping[str, float] = Field(default_factory=dict)
    impurity_thresholds: Mapping[str, float] = Field(default_factory=dict)
    impurity_penalty_per_ppm: Mapping[str, float] = Field(default_factory=dict)
    premiums: float = Field(0)
    fx_rate: PositiveFloat = Field(
        1, description="Multiplier to convert to scenario currency")
    currency_code: str | None = Field(
        None, description="Optional explicit currency override")

    @field_validator("impurity_ppm", mode="before")
    @classmethod
    def _validate_impurity_mapping(cls, value):
        if isinstance(value, Mapping):
            return {k: float(v) for k, v in value.items()}
        return value


class PricingResult(BaseModel):
    """Structured output summarising pricing computation results."""

    metal: str
    ore_tonnage: float
    head_grade_pct: float
    recovery_pct: float
    payable_metal_tonnes: float
    reference_price: float
    gross_revenue: float
    moisture_penalty: float
    impurity_penalty: float
    treatment_smelt_charges: float
    premiums: float
    net_revenue: float
    currency: str | None


@dataclass(frozen=True)
class PricingMetadata:
    """Metadata defaults applied when explicit inputs are omitted."""

    default_payable_pct: float = 100.0
    default_currency: str | None = "USD"
    moisture_threshold_pct: float = 8.0
    moisture_penalty_per_pct: float = 0.0
    impurity_thresholds: Mapping[str, float] = field(default_factory=dict)
    impurity_penalty_per_ppm: Mapping[str, float] = field(default_factory=dict)


def calculate_pricing(
    pricing_input: PricingInput,
    *,
    metadata: PricingMetadata | None = None,
    currency: str | None = None,
) -> PricingResult:
    """Calculate pricing metrics for the provided commodity input.

    Parameters
    ----------
    pricing_input:
        Normalised input data including ore tonnage, grades, charges, and
        optional penalties.
    metadata:
        Optional default metadata applied when specific values are omitted from
        ``pricing_input``.
    currency:
        Optional override for the output currency label. Falls back to
        ``metadata.default_currency`` when not provided.
    """

    applied_metadata = metadata or PricingMetadata()

    payable_pct = (
        pricing_input.payable_pct
        if pricing_input.payable_pct is not None
        else applied_metadata.default_payable_pct
    )
    moisture_threshold = (
        pricing_input.moisture_threshold_pct
        if pricing_input.moisture_threshold_pct is not None
        else applied_metadata.moisture_threshold_pct
    )
    moisture_penalty_factor = (
        pricing_input.moisture_penalty_per_pct
        if pricing_input.moisture_penalty_per_pct is not None
        else applied_metadata.moisture_penalty_per_pct
    )

    impurity_thresholds = {
        **applied_metadata.impurity_thresholds,
        **pricing_input.impurity_thresholds,
    }
    impurity_penalty_factors = {
        **applied_metadata.impurity_penalty_per_ppm,
        **pricing_input.impurity_penalty_per_ppm,
    }

    q_metal = pricing_input.ore_tonnage * (pricing_input.head_grade_pct / 100.0) * (
        pricing_input.recovery_pct / 100.0
    )
    payable_metal = q_metal * (payable_pct / 100.0)

    gross_revenue_ref = payable_metal * pricing_input.reference_price
    charges = pricing_input.treatment_charge + pricing_input.smelting_charge

    moisture_excess = max(0.0, pricing_input.moisture_pct - moisture_threshold)
    moisture_penalty = moisture_excess * moisture_penalty_factor

    impurity_penalty_total = 0.0
    for impurity, value in pricing_input.impurity_ppm.items():
        threshold = impurity_thresholds.get(impurity, 0.0)
        penalty_factor = impurity_penalty_factors.get(impurity, 0.0)
        impurity_penalty_total += max(0.0, value - threshold) * penalty_factor

    net_revenue_ref = (
        gross_revenue_ref - charges - moisture_penalty - impurity_penalty_total
    )
    net_revenue_ref += pricing_input.premiums

    net_revenue = net_revenue_ref * pricing_input.fx_rate

    currency_code = require_currency(
        currency or pricing_input.currency_code,
        default=applied_metadata.default_currency,
    )

    return PricingResult(
        metal=pricing_input.metal,
        ore_tonnage=pricing_input.ore_tonnage,
        head_grade_pct=pricing_input.head_grade_pct,
        recovery_pct=pricing_input.recovery_pct,
        payable_metal_tonnes=payable_metal,
        reference_price=pricing_input.reference_price,
        gross_revenue=gross_revenue_ref,
        moisture_penalty=moisture_penalty,
        impurity_penalty=impurity_penalty_total,
        treatment_smelt_charges=charges,
        premiums=pricing_input.premiums,
        net_revenue=net_revenue,
        currency=currency_code,
    )
