#!/usr/bin/env python3
"""Generate website address constants from the canonical deployment inventory.

The website must never carry a second, hand-maintained copy of V2 addresses.
`baseline/deployments.json` is the only source of truth; this script projects it
into a typed TypeScript module that the nested website source imports.

Usage:
    python3 pr9/website/scripts/generate_website_addresses.py           # write
    python3 pr9/website/scripts/generate_website_addresses.py --check   # verify

`--check` regenerates in memory and exits non-zero when the committed file is
stale, so address drift fails CI instead of shipping silently.
"""
from __future__ import annotations

import argparse
import hashlib
import json
import sys
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[3]
SOURCE = ROOT / "baseline" / "deployments.json"
TARGET = ROOT / "pr9" / "website" / "generated" / "website-addresses.generated.ts"

CHAIN_ORDER = ["base", "avalanche", "arbitrum", "polygon"]
# Truth labels are fixed strings shared with src/truth-labels.ts. Any new value
# must be added in both places and to the label tests.
LEGACY_V2 = "Legacy V2"
DEPRECATED = "Deprecated"


def _sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _ts_string(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False)


def _baseline_truth_label(record: dict[str, Any]) -> str:
    """Deployment-derived label only. Runtime flags never widen this."""
    status = record.get("status")
    if status in ("deprecated", "retired"):
        return DEPRECATED
    if record.get("version") == "v2":
        return LEGACY_V2
    # V3 rows are never emitted by this generator (see V3_ADDRESSES below), so a
    # non-v2 row means the canonical file changed shape and must be reviewed.
    raise SystemExit(
        f"unsupported deployment version for {record.get('name')}: {record.get('version')!r}"
    )


def _entry_literal(record: dict[str, Any]) -> str:
    fields = [
        f"    name: {_ts_string(record['name'])},",
        f"    chain: {_ts_string(record['chain'])},",
        f"    chainId: {int(record['chainId'])},",
        f"    address: {_ts_string(record['address'])},",
        f"    category: {_ts_string(record['category'])},",
        f"    protocolVersion: {_ts_string(record['version'])},",
        f"    status: {_ts_string(record['status'])},",
        f"    custodiesFunds: {'true' if record.get('custodiesFunds') else 'false'},",
        f"    explorer: {_ts_string(record['explorer'])},",
        f"    baselineTruthLabel: {_ts_string(_baseline_truth_label(record))},",
    ]
    return "  {\n" + "\n".join(fields) + "\n  },"


