import argparse
import datetime as _dt
import html
import json
import logging
import os
import re
import urllib.request
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple


OPENAI_PRICING_URL = "https://developers.openai.com/api/docs/pricing"
OPENAI_PRIORITY_URL = "https://openai.com/api-priority-processing/"
OPENAI_MODEL_DOC_URL_TEMPLATE = "https://developers.openai.com/api/docs/models/{model}"
GOOGLE_PRICING_URL = "https://ai.google.dev/gemini-api/docs/pricing"
VERTEX_PRICING_URL = "https://cloud.google.com/vertex-ai/generative-ai/pricing"
INCEPTION_PRICING_URL = "https://docs.inceptionlabs.ai/get-started/models"
REQUESTY_PROVIDER_URL_TEMPLATE = "https://www.requesty.ai/models/{provider}"
USER_AGENT = "Mozilla/5.0 (compatible; dhAIBenchPriceCatalog/1.0; +https://github.com/otichy/dhAIBench)"
HTTP_TIMEOUT_SECONDS = 30


FetchHtmlFn = Callable[[str], Tuple[str, str]]


def utc_timestamp() -> str:
    return _dt.datetime.now(tz=_dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def sanitize_model_identifier(model: str) -> str:
    slug = re.sub(r"[^0-9A-Za-z]+", "", str(model or "").lower())
    return slug or "model"


def normalize_provider_key(provider: str) -> str:
    normalized = str(provider or "").strip().lower()
    if normalized == "einfra":
        return "e-infra"
    return normalized


def load_model_catalog_js(path: str) -> Dict[str, Any]:
    with open(path, "r", encoding="utf-8") as handle:
        text = handle.read()
    match = re.search(r"window\.MODEL_CATALOG\s*=\s*(\{.*\})\s*;\s*$", text, re.DOTALL)
    if not match:
        raise ValueError(f"Could not extract MODEL_CATALOG from {path}")
    return json.loads(match.group(1))


def load_pricing_catalog_js(path: str) -> Dict[str, Any]:
    with open(path, "r", encoding="utf-8") as handle:
        text = handle.read()
    match = re.search(r"window\.MODEL_PRICING_CATALOG\s*=\s*(\{.*\})\s*;\s*$", text, re.DOTALL)
    if not match:
        raise ValueError(f"Could not extract MODEL_PRICING_CATALOG from {path}")
    return json.loads(match.group(1))


def write_pricing_catalog_js(catalog: Dict[str, Any], output_path: str) -> None:
    normalized_catalog = strip_status_fields(catalog)
    content = (
        "// Auto-generated by scripts/update_model_prices.py\n"
        "window.MODEL_PRICING_CATALOG = "
        + json.dumps(normalized_catalog, indent=2, ensure_ascii=False)
        + ";\n"
    )
    target_paths = [Path(output_path)]
    primary_path = target_paths[0]
    dashboard_mirror_path = primary_path.parent / "web" / primary_path.name
    if (
        primary_path.name == "config_prices.js"
        and dashboard_mirror_path != primary_path
        and dashboard_mirror_path.parent.is_dir()
    ):
        target_paths.append(dashboard_mirror_path)

    for path in target_paths:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")


class HTTP308RedirectHandler(urllib.request.HTTPRedirectHandler):
    def http_error_308(self, req, fp, code, msg, headers):
        return self.http_error_307(req, fp, code, msg, headers)


def build_default_fetcher() -> FetchHtmlFn:
    opener = urllib.request.build_opener(HTTP308RedirectHandler())
    cache: Dict[str, Tuple[str, str]] = {}

    def fetch(url: str) -> Tuple[str, str]:
        cached = cache.get(url)
        if cached is not None:
            return cached
        request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
        with opener.open(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
            body = response.read().decode("utf-8", errors="replace")
            final_url = response.geturl()
        result = (final_url, body)
        cache[url] = result
        return result

    return fetch


def html_to_text(raw_html: str) -> str:
    text = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", raw_html or "", flags=re.IGNORECASE | re.DOTALL)
    text = re.sub(r"<!--.*?-->", " ", text, flags=re.DOTALL)
    text = re.sub(r"<[^>]+>", " ", text)
    text = html.unescape(text)
    text = text.replace("\xa0", " ")
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def extract_hrefs(raw_html: str) -> List[str]:
    return sorted(set(re.findall(r'href="([^"]+)"', raw_html or "", flags=re.IGNORECASE)))


def price_number(token: Optional[str]) -> Optional[float]:
    if token is None:
        return None
    stripped = str(token).strip()
    if not stripped or stripped in {"-", "\u2014", "N/A", "NA"}:
        return None
    normalized = stripped.replace("$", "").replace(",", "")
    return float(normalized)


def service_tier_entry(
    input_price: Optional[float],
    cached_input_price: Optional[float],
    output_price: Optional[float],
) -> Dict[str, Optional[float]]:
    return {
        "input_usd_per_mtokens": input_price,
        "cached_input_usd_per_mtokens": cached_input_price,
        "output_usd_per_mtokens": output_price,
    }


def parse_optional_float(value: Any) -> Optional[float]:
    if value is None or isinstance(value, bool):
        return None
    if isinstance(value, (int, float)):
        parsed = float(value)
        return parsed if parsed == parsed and parsed not in (float("inf"), float("-inf")) else None
    text = str(value).strip()
    if not text or "*" in text:
        return None
    try:
        parsed = float(text)
    except ValueError:
        return None
    return parsed if parsed == parsed and parsed not in (float("inf"), float("-inf")) else None


def dedupe_sources(sources: Iterable[Dict[str, str]]) -> List[Dict[str, str]]:
    seen = set()
    deduped: List[Dict[str, str]] = []
    for source in sources:
        if not isinstance(source, dict):
            continue
        label = str(source.get("label") or "").strip()
        url = str(source.get("url") or "").strip()
        key = (label, url)
        if not label or not url or key in seen:
            continue
        seen.add(key)
        deduped.append({"label": label, "url": url})
    return deduped


def has_pricing_ref(entry: Optional[Dict[str, Any]]) -> bool:
    return bool(str((entry or {}).get("pricing_ref") or "").strip())


def has_reason(entry: Optional[Dict[str, Any]]) -> bool:
    return bool(str((entry or {}).get("reason") or "").strip())


def get_service_tiers(entry: Optional[Dict[str, Any]]) -> Dict[str, Dict[str, Optional[float]]]:
    service_tiers = (entry or {}).get("service_tiers")
    return service_tiers if isinstance(service_tiers, dict) else {}


def has_any_usable_rates(entry: Optional[Dict[str, Any]]) -> bool:
    for tier_payload in get_service_tiers(entry).values():
        if not isinstance(tier_payload, dict):
            continue
        if any(
            tier_payload.get(key) is not None
            for key in ("input_usd_per_mtokens", "cached_input_usd_per_mtokens", "output_usd_per_mtokens")
        ):
            return True
    return False


def is_priced_entry(entry: Optional[Dict[str, Any]]) -> bool:
    return not has_pricing_ref(entry) and has_any_usable_rates(entry)


def strip_status_fields(obj: Any) -> Any:
    if isinstance(obj, dict):
        return {key: strip_status_fields(value) for key, value in obj.items() if key != "status"}
    if isinstance(obj, list):
        return [strip_status_fields(item) for item in obj]
    return obj


def priced_entry(
    service_tiers: Dict[str, Dict[str, Optional[float]]],
    sources: Iterable[Dict[str, str]],
    long_context: Optional[Dict[str, Any]] = None,
    notes: Optional[List[str]] = None,
) -> Dict[str, Any]:
    entry: Dict[str, Any] = {
        "service_tiers": service_tiers,
        "sources": dedupe_sources(sources),
    }
    if long_context:
        entry["long_context"] = long_context
    if notes:
        entry["notes"] = list(notes)
    return entry


def alias_entry(pricing_ref: str, alias_kind: str) -> Dict[str, Any]:
    return {
        "pricing_ref": pricing_ref,
        "alias_kind": alias_kind,
    }


def unsupported_entry(reason: str, sources: Optional[Iterable[Dict[str, str]]] = None) -> Dict[str, Any]:
    entry: Dict[str, Any] = {
        "reason": reason,
    }
    deduped = dedupe_sources(sources or [])
    if deduped:
        entry["sources"] = deduped
    return entry


def unpriced_entry(reason: str) -> Dict[str, Any]:
    return {
        "reason": reason,
        "needs_manual_update": True,
        "service_tiers": {
            "standard": service_tier_entry(None, None, None),
        },
    }


def metadata_priced_entry(
    metadata: Dict[str, Any],
    source_url: str,
) -> Optional[Dict[str, Any]]:
    input_cost = parse_optional_float(metadata.get("input_cost_per_token"))
    cached_input_cost = parse_optional_float(metadata.get("cached_input_cost_per_token"))
    output_cost = parse_optional_float(metadata.get("output_cost_per_token"))
    if input_cost is None and cached_input_cost is None and output_cost is None:
        return None
    sources = [{"label": "Provider model metadata", "url": source_url}] if source_url else []
    notes = ["Pricing was imported from provider model metadata."]
    return priced_entry(
        {
            "standard": service_tier_entry(
                input_cost * 1_000_000 if input_cost is not None else None,
                cached_input_cost * 1_000_000 if cached_input_cost is not None else None,
                output_cost * 1_000_000 if output_cost is not None else None,
            )
        },
        sources=sources,
        notes=notes,
    )


def model_metadata_source_entries(provider: str, model_catalog: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    provider_entry = model_catalog.get(provider) or {}
    metadata_map = provider_entry.get("model_metadata")
    if not isinstance(metadata_map, dict):
        return {}
    source_url = str(provider_entry.get("model_metadata_endpoint") or provider_entry.get("models_api_base") or "").strip()
    entries: Dict[str, Dict[str, Any]] = {}
    for model_id, metadata in metadata_map.items():
        if not isinstance(metadata, dict):
            continue
        entry = metadata_priced_entry(metadata, source_url)
        if entry is not None:
            entries[str(model_id)] = entry
    return entries


def merge_priced_entries(*entries: Dict[str, Any]) -> Dict[str, Any]:
    merged_tiers: Dict[str, Dict[str, Optional[float]]] = {}
    merged_sources: List[Dict[str, str]] = []
    merged_long_context: Optional[Dict[str, Any]] = None
    merged_notes: List[str] = []
    for entry in entries:
        if not is_priced_entry(entry):
            continue
        for tier_name, tier_payload in (entry.get("service_tiers") or {}).items():
            if tier_name not in merged_tiers:
                merged_tiers[tier_name] = dict(tier_payload)
                continue
            current = merged_tiers[tier_name]
            for key, value in tier_payload.items():
                if current.get(key) is None and value is not None:
                    current[key] = value
        merged_sources.extend(entry.get("sources") or [])
        if not merged_long_context and entry.get("long_context"):
            merged_long_context = dict(entry["long_context"])
        if entry.get("notes"):
            merged_notes.extend(str(note) for note in entry["notes"])
    merged = priced_entry(merged_tiers, merged_sources, long_context=merged_long_context)
    if merged_notes:
        merged["notes"] = sorted(set(merged_notes))
    return merged


OPENAI_UNSUPPORTED_MARKERS = (
    "audio",
    "image",
    "transcribe",
    "tts",
    "search",
    "moderation",
    "embedding",
    "whisper",
    "dall-e",
    "sora",
    "deep-research",
    "computer-use",
)
SAFE_OPENAI_SNAPSHOT_PREFIXES = (
    "gpt-4.1",
    "gpt-5",
    "gpt-5.1",
    "gpt-5.2",
    "gpt-5.5",
    "gpt-5.4",
    "o1-pro",
    "o3",
    "o4-mini",
)

OPENAI_FLAGSHIP_MODELS = (
    "gpt-5.6-sol",
    "gpt-5.6-terra",
    "gpt-5.6-luna",
    "gpt-5.5",
    "gpt-5.5-pro",
    "gpt-5.4",
    "gpt-5.4-mini",
    "gpt-5.4-nano",
    "gpt-5.4-pro",
)


def is_openai_text_pricing_candidate(model: str) -> bool:
    normalized = str(model or "").strip().lower()
    if not normalized:
        return False
    if normalized in {"babbage-002", "davinci-002"}:
        return True
    if not (normalized.startswith("gpt") or normalized.startswith("o")):
        return False
    if any(marker in normalized for marker in OPENAI_UNSUPPORTED_MARKERS):
        return False
    return True


def openai_snapshot_base(model: str) -> Optional[str]:
    candidate = str(model or "").strip().lower()
    if not re.search(r"-\d{4}-\d{2}-\d{2}$", candidate):
        return None
    base = re.sub(r"-\d{4}-\d{2}-\d{2}$", "", candidate)
    if base == candidate:
        return None
    if not any(base.startswith(prefix) for prefix in SAFE_OPENAI_SNAPSHOT_PREFIXES):
        return None
    if base.startswith("gpt-4o"):
        return None
    return base


def extract_openai_model_page_prices(page_text: str) -> Optional[Dict[str, Any]]:
    matches = list(
        re.finditer(
            r"Text tokens\s+Per\s+1M tokens(?:\s+\u2219\s+Batch API price)?\s+Input\s+(\$[0-9.]+)"
            r"(?:\s+Cached input\s+(\$[0-9.]+))?\s+Output\s+(\$[0-9.]+)",
            page_text,
            flags=re.IGNORECASE,
        )
    )
    if not matches:
        return None
    service_tiers: Dict[str, Dict[str, Optional[float]]] = {}
    standard = matches[0]
    service_tiers["standard"] = service_tier_entry(
        price_number(standard.group(1)),
        price_number(standard.group(2)),
        price_number(standard.group(3)),
    )
    if len(matches) > 1:
        batch = matches[1]
        service_tiers["batch"] = service_tier_entry(
            price_number(batch.group(1)),
            price_number(batch.group(2)),
            price_number(batch.group(3)),
        )
    return service_tiers


def extract_openai_flagship_row(body: str, model: str) -> Optional[Tuple[Dict[str, Optional[float]], Optional[Dict[str, Any]]]]:
    pattern = re.compile(
        rf"{re.escape(model)}(?P<prices>(?:\s*(?:\$[0-9.]+|-)){{6,8}})",
        flags=re.IGNORECASE,
    )
    match = pattern.search(body)
    if not match:
        return None
    prices = [
        price_number(token)
        for token in re.findall(r"\$[0-9.]+|-", match.group("prices"))
    ]
    if len(prices) >= 8:
        # The current table includes cache-write prices. The dashboard does not
        # yet model that usage bucket, so retain input/cache-read/output only.
        short_prices = (prices[0], prices[1], prices[3])
        long_prices = (prices[4], prices[5], prices[7])
    else:
        short_prices = (prices[0], prices[1], prices[2])
        long_prices = (prices[3], prices[4], prices[5])
    short_tier = service_tier_entry(
        *short_prices,
    )
    long_context_prices = service_tier_entry(
        *long_prices,
    )
    long_context = None
    if any(value is not None for value in long_context_prices.values()):
        threshold = 272000 if model.startswith(("gpt-5.4", "gpt-5.6")) else None
        long_context = {
            "threshold_input_tokens": threshold,
            "service_tiers": {
                "standard": long_context_prices,
            },
        }
    return short_tier, long_context


def extract_openai_three_price_row(body: str, model: str) -> Optional[Tuple[Optional[float], Optional[float], Optional[float]]]:
    pattern = re.compile(
        rf"{re.escape(model)}\s+(\$[0-9.]+|-)\s+(\$[0-9.]+|-)\s+(\$[0-9.]+|-)",
        flags=re.IGNORECASE,
    )
    match = pattern.search(body)
    if not match:
        return None
    return (
        price_number(match.group(1)),
        price_number(match.group(2)),
        price_number(match.group(3)),
    )


def parse_openai_pricing_page(page_text: str) -> Dict[str, Dict[str, Any]]:
    entries: Dict[str, Dict[str, Any]] = {}
    flagship_sections = {
        "standard": "Standard",
        "batch": "Batch",
        "flex": "Flex",
        "priority": "Priority",
    }
    table_header = r"Short context Long context Model Input Cached input(?: Cache writes)? Output Input Cached input(?: Cache writes)? Output"
    for tier_name, tier_label in flagship_sections.items():
        marker = re.search(
            rf"(?<![A-Za-z]){tier_label}\s+{table_header}",
            page_text,
            flags=re.IGNORECASE,
        )
        if marker is None:
            continue
        idx = marker.start()
        body_start = marker.end()
        next_all_models = page_text.find("All models", body_start)
        next_multimodal = page_text.find("Multimodal models", body_start)
        boundaries = [value for value in (next_all_models, next_multimodal) if value != -1]
        end_idx = min(boundaries) if boundaries else len(page_text)
        body = page_text[body_start:end_idx]
        for model in OPENAI_FLAGSHIP_MODELS:
            row = extract_openai_flagship_row(body, model)
            if row is None:
                continue
            short_tier, long_context = row
            base = priced_entry(
                {tier_name: short_tier},
                [{"label": "OpenAI Pricing", "url": OPENAI_PRICING_URL}],
                long_context=long_context,
            )
            entries[model] = merge_priced_entries(entries.get(model) or {}, base)

    specialized_standard_idx = page_text.find("Standard Category Model Input Cached input Output")
    specialized_priority_idx = page_text.find("Priority Category Model Input Cached input Output")
    if specialized_standard_idx != -1:
        specialized_standard_body = page_text[specialized_standard_idx:]
        for model in ("gpt-5.3-chat-latest", "gpt-5.3-codex"):
            prices = extract_openai_three_price_row(specialized_standard_body, model)
            if prices is None:
                continue
            entries[model] = merge_priced_entries(
                entries.get(model) or {},
                priced_entry(
                    {"standard": service_tier_entry(*prices)},
                    [{"label": "OpenAI Pricing", "url": OPENAI_PRICING_URL}],
                ),
            )
    if specialized_priority_idx != -1:
        specialized_priority_body = page_text[specialized_priority_idx:]
        prices = extract_openai_three_price_row(specialized_priority_body, "gpt-5.3-codex")
        if prices is not None:
            entries["gpt-5.3-codex"] = merge_priced_entries(
                entries.get("gpt-5.3-codex") or {},
                priced_entry(
                    {"priority": service_tier_entry(*prices)},
                    [{"label": "OpenAI Pricing", "url": OPENAI_PRICING_URL}],
                ),
            )
    return entries


def extract_priority_row(page_text: str, label: str) -> Optional[Tuple[Optional[float], Optional[float], Optional[float]]]:
    pattern = re.compile(
        rf"(?<![A-Za-z0-9.]){re.escape(label)}(?![A-Za-z0-9.-]).{{0,120}}?(\$[0-9.]+|-|\u2014)\s+(\$[0-9.]+|-|\u2014)\s+(\$[0-9.]+|-|\u2014)",
        flags=re.IGNORECASE | re.DOTALL,
    )
    match = pattern.search(page_text)
    if not match:
        return None
    return (
        price_number(match.group(1)),
        price_number(match.group(2)),
        price_number(match.group(3)),
    )


def parse_openai_priority_page(page_text: str) -> Dict[str, Dict[str, Any]]:
    label_map = {
        "GPT-5.4": "gpt-5.4",
        "GPT-5.2": "gpt-5.2",
        "GPT-5.1": "gpt-5.1",
        "GPT-5 mini": "gpt-5-mini",
        "GPT-5.1 codex": "gpt-5.1-codex",
        "GPT-5 codex": "gpt-5-codex",
        "GPT-4.1 nano": "gpt-4.1-nano",
        "GPT-4.1 mini": "gpt-4.1-mini",
        "GPT-4.1": "gpt-4.1",
        "GPT-4o mini": "gpt-4o-mini",
        "o3": "o3",
        "o4-mini": "o4-mini",
    }
    entries: Dict[str, Dict[str, Any]] = {}
    for label, model in label_map.items():
        row = extract_priority_row(page_text, label)
        if row is None:
            continue
        entries[model] = priced_entry(
            {"priority": service_tier_entry(*row)},
            [{"label": "OpenAI Priority Processing", "url": OPENAI_PRIORITY_URL}],
        )
    return entries


def build_openai_source_entries(models: Iterable[str], fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    entries: Dict[str, Dict[str, Any]] = {}

    try:
        _, pricing_html = fetch_html(OPENAI_PRICING_URL)
        pricing_entries = parse_openai_pricing_page(html_to_text(pricing_html))
        for model, entry in pricing_entries.items():
            entries[model] = entry
    except Exception as exc:  # noqa: BLE001
        logging.warning("Failed to fetch OpenAI pricing page: %s", exc)
    try:
        _, priority_html = fetch_html(OPENAI_PRIORITY_URL)
        priority_entries = parse_openai_priority_page(html_to_text(priority_html))
        for model, entry in priority_entries.items():
            entries[model] = merge_priced_entries(entries.get(model) or {}, entry)
    except Exception as exc:  # noqa: BLE001
        logging.warning("Failed to fetch OpenAI priority page: %s", exc)
    for model in sorted(set(str(item).strip() for item in models if str(item).strip())):
        if not is_openai_text_pricing_candidate(model):
            continue
        page_url = OPENAI_MODEL_DOC_URL_TEMPLATE.format(model=model)
        try:
            final_url, raw_html = fetch_html(page_url)
        except Exception as exc:  # noqa: BLE001
            logging.info("OpenAI model page unavailable for %s: %s", model, exc)
            continue
        parsed_tiers = extract_openai_model_page_prices(html_to_text(raw_html))
        if not parsed_tiers:
            continue
        entry = priced_entry(parsed_tiers, [{"label": "OpenAI Model Page", "url": final_url}])
        entries[model] = merge_priced_entries(entries.get(model) or {}, entry)
    return entries


GOOGLE_MODEL_LABELS = {
    "models/gemini-3.5-flash": "Gemini 3.5 Flash",
    "models/gemini-3.1-pro-preview": "Gemini 3.1 Pro Preview",
    "models/gemini-3.1-flash-lite": "Gemini 3.1 Flash-Lite",
    "models/gemini-3.1-flash-lite-preview": "Gemini 3.1 Flash-Lite Preview",
    "models/gemini-3-flash-preview": "Gemini 3 Flash Preview",
    "models/gemini-2.5-pro": "Gemini 2.5 Pro",
    "models/gemini-2.5-flash": "Gemini 2.5 Flash",
    "models/gemini-2.5-flash-lite": "Gemini 2.5 Flash-Lite",
    "models/gemini-2.5-flash-lite-preview-09-2025": "Gemini 2.5 Flash-Lite Preview",
}
GOOGLE_EXPLICIT_ALIASES = {
    "models/gemini-3.1-pro-preview-customtools": "models/gemini-3.1-pro-preview",
    "models/gemini-2.0-flash-001": "models/gemini-2.0-flash",
    "models/gemini-2.0-flash-lite-001": "models/gemini-2.0-flash-lite",
}
VERTEX_MODEL_LABELS = {
    "gemini-3.5-flash": "Gemini 3.5 Flash",
    "gemini-3.1-pro-preview": "Gemini 3.1 Pro Preview",
    "gemini-3.1-flash-lite": "Gemini 3.1 Flash-Lite",
    "gemini-3.1-flash-lite-preview": "Gemini 3.1 Flash-Lite Preview",
    "gemini-3-flash-preview": "Gemini 3 Flash Preview",
    "gemini-3-pro-preview": "Gemini 3 Pro Preview",
    "gemini-2.5-pro": "Gemini 2.5 Pro",
    "gemini-2.5-flash": "Gemini 2.5 Flash",
    "gemini-2.5-flash-lite": "Gemini 2.5 Flash Lite",
}

GOOGLE_TEXT_RATE_OVERRIDES = {
    "models/gemini-3-pro-image": {
        "service_tiers": {
            "standard": service_tier_entry(2.0, None, 12.0),
            "batch": service_tier_entry(1.0, None, 6.0),
            "flex": service_tier_entry(1.0, None, 6.0),
            "priority": service_tier_entry(3.6, None, 21.6),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "models/gemini-3.1-flash-image": {
        "service_tiers": {
            "standard": service_tier_entry(0.5, None, 3.0),
            "batch": service_tier_entry(0.25, None, 1.5),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "models/gemini-3.5-live-translate-preview": {
        "service_tiers": {"standard": service_tier_entry(3.5, None, 21.0)},
        "notes": [
            "Audio minute equivalents are documented by the provider but are not represented in this catalog entry."
        ],
    },
    "models/gemini-3.1-flash-lite-image": {
        "service_tiers": {
            "standard": service_tier_entry(0.25, None, 1.5),
            "batch": service_tier_entry(0.125, None, 0.75),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "models/gemini-omni-flash-preview": {
        "service_tiers": {"standard": service_tier_entry(1.5, None, 9.0)},
        "notes": ["Text-token rates only; video output rates are documented separately by the provider."],
    },
}

GOOGLE_TEXT_RATE_ALIASES = {
    "models/gemini-3-pro-image-preview": "models/gemini-3-pro-image",
    "models/gemini-3.1-flash-image-preview": "models/gemini-3.1-flash-image",
}

VERTEX_TEXT_RATE_OVERRIDES = {
    "gemini-3-pro-image": {
        "service_tiers": {
            "standard": service_tier_entry(2.0, None, 12.0),
            "flex": service_tier_entry(1.0, None, 6.0),
            "batch": service_tier_entry(1.0, None, 6.0),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "gemini-3.1-flash-image": {
        "service_tiers": {
            "standard": service_tier_entry(0.5, None, 3.0),
            "flex": service_tier_entry(0.25, None, 1.5),
            "batch": service_tier_entry(0.25, None, 1.5),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "gemini-3.1-flash-lite-image": {
        "service_tiers": {
            "standard": service_tier_entry(0.25, None, 1.5),
            "flex": service_tier_entry(0.125, None, 0.75),
            "batch": service_tier_entry(0.125, None, 0.75),
        },
        "notes": ["Text-token rates only; image output rates are documented separately by the provider."],
    },
    "gemini-omni-flash-preview": {
        "service_tiers": {"standard": service_tier_entry(1.5, None, 9.0)},
        "notes": ["Text-token rates only; video output rates are documented separately by the provider."],
    },
}

VERTEX_TEXT_RATE_ALIASES = {
    "gemini-3-pro-image-preview": "gemini-3-pro-image",
    "gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
}

VERTEX_UNSUPPORTED_OVERRIDES = {
    "diffusiongemma": {
        "reason": "No compatible official per-token pricing was found for this model.",
        "sources": [{"label": "Vertex AI Pricing", "url": VERTEX_PRICING_URL}],
    },
    "alphagenome": {
        "reason": (
            "AlphaGenome uses licensing and infrastructure pricing; no compatible official per-token pricing "
            "was found for this model."
        ),
        "sources": [
            {
                "label": "AlphaGenome model page",
                "url": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/open-models/alphagenome",
            }
        ],
    },
    "alphagenome-request": {
        "reason": (
            "AlphaGenome uses licensing and infrastructure pricing; no compatible official per-token pricing "
            "was found for this model."
        ),
        "sources": [
            {
                "label": "AlphaGenome model page",
                "url": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/open-models/alphagenome",
            }
        ],
    },
}


def extract_section(text: str, heading: str) -> Optional[str]:
    idx = text.find(heading)
    if idx == -1:
        return None
    next_candidates = []
    for other_heading in list(GOOGLE_MODEL_LABELS.values()) + list(VERTEX_MODEL_LABELS.values()):
        if other_heading == heading:
            continue
        other_idx = text.find(other_heading, idx + len(heading))
        if other_idx != -1:
            next_candidates.append(other_idx)
    next_idx = min(next_candidates) if next_candidates else len(text)
    return text[idx:next_idx]


def parse_google_gemini_tier_block(block: str) -> Tuple[Optional[Dict[str, Optional[float]]], Optional[Dict[str, Optional[float]]]]:
    def extract_price_series(label: str, next_labels: List[str]) -> List[Optional[float]]:
        label_idx = block.lower().find(label.lower())
        if label_idx == -1:
            return []
        end_candidates = []
        for next_label in next_labels:
            next_idx = block.lower().find(next_label.lower(), label_idx + len(label))
            if next_idx != -1:
                end_candidates.append(next_idx)
        end_idx = min(end_candidates) if end_candidates else len(block)
        segment = block[label_idx:end_idx]
        return [price_number(token) for token in re.findall(r"\$[0-9.]+", segment)[:2]]

    input_prices = extract_price_series("Input price", ["Output price", "Context caching price"])
    output_prices = extract_price_series("Output price", ["Context caching price"])
    cache_prices = extract_price_series("Context caching price", [])
    tier = service_tier_entry(
        input_prices[0] if input_prices else None,
        cache_prices[0] if cache_prices else None,
        output_prices[0] if output_prices else None,
    )
    long_context = None
    if len(input_prices) > 1 or len(output_prices) > 1 or len(cache_prices) > 1:
        long_context = service_tier_entry(
            input_prices[1] if len(input_prices) > 1 else None,
            cache_prices[1] if len(cache_prices) > 1 else None,
            output_prices[1] if len(output_prices) > 1 else None,
        )
    return tier, long_context


def parse_gemini_service_tiers(section_text: str) -> Optional[Dict[str, Any]]:
    tier_markers = list(
        re.finditer(
            r"###\s*(?P<fixture>Standard|Batch|Flex|Priority)\b"
            r"|(?P<live>Standard|Batch|Flex|Priority)\s+Free Tier Paid Tier\b",
            section_text,
            flags=re.IGNORECASE,
        )
    )
    if not tier_markers:
        return None
    service_tiers: Dict[str, Dict[str, Optional[float]]] = {}
    long_context: Optional[Dict[str, Any]] = None
    for index, marker in enumerate(tier_markers):
        tier_name = (marker.group("fixture") or marker.group("live")).lower()
        # A model section can contain an unlisted submodel before the next known
        # heading. Stop when the tier sequence starts again instead of letting
        # the submodel overwrite the requested model's prices.
        if tier_name in service_tiers:
            break
        block_end = tier_markers[index + 1].start() if index + 1 < len(tier_markers) else len(section_text)
        tier, tier_long = parse_google_gemini_tier_block(section_text[marker.end():block_end])
        if tier:
            service_tiers[tier_name] = tier
        if tier_long:
            if not long_context:
                long_context = {"threshold_input_tokens": 200000, "service_tiers": {}}
            long_context["service_tiers"][tier_name] = tier_long
    if "standard" not in service_tiers:
        return None
    return {"service_tiers": service_tiers, "long_context": long_context}


def parse_gemini_standard_and_batch(section_text: str) -> Optional[Dict[str, Any]]:
    """Backward-compatible wrapper for callers using the original parser name."""
    return parse_gemini_service_tiers(section_text)


def build_google_source_entries(fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    final_url, raw_html = fetch_html(GOOGLE_PRICING_URL)
    text = html_to_text(raw_html)
    entries: Dict[str, Dict[str, Any]] = {}
    for model, heading in GOOGLE_MODEL_LABELS.items():
        section = extract_section(text, heading)
        if not section:
            continue
        parsed = parse_gemini_service_tiers(section)
        if not parsed:
            continue
        entries[model] = priced_entry(
            parsed["service_tiers"],
            [{"label": "Gemini Developer API Pricing", "url": final_url}],
            long_context=parsed["long_context"],
        )
    for model, override in GOOGLE_TEXT_RATE_OVERRIDES.items():
        entries[model] = priced_entry(
            override["service_tiers"],
            [{"label": "Gemini Developer API Pricing", "url": final_url}],
            notes=override.get("notes"),
        )
    for alias, canonical in GOOGLE_TEXT_RATE_ALIASES.items():
        if canonical in entries:
            entries[alias] = alias_entry(canonical, "provider_alias")
    for alias, canonical in GOOGLE_EXPLICIT_ALIASES.items():
        if canonical in entries:
            entries[alias] = alias_entry(canonical, "provider_alias")
    return entries


def extract_vertex_model_block(section_text: str, model_label: str) -> Optional[str]:
    idx = section_text.find(model_label)
    if idx == -1:
        return None
    next_candidates = []
    for other_label in sorted(VERTEX_MODEL_LABELS.values(), key=len, reverse=True):
        if other_label == model_label:
            continue
        other_idx = section_text.find(other_label, idx + len(model_label))
        if other_idx != -1:
            next_candidates.append(other_idx)
    end_idx = min(next_candidates) if next_candidates else len(section_text)
    return section_text[idx:end_idx]


def parse_vertex_tier_model_block(block: str, mode: str) -> Tuple[Optional[Dict[str, Optional[float]]], Optional[Dict[str, Optional[float]]]]:
    if mode == "flex":
        input_matches = re.findall(r"Input .*?(\$[0-9.]+)(?:\s+(\$[0-9.]+))?", block, flags=re.IGNORECASE)
        output_matches = re.findall(r"Text output .*?(\$[0-9.]+)(?:\s+(\$[0-9.]+))?", block, flags=re.IGNORECASE)
        if not input_matches or not output_matches:
            return None, None
        tier = service_tier_entry(
            price_number(input_matches[0][0]),
            None,
            price_number(output_matches[0][0]),
        )
        long_context = None
        if input_matches[0][1] or output_matches[0][1]:
            long_context = service_tier_entry(
                price_number(input_matches[0][1]) if input_matches[0][1] else None,
                None,
                price_number(output_matches[0][1]) if output_matches[0][1] else None,
            )
        return tier, long_context
    input_match = re.search(
        r"Input .*?(\$[0-9.]+)\s+(\$[0-9.]+)\s+(\$[0-9.]+)\s+(\$[0-9.]+)",
        block,
        flags=re.IGNORECASE,
    )
    output_match = re.search(
        r"Text output .*?(\$[0-9.]+)\s+(\$[0-9.]+)(?:\s+(?:\$[0-9.]+|N/A|-|\u2014)){0,2}",
        block,
        flags=re.IGNORECASE,
    )
    if not input_match or not output_match:
        return None, None
    tier = service_tier_entry(
        price_number(input_match.group(1)),
        price_number(input_match.group(3)),
        price_number(output_match.group(1)),
    )
    long_context = service_tier_entry(
        price_number(input_match.group(2)),
        price_number(input_match.group(4)),
        price_number(output_match.group(2)),
    )
    return tier, long_context


def build_vertex_source_entries(fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    final_url, raw_html = fetch_html(VERTEX_PRICING_URL)
    text = html_to_text(raw_html)
    standard_idx = text.find("Standard Model Type")
    if standard_idx == -1:
        standard_idx = text.find("### Standard")
    priority_idx = text.find("Priority Model Type")
    if priority_idx == -1:
        priority_idx = text.find("### Priority")
    flex_idx = text.find("Flex/Batch Model Type")
    if flex_idx == -1:
        flex_idx = text.find("### Flex/Batch")
    if min(standard_idx, priority_idx, flex_idx) == -1:
        return {}
    standard_text = text[standard_idx:priority_idx]
    priority_text = text[priority_idx:flex_idx]
    flex_text = text[flex_idx:]
    entries: Dict[str, Dict[str, Any]] = {}
    for model, label in VERTEX_MODEL_LABELS.items():
        service_tiers: Dict[str, Dict[str, Optional[float]]] = {}
        long_context: Optional[Dict[str, Any]] = None
        standard_block = extract_vertex_model_block(standard_text, label)
        priority_block = extract_vertex_model_block(priority_text, label)
        flex_block = extract_vertex_model_block(flex_text, label)
        if standard_block:
            standard_tier, standard_long = parse_vertex_tier_model_block(standard_block, "standard")
            if standard_tier:
                service_tiers["standard"] = standard_tier
            if standard_long:
                long_context = {"threshold_input_tokens": 200000, "service_tiers": {"standard": standard_long}}
        if priority_block:
            priority_tier, priority_long = parse_vertex_tier_model_block(priority_block, "priority")
            if priority_tier:
                service_tiers["priority"] = priority_tier
            if priority_long:
                if not long_context:
                    long_context = {"threshold_input_tokens": 200000, "service_tiers": {}}
                long_context["service_tiers"]["priority"] = priority_long
        if flex_block:
            flex_tier, flex_long = parse_vertex_tier_model_block(flex_block, "flex")
            if flex_tier:
                service_tiers["flex"] = flex_tier
                service_tiers["batch"] = dict(flex_tier)
            if flex_long:
                if not long_context:
                    long_context = {"threshold_input_tokens": 200000, "service_tiers": {}}
                long_context["service_tiers"]["flex"] = flex_long
                long_context["service_tiers"]["batch"] = dict(flex_long)
        if service_tiers:
            entries[model] = priced_entry(
                service_tiers,
                [{"label": "Vertex AI Pricing", "url": final_url}],
                long_context=long_context,
            )
    for model, override in VERTEX_TEXT_RATE_OVERRIDES.items():
        entries[model] = priced_entry(
            override["service_tiers"],
            [{"label": "Vertex AI Pricing", "url": final_url}],
            notes=override.get("notes"),
        )
    for alias, canonical in VERTEX_TEXT_RATE_ALIASES.items():
        if canonical in entries:
            entries[alias] = alias_entry(canonical, "provider_alias")
    for model, override in VERTEX_UNSUPPORTED_OVERRIDES.items():
        entries[model] = unsupported_entry(override["reason"], override.get("sources"))
    return entries


def build_inception_source_entries(fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    final_url, raw_html = fetch_html(INCEPTION_PRICING_URL)
    text = html_to_text(raw_html)
    entries: Dict[str, Dict[str, Any]] = {}
    for model_label, model_key in (("Mercury 2", "mercury-2"), ("Mercury Edit", "mercury-edit")):
        pattern = re.compile(
            rf"{re.escape(model_label)}\s+(\$[0-9.]+)\s+(\$[0-9.]+)\s+(\$[0-9.]+)",
            flags=re.IGNORECASE,
        )
        match = pattern.search(text)
        if not match:
            continue
        entries[model_key] = priced_entry(
            {
                "standard": service_tier_entry(
                    price_number(match.group(1)),
                    price_number(match.group(2)),
                    price_number(match.group(3)),
                )
            },
            [{"label": "Inception Models, Endpoints, and Pricing", "url": final_url}],
        )
    return entries


REQUESTY_UNSUPPORTED_PREFIXES = ("policy/",)


def normalize_requesty_tail(tail: str) -> str:
    lowered = str(tail or "").strip().lower()
    return re.sub(r"[^a-z0-9]+", "", lowered)


def requesty_model_detail_url(model_id: str, provider_links: Dict[str, str]) -> Optional[str]:
    normalized_model_id = str(model_id or "").strip()
    if not normalized_model_id or "/" not in normalized_model_id:
        return None
    provider = normalized_model_id.split("/", 1)[0].strip().lower()
    target_tail = normalize_requesty_tail(normalized_model_id.split("/", 1)[1])
    for href in provider_links.values():
        prefix = f"/models/{provider}/"
        if not (href.startswith(prefix) or href.startswith(f"https://www.requesty.ai{prefix}")):
            continue
        if href.startswith("http"):
            tail = href.split(prefix, 1)[1]
            full_url = href
        else:
            tail = href.split(prefix, 1)[1]
            full_url = f"https://www.requesty.ai{href}"
        if normalize_requesty_tail(tail) == target_tail:
            return full_url
    return None


def parse_requesty_detail_page(page_text: str) -> Optional[Dict[str, Dict[str, Optional[float]]]]:
    match = re.search(
        r"Pricing per 1M tokens\s+Input\s+(\$[0-9.]+)\s+Output\s+(\$[0-9.]+)(?:\s+Cache write\s+\$[0-9.]+)?(?:\s+Cache read\s+(\$[0-9.]+))?",
        page_text,
        flags=re.IGNORECASE,
    )
    if not match:
        return None
    return {
        "standard": service_tier_entry(
            price_number(match.group(1)),
            price_number(match.group(3)),
            price_number(match.group(2)),
        )
    }


def build_requesty_source_entries(models: Iterable[str], fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    model_list = sorted(set(str(item).strip() for item in models if str(item).strip()))
    providers = sorted({item.split("/", 1)[0].strip().lower() for item in model_list if "/" in item})
    provider_links_map: Dict[str, Dict[str, str]] = {}
    entries: Dict[str, Dict[str, Any]] = {}

    for provider in providers:
        if not provider:
            continue
        try:
            _, raw_html = fetch_html(REQUESTY_PROVIDER_URL_TEMPLATE.format(provider=provider))
        except Exception as exc:  # noqa: BLE001
            logging.info("Requesty provider page unavailable for %s: %s", provider, exc)
            provider_links_map[provider] = {}
            continue
        links = {}
        for href in extract_hrefs(raw_html):
            if href.startswith(f"/models/{provider}/") or href.startswith(f"https://www.requesty.ai/models/{provider}/"):
                links[href] = href
        provider_links_map[provider] = links

    for model in model_list:
        provider = model.split("/", 1)[0].strip().lower() if "/" in model else ""
        if not provider or model.startswith(REQUESTY_UNSUPPORTED_PREFIXES):
            continue
        detail_url = requesty_model_detail_url(model, provider_links_map.get(provider, {}))
        if not detail_url:
            continue
        try:
            final_url, raw_html = fetch_html(detail_url)
        except Exception as exc:  # noqa: BLE001
            logging.info("Requesty detail page unavailable for %s: %s", model, exc)
            continue
        parsed = parse_requesty_detail_page(html_to_text(raw_html))
        if not parsed:
            continue
        entries[model] = priced_entry(parsed, [{"label": "Requesty Model Page", "url": final_url}])
    return entries


def entries_equivalent_for_alias(candidate: Dict[str, Any], target: Dict[str, Any]) -> bool:
    if not is_priced_entry(candidate) or not is_priced_entry(target):
        return False
    candidate_tiers = candidate.get("service_tiers") or {}
    target_tiers = target.get("service_tiers") or {}
    for tier_name, candidate_payload in candidate_tiers.items():
        target_payload = target_tiers.get(tier_name)
        if not isinstance(target_payload, dict):
            return False
        if json.dumps(candidate_payload, sort_keys=True) != json.dumps(target_payload, sort_keys=True):
            return False
    return True


def provider_source_entries(provider: str, model_catalog: Dict[str, Any], fetch_html: FetchHtmlFn) -> Dict[str, Dict[str, Any]]:
    normalized = normalize_provider_key(provider)
    models = (model_catalog.get(provider) or {}).get("models") or []
    if normalized == "openai":
        return build_openai_source_entries(models, fetch_html)
    if normalized == "google":
        return build_google_source_entries(fetch_html)
    if normalized == "vertex":
        return build_vertex_source_entries(fetch_html)
    if normalized == "inception":
        return build_inception_source_entries(fetch_html)
    if normalized == "requesty":
        return build_requesty_source_entries(models, fetch_html)
    return {}


def default_unsupported_reason(provider: str) -> str:
    normalized = normalize_provider_key(provider)
    if normalized == "e-infra":
        return "No official compatible per-token pricing source is configured for this provider."
    return "No compatible official per-token pricing was found for this model."


def add_legacy_slug_aliases(models_map: Dict[str, Dict[str, Any]]) -> None:
    canonical_keys = list(models_map.keys())
    for model_key in canonical_keys:
        slug = sanitize_model_identifier(model_key)
        if slug == model_key or slug in models_map:
            continue
        models_map[slug] = alias_entry(model_key, "legacy_slug")


REQUESTY_SHARED_PRICING_ROUTES = (
    ("openai-responses/", "gpt-", (("openai", ""), ("openrouter", "openai/"))),
    ("openai/", "gpt-", (("openai", ""), ("openrouter", "openai/"))),
    ("google/", "gemini-", (("google", "models/"), ("openrouter", "google/"))),
    ("vertex/", "gemini-", (("vertex", ""),)),
)


def resolve_priced_entry(models_map: Dict[str, Dict[str, Any]], model_key: str) -> Optional[Dict[str, Any]]:
    """Resolve a provider-local pricing alias and return a detached priced entry."""
    current_key = str(model_key or "").strip()
    visited = set()
    while current_key and current_key not in visited:
        visited.add(current_key)
        entry = models_map.get(current_key)
        if not isinstance(entry, dict):
            return None
        if is_priced_entry(entry):
            return json.loads(json.dumps(entry))
        current_key = str(entry.get("pricing_ref") or "").strip()
    return None


def copy_missing_service_tiers(
    target_entry: Dict[str, Any],
    source_entry: Dict[str, Any],
    tier_names: Iterable[str],
    note: Optional[str] = None,
) -> None:
    """Copy explicitly sourced tiers without replacing route-specific rates."""
    if not is_priced_entry(target_entry) or not is_priced_entry(source_entry):
        return
    target_tiers = target_entry.setdefault("service_tiers", {})
    source_tiers = source_entry.get("service_tiers") or {}
    copied_tiers = []
    for tier_name in tier_names:
        source_tier = source_tiers.get(tier_name)
        if tier_name in target_tiers or not isinstance(source_tier, dict):
            continue
        target_tiers[tier_name] = dict(source_tier)
        copied_tiers.append(tier_name)
    if not copied_tiers:
        return

    target_long = target_entry.get("long_context")
    source_long = source_entry.get("long_context")
    if isinstance(source_long, dict):
        if not isinstance(target_long, dict):
            target_long = {
                "threshold_input_tokens": source_long.get("threshold_input_tokens"),
                "service_tiers": {},
            }
            target_entry["long_context"] = target_long
        target_long_tiers = target_long.setdefault("service_tiers", {})
        source_long_tiers = source_long.get("service_tiers") or {}
        for tier_name in copied_tiers:
            source_long_tier = source_long_tiers.get(tier_name)
            if tier_name not in target_long_tiers and isinstance(source_long_tier, dict):
                target_long_tiers[tier_name] = dict(source_long_tier)

    target_entry["sources"] = dedupe_sources(
        list(target_entry.get("sources") or []) + list(source_entry.get("sources") or [])
    )
    if note:
        target_entry["notes"] = sorted(set(list(target_entry.get("notes") or []) + [note]))


def populate_documented_flex_prices(providers: Dict[str, Any]) -> None:
    """Fill Flex tiers documented upstream and propagate them to proxy routes."""
    openai_models = ((providers.get("openai") or {}).get("models") or {})
    if isinstance(openai_models, dict):
        for model in OPENAI_FLAGSHIP_MODELS:
            entry = openai_models.get(model)
            if not is_priced_entry(entry):
                continue
            tiers = entry.setdefault("service_tiers", {})
            if "flex" in tiers:
                continue
            batch_tier = tiers.get("batch")
            standard_tier = tiers.get("standard")
            if isinstance(batch_tier, dict):
                flex_tier = dict(batch_tier)
            elif isinstance(standard_tier, dict):
                flex_tier = {
                    key: value * 0.5 if isinstance(value, (int, float)) else None
                    for key, value in standard_tier.items()
                }
            else:
                continue
            tiers["flex"] = flex_tier
            entry["sources"] = dedupe_sources(
                list(entry.get("sources") or [])
                + [{"label": "OpenAI Pricing", "url": OPENAI_PRICING_URL}]
            )

    google_models = ((providers.get("google") or {}).get("models") or {})
    if isinstance(google_models, dict):
        for model in GOOGLE_MODEL_LABELS:
            entry = google_models.get(model)
            if not is_priced_entry(entry):
                continue
            tiers = entry.setdefault("service_tiers", {})
            batch_tier = tiers.get("batch")
            if "flex" not in tiers and isinstance(batch_tier, dict):
                tiers["flex"] = dict(batch_tier)
                entry["sources"] = dedupe_sources(
                    list(entry.get("sources") or [])
                    + [{"label": "Gemini Developer API Pricing", "url": GOOGLE_PRICING_URL}]
                )
            long_context = entry.get("long_context")
            if isinstance(long_context, dict):
                long_tiers = long_context.setdefault("service_tiers", {})
                if "flex" not in long_tiers and isinstance(long_tiers.get("batch"), dict):
                    long_tiers["flex"] = dict(long_tiers["batch"])

    route_sources = (
        ("openrouter", "openai/", "openai", ""),
        ("openrouter", "google/", "google", "models/"),
        ("requesty", "openai/", "openai", ""),
        ("requesty", "openai-responses/", "openai", ""),
        ("requesty", "google/", "google", "models/"),
        ("requesty", "vertex/", "vertex", ""),
    )
    for target_provider, route_prefix, source_provider, source_prefix in route_sources:
        target_models = ((providers.get(target_provider) or {}).get("models") or {})
        source_models = ((providers.get(source_provider) or {}).get("models") or {})
        if not isinstance(target_models, dict) or not isinstance(source_models, dict):
            continue
        for target_key, target_entry in target_models.items():
            if not target_key.startswith(route_prefix) or has_pricing_ref(target_entry):
                continue
            model_tail = target_key[len(route_prefix):]
            source_entry = resolve_priced_entry(source_models, f"{source_prefix}{model_tail}")
            if source_entry is None:
                continue
            copy_missing_service_tiers(
                target_entry,
                source_entry,
                ("flex",),
                note=(
                    f"{target_provider.capitalize()} {route_prefix.rstrip('/')} route uses the "
                    f"upstream {source_provider} Flex token rates."
                ),
            )


def populate_requesty_shared_model_prices(providers: Dict[str, Any]) -> None:
    """Reuse upstream GPT/Gemini rates for matching Requesty direct routes.

    Requesty publishes these routes at the upstream provider's per-token rates.
    Route prefixes remain significant: Azure, Bedrock, Coding API, and other
    Requesty backends are deliberately not inferred from an identically named
    OpenAI or Gemini model.
    """
    requesty_models = ((providers.get("requesty") or {}).get("models") or {})
    if not isinstance(requesty_models, dict):
        return

    for requesty_key, existing_entry in list(requesty_models.items()):
        if is_priced_entry(existing_entry) or has_pricing_ref(existing_entry):
            continue
        for route_prefix, model_family_prefix, source_routes in REQUESTY_SHARED_PRICING_ROUTES:
            if not requesty_key.startswith(route_prefix):
                continue
            model_tail = requesty_key[len(route_prefix):]
            if not model_tail.startswith(model_family_prefix):
                break
            for source_provider, source_prefix in source_routes:
                source_models = ((providers.get(source_provider) or {}).get("models") or {})
                if not isinstance(source_models, dict):
                    continue
                inherited_entry = resolve_priced_entry(source_models, f"{source_prefix}{model_tail}")
                if inherited_entry is None:
                    continue
                notes = list(inherited_entry.get("notes") or [])
                direct_source = route_prefix.startswith(f"{source_provider}/")
                if route_prefix == "openai-responses/" and source_provider == "openai":
                    direct_source = True
                notes.append(
                    f"Requesty {route_prefix.rstrip('/')} route uses the upstream {source_provider} token rates."
                    if direct_source
                    else f"Requesty {route_prefix.rstrip('/')} route uses matching token rates from the {source_provider} catalog."
                )
                inherited_entry["notes"] = notes
                requesty_models[requesty_key] = inherited_entry
                break
            break


def build_pricing_catalog(
    model_catalog: Dict[str, Any],
    selected_providers: Optional[Iterable[str]] = None,
    fetch_html: Optional[FetchHtmlFn] = None,
    updated_at: Optional[str] = None,
) -> Dict[str, Any]:
    fetcher = fetch_html or build_default_fetcher()
    selected = (
        [normalize_provider_key(item) for item in selected_providers]
        if selected_providers
        else [normalize_provider_key(item) for item in model_catalog.keys()]
    )
    providers_out: Dict[str, Any] = {}

    for provider_key in model_catalog.keys():
        normalized_provider = normalize_provider_key(provider_key)
        if normalized_provider not in selected:
            continue
        source_entries = provider_source_entries(provider_key, model_catalog, fetcher)
        metadata_entries = model_metadata_source_entries(provider_key, model_catalog)
        config_models = sorted(set((model_catalog.get(provider_key) or {}).get("models") or []))
        provider_models: Dict[str, Dict[str, Any]] = {}
        for model in config_models:
            if normalized_provider == "openai":
                snapshot_base = openai_snapshot_base(model)
                if snapshot_base and snapshot_base in source_entries:
                    exact_snapshot_entry = source_entries.get(model)
                    if exact_snapshot_entry is None or entries_equivalent_for_alias(exact_snapshot_entry, source_entries[snapshot_base]):
                        provider_models[model] = alias_entry(snapshot_base, "snapshot")
                        continue

            if normalized_provider == "google" and model in GOOGLE_EXPLICIT_ALIASES:
                canonical = GOOGLE_EXPLICIT_ALIASES[model]
                if canonical in source_entries:
                    provider_models[model] = alias_entry(canonical, "provider_alias")
                    continue

            if model in source_entries and is_priced_entry(source_entries[model]):
                provider_models[model] = source_entries[model]
                continue

            if model in metadata_entries and is_priced_entry(metadata_entries[model]):
                provider_models[model] = metadata_entries[model]
                continue

            provider_models[model] = source_entries.get(model) or unsupported_entry(default_unsupported_reason(provider_key))

        add_legacy_slug_aliases(provider_models)
        providers_out[provider_key] = {"models": provider_models}

    populate_documented_flex_prices(providers_out)
    populate_requesty_shared_model_prices(providers_out)
    populate_documented_flex_prices(providers_out)

    return {
        "updated_at": updated_at or utc_timestamp(),
        "providers": providers_out,
    }


def update_model_prices(
    models_path: str,
    output_path: str,
    providers: Optional[Iterable[str]] = None,
    fetch_html: Optional[FetchHtmlFn] = None,
) -> Dict[str, Any]:
    model_catalog = load_model_catalog_js(models_path)
    pricing_catalog = build_pricing_catalog(
        model_catalog=model_catalog,
        selected_providers=providers,
        fetch_html=fetch_html,
    )
    write_pricing_catalog_js(pricing_catalog, output_path)
    return pricing_catalog


def sync_pricing_catalog_with_model_catalog(
    model_catalog: Dict[str, Any],
    pricing_catalog: Optional[Dict[str, Any]] = None,
    selected_providers: Optional[Iterable[str]] = None,
) -> Tuple[Dict[str, Any], List[Tuple[str, str]]]:
    selected = (
        {normalize_provider_key(item) for item in selected_providers}
        if selected_providers
        else {normalize_provider_key(item) for item in model_catalog.keys()}
    )
    base_catalog = strip_status_fields(pricing_catalog if isinstance(pricing_catalog, dict) else {})
    providers_out = json.loads(json.dumps(base_catalog.get("providers") or {}))
    added_models: List[Tuple[str, str]] = []

    for provider_key in model_catalog.keys():
        normalized_provider = normalize_provider_key(provider_key)
        if normalized_provider not in selected:
            continue
        provider_entry = providers_out.setdefault(provider_key, {})
        models_map = provider_entry.setdefault("models", {})
        config_models = sorted(set((model_catalog.get(provider_key) or {}).get("models") or []))
        metadata_entries = model_metadata_source_entries(provider_key, model_catalog)
        for model in config_models:
            metadata_entry = metadata_entries.get(model)
            existing_entry = models_map.get(model)
            if (
                metadata_entry is not None
                and is_priced_entry(metadata_entry)
                and not is_priced_entry(existing_entry)
            ):
                models_map[model] = metadata_entry
                continue
            if model in models_map:
                continue
            models_map[model] = unpriced_entry(
                "Added by --update-models; fill in pricing manually in config_prices.js."
            )
            added_models.append((provider_key, model))
        add_legacy_slug_aliases(models_map)

    populate_requesty_shared_model_prices(providers_out)

    return {
        "updated_at": utc_timestamp(),
        "providers": providers_out,
    }, added_models


def sync_missing_price_entries(
    models_path: str,
    prices_path: str,
    providers: Optional[Iterable[str]] = None,
) -> Tuple[Dict[str, Any], List[Tuple[str, str]]]:
    model_catalog = load_model_catalog_js(models_path)
    existing_catalog: Optional[Dict[str, Any]] = None
    if os.path.exists(prices_path):
        existing_catalog = load_pricing_catalog_js(prices_path)
    synced_catalog, added_models = sync_pricing_catalog_with_model_catalog(
        model_catalog=model_catalog,
        pricing_catalog=existing_catalog,
        selected_providers=providers,
    )
    if (
        added_models
        or not os.path.exists(prices_path)
        or strip_status_fields(existing_catalog or {}) != strip_status_fields(synced_catalog)
    ):
        write_pricing_catalog_js(synced_catalog, prices_path)
    return synced_catalog, added_models


def build_arg_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Generate config_prices.js from official provider pricing pages.")
    parser.add_argument("--models", default="config_models.js", help="Path to config_models.js.")
    parser.add_argument("--output", default="config_prices.js", help="Output path for generated pricing catalog JS.")
    parser.add_argument(
        "--providers",
        nargs="*",
        default=None,
        help="Optional provider slugs to regenerate.",
    )
    return parser


def main(argv: Optional[List[str]] = None) -> int:
    parser = build_arg_parser()
    args = parser.parse_args(argv)
    update_model_prices(args.models, args.output, providers=args.providers)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
