from __future__ import annotations

import csv
import hashlib
import json
import shutil
import statistics
import zipfile
from pathlib import Path
from typing import Any

from .config import CHAMBER_VERSION, load_config, sha256_path
from .run_store import atomic_json, utc_now


MODE_LABELS = {
    "LEAVE_ONE_RADIUS_OUT": "deletion",
    "THINNING_N10_DETERMINISTIC": "thinning_n10_deterministic",
    "THINNING_N10_RANDOM": "thinning_n10_random",
    "COVERAGE": "coverage",
    "ML_SENSITIVITY": "ml_sensitivity",
    "NULL_GAP_ORDER": "null_gap_order",
    "NULL_UNIFORM_RANGE": "null_uniform_range",
}

BRANCH_CODES = {
    "LEAVE_ONE_RADIUS_OUT": "L",
    "THINNING_N10_DETERMINISTIC": "T",
    "THINNING_N10_RANDOM": "TR",
    "COVERAGE": "C",
    "ML_SENSITIVITY": "M",
    "NULL_GAP_ORDER": "NG",
    "NULL_UNIFORM_RANGE": "NU",
    "REPEAT": "R",
}


def _summary(profile: list[dict[str, Any]]) -> dict[str, Any]:
    aks = [float(p["Ak"]) for p in profile]
    rhos = [float(p["rho"]) for p in profile]
    mean_ak = statistics.fmean(aks)
    min_ak = min(aks)
    mean_rho = statistics.fmean(rhos)
    max_rho = max(rhos)
    if mean_ak >= 0.93:
        regime = "Geometric Persistence"
        if mean_ak >= 0.975 and min_ak >= 0.93 and mean_rho < 0.30:
            state = "Stable Structure"
        elif mean_ak >= 0.975 and min_ak >= 0.93 and mean_rho >= 0.60:
            state = "Boundary-Stabilized"
        else:
            state = "Weak Persistence"
    elif mean_ak >= 0.60:
        regime = "Structural Boundary"
        high = next((float(p["Ak"]) for p in profile if float(p["kappa"]) >= 0.7), aks[-1])
        state = "Transitional Structure" if high >= 0.70 else "Near-Critical Structure"
    else:
        regime = "Structural Instability"
        low = next((float(p["Ak"]) for p in profile if float(p["kappa"]) >= 0.25), aks[len(aks)//3])
        state = "Weak Structure" if low < 0.50 else "Random Structure"
    return {
        "regime": regime,
        "state": state,
        "mean_Ak": round(mean_ak, 6),
        "min_Ak": round(min_ak, 6),
        "Ak_at_kappa_max": aks[-1],
        "mean_rho": round(mean_rho, 6),
        "max_rho": round(max_rho, 6),
        "rho_at_kappa_max": rhos[-1],
    }


def _label(state: dict[str, Any]) -> str:
    if state.get("repeat_id"):
        return state["repeat_id"].lower()
    return MODE_LABELS[state["mode"]]


def _compact_package_name(run_id: str, state: dict[str, Any]) -> str:
    """Keep the canonical Windows path below MAX_PATH even in deeply nested projects."""
    code = BRANCH_CODES.get(state.get("mode", ""), "X")
    token = run_id.rsplit("_", 1)[-1][:8]
    return f"GFI_{code}_{token}_RUN_PACKAGE.zip"


def build_exports(run_id: str, state: dict[str, Any], run_dir: Path, export_dir: Path) -> dict[str, Any]:
    measurements: list[tuple[dict[str, Any], dict[str, Any]]] = []
    for item in state["items"]:
        token = hashlib.sha256(item["item_id"].encode("utf-8")).hexdigest()[:20]
        checkpoint_path = run_dir / "checkpoints" / f"{token}.json"
        checkpoint = json.loads(checkpoint_path.read_text(encoding="utf-8"))
        measurements.append((item, checkpoint["measurement"]))

    # Build every loose artifact in the short runtime tree. Only the complete ZIP is
    # copied into the canonical project export folder. This avoids Windows MAX_PATH
    # failures while preserving a self-contained direct chamber output package.
    staging_dir = run_dir / "finalization_staging"
    if staging_dir.exists():
        shutil.rmtree(staging_dir)
    staging_dir.mkdir(parents=True, exist_ok=True)

    label = _label(state)
    stem = f"chamber_struc_i_v1_0_4_{label}"
    json_path = staging_dir / f"{stem}_results.json"
    csv_path = staging_dir / f"{stem}_profiles.csv"
    manifest_path = staging_dir / f"{stem}_manifest.json"
    record_path = staging_dir / f"{stem}_result_record.md"
    run_manifest_copy = staging_dir / f"{stem}_run_manifest.csv"
    recovery_path = staging_dir / "FINALIZATION_PROVENANCE.md"

    ladders = []
    profiles = []
    for item, measurement in measurements:
        profile = measurement["profile"]
        ladders.append({
            "name": item["ladder_name"],
            "domain": "galaxy-baryonic-profile",
            "n": item["n_realization"],
            "subsampled": item["n_realization"] != item["n_source"],
            **_summary(profile),
        })
        profiles.append({
            "name": item["ladder_name"],
            "curve": [
                {
                    "kappa": float(pt["kappa"]),
                    "nu": int(pt["nu"]),
                    "vulnCount": int(pt["vulnCount"]),
                    "inv": float(pt["inv"]),
                    "rho": float(pt["rho"]),
                    "Ak": float(pt["Ak"]),
                }
                for pt in profile
            ],
        })

    config_provenance = load_config()["controller_provenance"]
    execution_controller_version = str(state.get("chamber_version") or CHAMBER_VERSION)
    realization_metadata = [
        {key: item.get(key) for key in [
            "item_id", "mode", "branch", "realization_id", "repeat_id", "galaxy_id",
            "source_galaxy_id", "ladder_name", "input_sha256", "n_source", "n_realization",
            "generation_rule", "generation_seed", "radial_coverage_min", "radial_coverage_max",
            "upsilon_disk", "upsilon_bulge", "variant_details",
        ]}
        for item, _ in measurements
    ]
    payload = {
        "chamber": "STRUC-I",
        "version": "1.0.4",
        "program": "UNNS Substrate Program",
        "timestamp": utc_now(),
        "dataset": {
            "family": "GAL-FINGERPRINT-I",
            "class": label.upper(),
            "files": [f"registered://{item['ladder_name']}" for item, _ in measurements],
        },
        "inequality": "inv(P_ε; L) ≤ ν(V_ε(L))",
        "perturbation": "δᵢ ~ Uniform[-ε, ε]",
        "scale": "ε = κ · median(gaps)",
        "kappa_range": [0.01, 1.0],
        "kappa_steps": 40,
        "mc_runs": 2000,
        "ladders": ladders,
        "profiles": profiles,
        "gal_fingerprint_i": {
            "controller": config_provenance["controller"],
            "controller_version": execution_controller_version,
            "runtime_package_version": execution_controller_version,
            "finalizer_version": CHAMBER_VERSION,
            "measurement_kernel": config_provenance["measurement_kernel"],
            "measurement_kernel_version": config_provenance["measurement_kernel_version"],
            "protocol_version": "0.1",
            "gate": "GATE_C",
            "run_id": run_id,
            "mode": state["mode"],
            "repeat_id": state.get("repeat_id", ""),
            "execution_scope": state["execution_scope"],
            "randomness": "NATIVE_UNSEEDED_BROWSER_RNG",
            "profile_count": len(profiles),
            "checkpointed_profile_count": state["completed"],
            "realizations": realization_metadata,
        },
    }
    json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")

    with csv_path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow(["ladder", "domain", "kappa", "epsilon", "nu_V", "vuln_count", "inv", "rho", "Ak"])
        for item, measurement in measurements:
            for pt in measurement["profile"]:
                writer.writerow([
                    item["ladder_name"], "galaxy-baryonic-profile",
                    f"{float(pt['kappa']):.6f}", f"{float(pt['epsilon']):.8f}",
                    int(pt["nu"]), int(pt["vulnCount"]), f"{float(pt['inv']):.4f}",
                    f"{float(pt['rho']):.6f}", f"{float(pt['Ak']):.6f}",
                ])

    source_run_manifest = run_dir / "run_manifest.csv"
    run_manifest_copy.write_bytes(source_run_manifest.read_bytes())
    manifest = {
        "run_id": run_id,
        "created_at": state["created_at"],
        "completed_at": utc_now(),
        "execution_controller_version": execution_controller_version,
        "finalizer_version": CHAMBER_VERSION,
        "measurement_kernel": "STRUC-I v1.0.4",
        "gate": "GATE_C",
        "mode": state["mode"],
        "repeat_id": state.get("repeat_id", ""),
        "execution_scope": state["execution_scope"],
        "input_count": len(state["items"]),
        "outputs": {},
        "inputs": realization_metadata,
    }
    for path in [json_path, csv_path, run_manifest_copy]:
        manifest["outputs"][path.name] = {
            "sha256": sha256_path(path), "size_bytes": path.stat().st_size,
        }
    atomic_json(manifest_path, manifest)

    record = f"""# GAL-FINGERPRINT-I Gate C chamber run record

```text
run_id                         {run_id}
mode                           {state['mode']}
execution_scope                {state['execution_scope']}
profiles                       {len(profiles)}
kappa_points                   {len(profiles) * 40}
execution controller version   {execution_controller_version}
finalizer version              {CHAMBER_VERSION}
measurement kernel             STRUC-I v1.0.4
Monte Carlo runs               2000 per κ
randomness                     NATIVE_UNSEEDED_BROWSER_RNG
gate                           GATE_C
```

The STRUC-I measurement kernel was executed in browser JavaScript. Python provided the registered Gate C queue, validation, checkpointing, provenance, manifests, and export assembly.
"""
    record_path.write_text(record, encoding="utf-8")

    recovery_path.write_text(
        "# Finalization provenance\n\n"
        f"- Measurement execution controller: `{execution_controller_version}`\n"
        f"- Export finalizer: `{CHAMBER_VERSION}`\n"
        "- All 40-point profiles were read from the preserved per-realization checkpoints.\n"
        "- Loose artifacts were assembled under the short runtime path.\n"
        "- Only the complete ZIP package was copied to the canonical direct-export folder.\n",
        encoding="utf-8",
    )

    staged_package = staging_dir / f"GAL_FINGERPRINT_I_GATE_C_{label.upper()}_{run_id}_RUN_PACKAGE.zip"
    members = [json_path, csv_path, manifest_path, record_path, run_manifest_copy, recovery_path]
    with zipfile.ZipFile(staged_package, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        for path in members:
            archive.write(path, arcname=path.name)

    export_dir.mkdir(parents=True, exist_ok=True)
    canonical_package = export_dir / _compact_package_name(run_id, state)
    shutil.copy2(staged_package, canonical_package)

    return {
        "run_package": str(canonical_package.resolve()),
        "run_package_sha256": sha256_path(canonical_package),
        "run_package_size_bytes": canonical_package.stat().st_size,
        "package_member_count": len(members),
        "package_members": [path.name for path in members],
        "staging_package": str(staged_package.resolve()),
        "execution_controller_version": execution_controller_version,
        "finalizer_version": CHAMBER_VERSION,
    }
