from __future__ import annotations

import math
from typing import Any

import numpy as np

KAPPA_MIN = 0.01
KAPPA_MAX = 1.0
KAPPA_STEPS = 40
MC_RUNS = 2000


def canonical_kappa_grid() -> np.ndarray:
    return np.geomspace(KAPPA_MIN, KAPPA_MAX, KAPPA_STEPS)


def median(values: list[float] | np.ndarray) -> float:
    arr = np.sort(np.asarray(values, dtype=float))
    if len(arr) == 0:
        return 0.0
    m = len(arr) // 2
    return float(arr[m]) if len(arr) % 2 else float((arr[m - 1] + arr[m]) / 2)


def vulnerability(values: list[float] | np.ndarray, epsilon: float) -> tuple[int, int]:
    gaps = np.diff(np.asarray(values, dtype=float))
    runs: list[int] = []
    run = 0
    for gap in gaps:
        if gap <= 2 * epsilon:
            run += 1
        else:
            if run > 0:
                runs.append(run)
            run = 0
    if run > 0:
        runs.append(run)
    nu = sum(math.ceil(r / 2) for r in runs)
    vuln_count = sum(runs)
    return int(nu), int(vuln_count)


def validate_profile_payload(payload: dict[str, Any], expected: dict[str, Any]) -> list[str]:
    errors: list[str] = []
    if payload.get("engine") != "STRUC-I" or payload.get("engine_version") != "1.0.4":
        errors.append("engine identity mismatch")
    settings = payload.get("settings") or {}
    for key, value in {
        "kappa_min": KAPPA_MIN, "kappa_max": KAPPA_MAX,
        "kappa_steps": KAPPA_STEPS, "mc_runs": MC_RUNS,
    }.items():
        if settings.get(key) != value:
            errors.append(f"setting mismatch: {key}")
    if payload.get("item_id") != expected["item_id"]:
        errors.append("item_id mismatch")
    if payload.get("ladder_name") != expected["ladder_name"]:
        errors.append("ladder_name mismatch")
    if payload.get("input_sha256") != expected["input_sha256"]:
        errors.append("input_sha256 mismatch")
    profile = payload.get("profile")
    if not isinstance(profile, list) or len(profile) != KAPPA_STEPS:
        errors.append("profile must contain exactly 40 points")
        return errors
    values = np.asarray(expected["values"], dtype=float)
    gaps = np.diff(values)
    med_gap = median(gaps[gaps > 0]) or median(gaps) or 1.0
    grid = canonical_kappa_grid()
    for idx, point in enumerate(profile):
        context = f"point {idx}"
        try:
            kappa = float(point["kappa"])
            epsilon = float(point["epsilon"])
            nu = int(point["nu"])
            vuln_count = int(point["vulnCount"])
            inv = float(point["inv"])
            rho = float(point["rho"])
            ak = float(point["Ak"])
        except Exception:
            errors.append(f"{context}: missing or non-numeric field")
            continue
        if not np.isfinite([kappa, epsilon, inv, rho, ak]).all():
            errors.append(f"{context}: non-finite coordinate")
            continue
        if not math.isclose(kappa, float(grid[idx]), rel_tol=1e-12, abs_tol=1e-14):
            errors.append(f"{context}: kappa grid mismatch")
        expected_epsilon = kappa * med_gap
        if not math.isclose(epsilon, expected_epsilon, rel_tol=1e-10, abs_tol=1e-14):
            errors.append(f"{context}: epsilon mismatch")
        expected_nu, expected_vuln = vulnerability(values, expected_epsilon)
        if nu != expected_nu:
            errors.append(f"{context}: nu mismatch")
        if vuln_count != expected_vuln:
            errors.append(f"{context}: vulnerable count mismatch")
        if not (0.0 <= ak <= 1.0):
            errors.append(f"{context}: Ak outside [0,1]")
        # The canonical engine records Ak as admissible / 2000.
        if not math.isclose(ak * MC_RUNS, round(ak * MC_RUNS), rel_tol=0, abs_tol=1e-8):
            errors.append(f"{context}: Ak is not a 1/2000 increment")
        expected_rho = 0.0 if nu == 0 else inv / nu
        # inv is exported after four-decimal rounding, while rho is computed from the
        # unrounded mean inversion response and then rounded to six decimals.
        tolerance = 5e-7 if nu == 0 else (0.00005 / max(nu, 1) + 5e-7)
        if not math.isclose(rho, expected_rho, rel_tol=0, abs_tol=tolerance):
            errors.append(f"{context}: rho is inconsistent with inv/nu within export rounding")
    return errors
