"""Dynamic per-model pricing benchmarked against HuggingFace inference rates (issue 23).

Client-facing price per model tracks the market: 80% of the cheapest
comparable provider rate on HuggingFace's inference marketplace
(https://huggingface.co/inference/models), refreshed daily. Nodes are
unaffected — this only ever calls ``BillingLedger.set_price`` (the ledger's
existing write path), never touches node payouts (ADR-0015's 90/10 split
still applies to whatever price is charged).

Confirmed 2026-07-06: the pricing table is server-rendered into the initial
HTML response (SvelteKit SSR) — a plain stdlib ``urllib.request`` GET plus
HTML parsing is sufficient. No headless-browser fetch is required. Each
table row carries an anchor whose href is
``/<org>/<repo>/?inference_api=true&inference_provider=<provider>``, which is
a cheaper and more stable extraction anchor than the display text (which
duplicates the repo id at two responsive breakpoints).
"""

from __future__ import annotations

import json
import re
import sqlite3
import threading
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass
from html.parser import HTMLParser
from typing import Callable

HF_INFERENCE_MODELS_URL = "https://huggingface.co/inference/models"
DEFAULT_HF_PRICING_LOG_DB_PATH = "hf_pricing_log.sqlite"
DEFAULT_CLIENT_PRICE_FRACTION = 0.80  # charge 80% of the cheapest comparable rate

_ROW_HREF_RE = re.compile(
    r"^/(?P<repo>[^/]+/[^/?]+)/\?inference_api=true&inference_provider=(?P<provider>[^&\"]+)"
)
_PRICE_RE = re.compile(r"^\$[\d,]*\.?\d+$")


@dataclass(frozen=True)
class HfPriceQuote:
    """One (model, provider) row from the HF inference pricing table."""

    repo_id: str
    provider: str
    input_per_1m: float
    output_per_1m: float

    def blended_price_per_1k_tokens(self) -> float:
        """Average of input/output $-per-1M-token rates, converted to $/1k.

        The tracker bills a single per-1k-token rate (``BillingLedger``
        doesn't distinguish prompt vs. completion tokens), so this is the
        simplest fair proxy for "this provider's rate" in that unit.
        """
        return (self.input_per_1m + self.output_per_1m) / 2.0 / 1000.0

    def alias_keys(self) -> tuple[str, str]:
        """Both the bare-repo and repo::provider forms an ``hf_aliases`` entry may use."""
        return (self.repo_id.lower(), f"{self.repo_id.lower()}::{self.provider.lower()}")


class _HfPricingTableParser(HTMLParser):
    """Extracts (repo_id, provider, input$/1M, output$/1M) rows from the raw HTML."""

    def __init__(self) -> None:
        super().__init__()
        self._in_tr = False
        self._row_match: tuple[str, str] | None = None
        self._row_prices: list[float] = []
        self._in_td = False
        self._td_text: list[str] = []
        self.quotes: list[HfPriceQuote] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag == "tr":
            self._in_tr = True
            self._row_match = None
            self._row_prices = []
        elif tag == "a" and self._in_tr and self._row_match is None:
            href = dict(attrs).get("href") or ""
            m = _ROW_HREF_RE.match(href)
            if m:
                self._row_match = (
                    urllib.parse.unquote(m.group("repo")),
                    urllib.parse.unquote(m.group("provider")),
                )
        elif tag == "td":
            self._in_td = True
            self._td_text = []

    def handle_data(self, data: str) -> None:
        if self._in_td:
            self._td_text.append(data)

    def handle_endtag(self, tag: str) -> None:
        if tag == "td":
            self._in_td = False
            text = "".join(self._td_text).strip()
            if _PRICE_RE.match(text):
                self._row_prices.append(float(text.replace("$", "").replace(",", "")))
        elif tag == "tr":
            self._in_tr = False
            if self._row_match and len(self._row_prices) >= 2:
                repo_id, provider = self._row_match
                self.quotes.append(
                    HfPriceQuote(
                        repo_id=repo_id,
                        provider=provider,
                        input_per_1m=self._row_prices[0],
                        output_per_1m=self._row_prices[1],
                    )
                )
            self._row_match = None
            self._row_prices = []


def parse_hf_pricing_table(html: str) -> list[HfPriceQuote]:
    """Pure parsing function — no network I/O, so it's directly unit-testable."""
    parser = _HfPricingTableParser()
    parser.feed(html)
    return parser.quotes


def _default_fetch_html(url: str, *, timeout: float) -> str:
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return resp.read().decode("utf-8", errors="replace")


def fetch_hf_price_quotes(
    search_term: str,
    *,
    fetch_html: Callable[[str], str] | None = None,
    timeout: float = 15.0,
) -> list[HfPriceQuote]:
    """Fetch and parse the HF inference pricing table filtered by ``search_term``.

    ``fetch_html`` is the test injection point (mirrors the ``backend=``
    convention used elsewhere in this package) — it takes the full URL and
    returns the raw HTML text, so tests never hit the network.
    """
    url = f"{HF_INFERENCE_MODELS_URL}?{urllib.parse.urlencode({'search': search_term})}"
    if fetch_html is not None:
        html = fetch_html(url)
    else:
        html = _default_fetch_html(url, timeout=timeout)
    return parse_hf_pricing_table(html)


