#!/usr/bin/env python3
"""Independent O3 REST check — no repo clone, no mpmath.

Download this file next to math-o3-tables.json (or a capability reproduce.json)
and compare the public REST result to the tabulated expected_f64 within ≤2 ULP.

  python3 check-math-o3-rest.py tan
  python3 check-math-o3-rest.py --reproduce reproduce.json
  python3 check-math-o3-rest.py tan --url https://www.calculatorx.com/api/v1/calc/tan
"""
from __future__ import annotations

import argparse
import json
import math
import struct
import sys
import urllib.error
import urllib.request
from pathlib import Path

SITE = "https://www.calculatorx.com"
THRESHOLD_ULP = 2
FAMILY_TOOL = {
    "tan": "tan",
    "sin": "sin",
    "cos": "cos",
    "atan": "arctan",
    "asin": "arcsin",
    "ln": "ln",
    "log10": "log",
    "sqrt": "square-root",
    "exponent": "exponent",
    "antilog": "antilog",
    "root": "root",
    "exp_growth": "exponential-growth",
    "divide": "division",
}


def ulp_distance(a: float, b: float) -> int:
    if not (math.isfinite(a) and math.isfinite(b)):
        return 2**31
    if a == b:
        return 0

    def bits(x: float) -> int:
        return struct.unpack("<Q", struct.pack("<d", x))[0]

    def order(u: int) -> int:
        return u if (u >> 63) == 0 else (~u) & ((1 << 64) - 1)

    return abs(order(bits(a)) - order(bits(b)))


def result_number(payload: dict, family: str):
    r = payload.get("result")
    if isinstance(r, dict):
        if family in {"atan", "asin"} and r.get("deg") is not None:
            return float(r["deg"])
        if family == "sqrt":
            if r.get("principal") is not None:
                return float(r["principal"])
            if r.get("imag") is not None:
                return float(r["imag"])
        if r.get("value") is not None:
            return float(r["value"])
    if isinstance(r, (int, float)):
        return float(r)
    detail = payload.get("result_detail") or {}
    if isinstance(detail, dict) and detail.get("value") is not None:
        v = detail["value"]
        if isinstance(v, dict):
            if family == "sqrt":
                if v.get("principal") is not None:
                    return float(v["principal"])
                if v.get("imag") is not None:
                    return float(v["imag"])
            if v.get("value") is not None:
                return float(v["value"])
        return float(v)
    raise ValueError(f"no numeric result in REST payload keys={list(payload)}")


def load_vectors(table_path: Path, reproduce_path: Path | None, family: str):
    if reproduce_path:
        doc = json.loads(reproduce_path.read_text(encoding="utf-8"))
        fam = doc.get("capability_id") or family
        vectors = []
        for row in doc.get("vectors") or []:
            vectors.append(
                {
                    "id": row.get("id"),
                    "inputs": row.get("inputs") or {},
                    "y_f64": row.get("expected_f64"),
                    "threshold_ulp": row.get("threshold_ulp"),
                }
            )
        return fam, vectors, int(doc.get("result_summary", {}).get("threshold_ulp") or THRESHOLD_ULP)
    tables = json.loads(table_path.read_text(encoding="utf-8"))
    fam = tables.get("families", {}).get(family)
    if not fam:
        raise SystemExit(f"family {family!r} not in {table_path}")
    return family, fam.get("vectors") or [], THRESHOLD_ULP


def post_json(url: str, body: dict) -> dict:
    req = urllib.request.Request(
        url,
        data=json.dumps(body).encode("utf-8"),
        headers={"Content-Type": "application/json", "Accept": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def main() -> int:
    here = Path(__file__).resolve().parent
    parser = argparse.ArgumentParser(description="Compare public REST results to the O3 table.")
    parser.add_argument("family", nargs="?", help="O3 family name, e.g. tan")
    parser.add_argument("--table", type=Path, default=here / "math-o3-tables.json")
    parser.add_argument("--reproduce", type=Path, help="Capability reproduce.json (preferred when present)")
    parser.add_argument("--url", help="Override REST URL")
    parser.add_argument("--threshold", type=int, default=None)
    args = parser.parse_args()
    if not args.family and not args.reproduce:
        parser.error("pass a family (tan) or --reproduce reproduce.json")

    family_key = args.family or "tan"
    family, vectors, default_thr = load_vectors(args.table, args.reproduce, family_key)
    if args.reproduce and not args.family:
        cap = str(json.loads(args.reproduce.read_text(encoding="utf-8")).get("capability_id") or "")
        family = cap.split(".", 1)[-1] if cap.startswith("math.") else family
        if family == "arctan":
            family = "atan"
        if family == "arcsin":
            family = "asin"
        if family == "square_root":
            family = "sqrt"
    global_thr = args.threshold
    tool = FAMILY_TOOL.get(family, family)
    url = args.url or f"{SITE}/api/v1/calc/{tool}"
    if not vectors:
        print("no vectors", file=sys.stderr)
        return 1

    failed = 0
    max_ulp = 0
    max_thr_used = default_thr
    for row in vectors:
        expected = row.get("y_f64")
        row_thr = row.get("threshold_ulp")
        thr = global_thr if global_thr is not None else (int(row_thr) if row_thr is not None else default_thr)
        max_thr_used = max(max_thr_used, thr)
        try:
            payload = post_json(url, row["inputs"])
            if payload.get("status") not in (None, "success", "ok"):
                raise ValueError(payload.get("error") or payload.get("status"))
            if expected is None:
                result = payload.get("result") or {}
                q = result.get("quotient")
                r = result.get("remainder")
                ok = str(q) == str(row.get("quotient")) and str(r) == str(row.get("remainder"))
                if not ok:
                    failed += 1
                print(f"{'PASS' if ok else 'FAIL'} {row.get('id')} exact q,r expected={row.get('quotient')} R {row.get('remainder')} actual={q} R {r}")
                continue
            actual = result_number(payload, family)
        except (urllib.error.URLError, ValueError, KeyError, TypeError) as exc:
            failed += 1
            print(f"FAIL {row.get('id')} REST {exc}")
            continue
        err = ulp_distance(float(expected), actual)
        max_ulp = max(max_ulp, err)
        ok = err <= thr
        if not ok:
            failed += 1
        print(f"{'PASS' if ok else 'FAIL'} {row.get('id')} ulp={err} thr={thr} expected={expected} actual={actual}")
    total = len(vectors)
    print(f"{total - failed}/{total} within per-vector ULP (summary default ≤{default_thr}, max declared ≤{max_thr_used}) · max {max_ulp} ULP · {url}")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())