def render(source: Path = SOURCE) -> str:
    data = json.loads(source.read_text())
    contracts: list[dict[str, Any]] = data["contracts"]
    digest = _sha256(source)

    chain_meta_lines = []
    for chain in CHAIN_ORDER:
        meta = data["chainMeta"][chain]
        chain_meta_lines.append(
            "  %s: { chainId: %d, explorer: %s, rpc: %s },"
            % (chain, int(meta["chainId"]), _ts_string(meta["explorer"]), _ts_string(meta["rpc"]))
        )

    by_chain: dict[str, list[dict[str, Any]]] = {chain: [] for chain in CHAIN_ORDER}
    for record in contracts:
        chain = record["chain"]
        if chain not in by_chain:
            raise SystemExit(f"unknown chain in canonical inventory: {chain!r}")
        by_chain[chain].append(record)

    lines: list[str] = []
    lines.append("// GENERATED FILE - DO NOT EDIT BY HAND.")
    lines.append("// Source: baseline/deployments.json (canonical V2 deployment inventory).")
    lines.append("// Regenerate: python3 pr9/website/scripts/generate_website_addresses.py")
    lines.append("// Verify:     python3 pr9/website/scripts/generate_website_addresses.py --check")
    lines.append("")
    lines.append(f"export const DEPLOYMENTS_SOURCE = {_ts_string('baseline/deployments.json')};")
    lines.append(f"export const DEPLOYMENTS_SCHEMA_VERSION = {_ts_string(data['schemaVersion'])};")
    lines.append(f"export const DEPLOYMENTS_SHA256 = {_ts_string(digest)};")
    lines.append(f"export const DEPLOYMENTS_GENERATED_AT = {_ts_string(data['generatedAt'])};")
    lines.append("")
    lines.append('export type WebsiteChain = ' + " | ".join(_ts_string(c) for c in CHAIN_ORDER) + ";")
    lines.append("")
    lines.append("export type WebsiteAddressEntry = {")
    lines.append("  readonly name: string;")
    lines.append("  readonly chain: WebsiteChain;")
    lines.append("  readonly chainId: number;")
    lines.append("  readonly address: `0x${string}`;")
    lines.append("  readonly category: string;")
    lines.append('  readonly protocolVersion: "v2" | "v3";')
    lines.append("  readonly status: string;")
    lines.append("  readonly custodiesFunds: boolean;")
    lines.append("  readonly explorer: string;")
    lines.append("  readonly baselineTruthLabel: string;")
    lines.append("};")
    lines.append("")
    lines.append(
        "export const CHAIN_META: Readonly<Record<WebsiteChain, { readonly chainId: number;"
        " readonly explorer: string; readonly rpc: string }>> = {"
    )
    lines.extend(chain_meta_lines)
    lines.append("} as const;")
    lines.append("")
    for chain in CHAIN_ORDER:
        const = f"{chain.upper()}_V2_ADDRESSES"
        lines.append(f"/** {len(by_chain[chain])} canonical V2 deployments on {chain}. */")
        lines.append(f"export const {const}: readonly WebsiteAddressEntry[] = [")
        for record in by_chain[chain]:
            lines.append(_entry_literal(record))
        lines.append("];")
        lines.append("")
    lines.append("export const V2_ADDRESSES: readonly WebsiteAddressEntry[] = [")
    for chain in CHAIN_ORDER:
        lines.append(f"  ...{chain.upper()}_V2_ADDRESSES,")
    lines.append("];")
    lines.append("")
    lines.append("/**")
    lines.append(" * No V3 deployment is recorded in the canonical inventory yet. This stays empty")
    lines.append(" * until verified V3 addresses land in baseline/deployments.json, which is what")
    lines.append(" * makes every V3 money-moving gate fail closed today.")
    lines.append(" */")
    lines.append("export const V3_ADDRESSES: readonly WebsiteAddressEntry[] = [];")
    lines.append("")
    lines.append("function key(name: string, chain: WebsiteChain): string {")
    lines.append("  return `${chain}:${name}`;")
    lines.append("}")
    lines.append("")
    lines.append(
        "export const V2_ADDRESS_INDEX: ReadonlyMap<string, WebsiteAddressEntry> = new Map("
    )
    lines.append("  V2_ADDRESSES.map((entry) => [key(entry.name, entry.chain), entry]),")
    lines.append(");")
    lines.append("")
    lines.append(
        "export const V3_ADDRESS_INDEX: ReadonlyMap<string, WebsiteAddressEntry> = new Map("
    )
    lines.append("  V3_ADDRESSES.map((entry) => [key(entry.name, entry.chain), entry]),")
    lines.append(");")
    lines.append("")
    lines.append("/** Returns null rather than throwing so callers can fail closed explicitly. */")
    lines.append("export function findDeployment(")
    lines.append("  name: string,")
    lines.append("  chain: WebsiteChain,")
    lines.append('  protocolVersion: "v2" | "v3",')
    lines.append("): WebsiteAddressEntry | null {")
    lines.append(
        '  const index = protocolVersion === "v3" ? V3_ADDRESS_INDEX : V2_ADDRESS_INDEX;'
    )
    lines.append("  return index.get(key(name, chain)) ?? null;")
    lines.append("}")
    lines.append("")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--check", action="store_true", help="fail if the committed file is stale")
    args = parser.parse_args()

    rendered = render()
    if args.check:
        current = TARGET.read_text() if TARGET.exists() else ""
        if current != rendered:
            print(
                "website address constants are stale; run "
                "python3 pr9/website/scripts/generate_website_addresses.py",
                file=sys.stderr,
            )
            return 1
        print(f"ok: {TARGET.relative_to(ROOT)} matches {SOURCE.relative_to(ROOT)}")
        return 0

    TARGET.parent.mkdir(parents=True, exist_ok=True)
    TARGET.write_text(rendered)
    print(f"wrote {TARGET.relative_to(ROOT)}")
    return 0


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