"""Payer config loader. SP9.

Reads `config/payers.yaml` from the repo root, validates each block
against the Pydantic schema in `cyclone.providers`, and returns a
``(payer_id, transaction_type) -> config`` dict. Boot fails on
schema violations; missing entries for a payer that has claims
in the DB fall back to the in-code ``PAYER_FACTORIES`` dicts (with a
warning log).

The loader is called once at boot from `cyclone.api.lifespan`. The
``/api/admin/reload-config`` endpoint re-runs it without restart.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import yaml
from pydantic import ValidationError

from cyclone.providers import PayerConfig277CA, PayerConfig837, PayerConfig835

log = logging.getLogger(__name__)

DEFAULT_CONFIG_PATH = Path(__file__).resolve().parents[3] / "config" / "payers.yaml"


# In-memory config registry
_CONFIGS: dict[tuple[str, str], dict[str, Any]] = {}


def _tx_to_model(transaction_type: str) -> type:
    """Map transaction type string to Pydantic config model.

    Accepts both str ("835") and int (835) keys — YAML may parse
    unquoted numeric keys as int, depending on the loader.
    """
    tx_str = str(transaction_type)
    if tx_str in ("837P", "837I", "837D"):
        return PayerConfig837
    if tx_str == "835":
        return PayerConfig835
    if tx_str == "277CA":
        return PayerConfig277CA
    # Other tx types (999, TA1, 270, 271) reuse the 837 schema for now
    # — they have similar per-payer config shapes.
    return PayerConfig837


def load_payer_configs(path: Path | str = DEFAULT_CONFIG_PATH) -> dict[tuple[str, str], dict[str, Any]]:
    """Load and validate payer configs from YAML. Returns the populated registry.

    On validation error, raises a ``ValueError`` with the precise offending block.
    """
    path = Path(path)
    if not path.exists():
        log.warning("payers.yaml not found at %s; using empty config", path)
        _CONFIGS.clear()
        return _CONFIGS

    raw = yaml.safe_load(path.read_text())
    if not isinstance(raw, dict) or "payers" not in raw:
        raise ValueError(f"{path}: missing top-level 'payers:' key")

    new_configs: dict[tuple[str, str], dict[str, Any]] = {}
    for payer in raw["payers"]:
        payer_id = payer.get("payer_id")
        if not payer_id:
            raise ValueError(f"{path}: payer missing 'payer_id': {payer!r}")
        for tx, block in payer.get("configs", {}).items():
            model = _tx_to_model(tx)
            try:
                validated = model.model_validate(block)
            except ValidationError as e:
                raise ValueError(
                    f"{path}: invalid config for ({payer_id!r}, {tx!r}): {e}"
                ) from e
            new_configs[(payer_id, tx)] = validated.model_dump()

    _CONFIGS.clear()
    _CONFIGS.update(new_configs)
    log.info("Loaded %d payer configs from %s", len(_CONFIGS), path)
    return _CONFIGS


def get_config(payer_id: str, transaction_type: str) -> dict[str, Any] | None:
    """Get a payer config block, or None if not present.

    Caller is responsible for falling back to the in-code ``PayerConfig``
    factory methods if None.
    """
    return _CONFIGS.get((payer_id, transaction_type))


def all_configs() -> dict[tuple[str, str], dict[str, Any]]:
    """Snapshot of the current config registry (for the admin endpoint)."""
    return dict(_CONFIGS)


def reset() -> None:
    """Clear the in-memory registry. Test-only."""
    _CONFIGS.clear()