def cheapest_matching_quote(
    quotes: list[HfPriceQuote], aliases: list[str]
) -> HfPriceQuote | None:
    """Cheapest quote whose repo (optionally ``repo::provider``) is in ``aliases``.

    An alias of ``"org/repo"`` matches that repo under any provider; an
    alias of ``"org/repo::provider"`` matches only that specific provider —
    useful when only one provider's deployment has been human-verified as a
    fair comparable (matching quantization/params).
    """
    alias_set = {a.strip().lower() for a in aliases if isinstance(a, str) and a.strip()}
    if not alias_set:
        return None
    matches = [q for q in quotes if alias_set & set(q.alias_keys())]
    if not matches:
        return None
    return min(matches, key=lambda q: q.blended_price_per_1k_tokens())


class HfPricingLog:
    """Thread-safe SQLite-backed audit log of dynamic price changes (issue 23).

    Every price change (old, new, source alias/provider, timestamp) is
    recorded here so a client dispute over a charge can be reconciled
    against exactly what the market-tracking job did and when — mirrors
    ``calibration.py``'s persistence shape.
    """

    def __init__(self, db_path: str | None = None) -> None:
        self._db_path = db_path
        self._lock = threading.Lock()
        self._changes: list[dict] = []
        if self._db_path:
            self._init_db()
            self._load_from_db()

    def record_change(
        self,
        *,
        model: str,
        old_price_per_1k: float,
        new_price_per_1k: float,
        source_repo_id: str,
        source_provider: str,
        ts: float | None = None,
    ) -> dict:
        change = {
            "model": model,
            "old_price_per_1k": old_price_per_1k,
            "new_price_per_1k": new_price_per_1k,
            "source_repo_id": source_repo_id,
            "source_provider": source_provider,
            "ts": ts if ts is not None else time.time(),
        }
        with self._lock:
            self._changes.append(change)
            self._save_change(change)
        return change

    def history(self, model: str | None = None, *, limit: int = 200) -> list[dict]:
        with self._lock:
            changes = list(self._changes)
        if model is not None:
            changes = [c for c in changes if c["model"] == model]
        return changes[-limit:]

    # ---- persistence (billing.py / calibration.py pattern) ----

    def _init_db(self) -> None:
        con = sqlite3.connect(self._db_path)  # type: ignore[arg-type]
        con.execute(
            "CREATE TABLE IF NOT EXISTS hf_price_changes "
            "(id INTEGER PRIMARY KEY AUTOINCREMENT, model TEXT NOT NULL, "
            "payload TEXT NOT NULL, ts REAL NOT NULL)"
        )
        con.commit()
        con.close()

    def _load_from_db(self) -> None:
        con = sqlite3.connect(self._db_path)  # type: ignore[arg-type]
        rows = con.execute(
            "SELECT payload FROM hf_price_changes ORDER BY ts, id"
        ).fetchall()
        con.close()
        for (payload,) in rows:
            try:
                self._changes.append(json.loads(payload))
            except json.JSONDecodeError:
                continue

    def _save_change(self, change: dict) -> None:
        if not self._db_path:
            return
        con = sqlite3.connect(self._db_path)  # type: ignore[arg-type]
        con.execute(
            "INSERT INTO hf_price_changes (model, payload, ts) VALUES (?, ?, ?)",
            (change["model"], json.dumps(change), float(change["ts"])),
        )
        con.commit()
        con.close()


def hf_search_term(preset: dict, model_name: str) -> str:
    """Best-effort search term for the HF pricing page's ``?search=`` filter."""
    hf_repo = preset.get("hf_repo")
    if isinstance(hf_repo, str) and hf_repo:
        return hf_repo.rsplit("/", 1)[-1]
    return model_name


def refresh_preset_price(
    *,
    model_name: str,
    preset: dict,
    current_price: float,
    fetch_html: Callable[[str], str] | None = None,
    price_fraction: float = DEFAULT_CLIENT_PRICE_FRACTION,
) -> dict | None:
    """Compute the new price for one preset, or None if nothing should change.

    Never raises — any fetch/parse failure or absence of a verified match is
    treated identically: keep the static default (deliverable's fallback
    requirement). Callers are responsible for actually applying the result
    (``BillingLedger.set_price`` + logging), so this function stays a pure
    "what should the new price be" computation and is trivially unit-testable.
    """
    aliases = preset.get("hf_aliases")
    if not aliases:
        return None
    try:
        quotes = fetch_hf_price_quotes(
            hf_search_term(preset, model_name), fetch_html=fetch_html
        )
        quote = cheapest_matching_quote(quotes, aliases)
    except Exception:
        return None
    if quote is None:
        return None
    new_price = round(quote.blended_price_per_1k_tokens() * price_fraction, 6)
    if new_price <= 0:
        return None
    return {
        "model": model_name,
        "old_price_per_1k": current_price,
        "new_price_per_1k": new_price,
        # US-045: per-side rates (per 1k tokens) so the ledger bills input
        # and output at the provider's actual asymmetry, not the average.
        "new_input_price_per_1k": round(quote.input_per_1m * price_fraction / 1000.0, 6),
        "new_output_price_per_1k": round(quote.output_per_1m * price_fraction / 1000.0, 6),
        "source_repo_id": quote.repo_id,
        "source_provider": quote.provider,
    }


__all__ = [
    "HF_INFERENCE_MODELS_URL",
    "DEFAULT_HF_PRICING_LOG_DB_PATH",
    "DEFAULT_CLIENT_PRICE_FRACTION",
    "HfPriceQuote",
    "HfPricingLog",
    "parse_hf_pricing_table",
    "fetch_hf_price_quotes",
    "cheapest_matching_quote",
    "hf_search_term",
    "refresh_preset_price",
]
