from __future__ import annotations

import csv
import hashlib
import json
import os
import shutil
import threading
import uuid
import zipfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from .config import CHAMBER_VERSION, active_roots, ensure_runtime_dirs, load_config, sha256_path
from .corpus import Realization, build_realizations
from .validation import validate_profile_payload
from .evidence import gate_b_evidence_report

_LOCK = threading.RLock()


def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def atomic_json(path: Path, data: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temp = path.with_suffix(path.suffix + ".tmp")
    try:
        temp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        os.replace(temp, path)
    except OSError as exc:
        raise RuntimeError(
            f"Checkpoint storage failure at {path}: {type(exc).__name__}: {exc}"
        ) from exc


def _run_root() -> Path:
    ensure_runtime_dirs()
    return Path(active_roots()["run_root"])


def list_runs() -> list[dict[str, Any]]:
    rows = []
    for state_path in sorted(_run_root().glob("*/run_state.json"), reverse=True):
        try:
            state = json.loads(state_path.read_text(encoding="utf-8"))
            state.pop("items", None)
            rows.append(state)
        except Exception:
            continue
    return rows


def get_run(run_id: str, include_items: bool = False) -> dict[str, Any]:
    path = _run_root() / run_id / "run_state.json"
    if not path.is_file():
        raise FileNotFoundError(run_id)
    state = json.loads(path.read_text(encoding="utf-8"))
    if not include_items:
        state.pop("items", None)
    return state


def _authorization(mode: str, execution_scope: str, override_phrase: str) -> tuple[bool, str]:
    config = load_config()
    auth = config["gate_authorization"]
    mode = mode.upper()
    if execution_scope != "REGISTERED_EXECUTION":
        return False, "Gate C v0.2.2 accepts REGISTERED_EXECUTION only"
    if mode not in auth["authorized_modes"]:
        return False, f"{mode} is not one of the four authorized GATE_C primary branches"
    evidence = gate_b_evidence_report()
    if not evidence.get("gate_b_pass"):
        return False, "Gate B pass evidence is missing or does not match the preserved R1-R5 sources"
    return True, "REGISTERED_EXECUTION"

def create_run(mode: str, repeat_id: str = "", execution_scope: str = "REGISTERED_EXECUTION", override_phrase: str = "") -> dict[str, Any]:
    with _LOCK:
        mode = mode.upper()
        allowed, effective_scope = _authorization(mode, execution_scope, override_phrase)
        if not allowed:
            raise PermissionError(effective_scope)
        repeat_id = ""
        realizations = build_realizations(mode)
        # Each registered Gate C primary branch has one authoritative run identity.
        for old in list_runs():
            if old.get("mode") == mode and old.get("state") not in {"FAILED", "CANCELLED"}:
                return get_run(old["run_id"])
        stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        suffix = mode.lower()
        run_id = f"{suffix}_{stamp}_{uuid.uuid4().hex[:6]}"
        run_dir = _run_root() / run_id
        checkpoint_dir = run_dir / "checkpoints"
        checkpoint_dir.mkdir(parents=True, exist_ok=True)
        items = [r.to_public_dict(include_values=True) for r in realizations]
        state = {
            "run_id": run_id,
            "chamber": "GAL-FINGERPRINT-I",
            "chamber_version": CHAMBER_VERSION,
            "protocol_version": "0.1",
            "mode": mode,
            "repeat_id": repeat_id,
            "execution_scope": effective_scope,
            "state": "READY",
            "created_at": utc_now(),
            "updated_at": utc_now(),
            "completed_at": None,
            "total": len(items),
            "completed": 0,
            "failed": 0,
            "active_item_id": None,
            "message": "Ready to execute in browser",
            "settings": load_config()["engine"],
            "items": items,
            "export_paths": {},
            "runtime_storage_mode": active_roots()["runtime_storage_mode"],
            "runtime_root": str(Path(active_roots()["run_root"]).resolve()),
            "canonical_export_root": str(Path(active_roots()["export_root"]).resolve()),
        }
        atomic_json(run_dir / "run_state.json", state)
        _write_manifest_csv(run_dir / "run_manifest.csv", items)
        return get_run(run_id)


def _write_manifest_csv(path: Path, items: list[dict[str, Any]]) -> None:
    columns = [
        "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",
    ]
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=columns)
        writer.writeheader()
        for item in items:
            writer.writerow({key: item.get(key, "") for key in columns})


def _checkpoint_path(run_id: str, item_id: str) -> Path:
    token = hashlib.sha256(item_id.encode("utf-8")).hexdigest()[:20]
    return _run_root() / run_id / "checkpoints" / f"{token}.json"


def next_item(run_id: str) -> dict[str, Any] | None:
    with _LOCK:
        state = get_run(run_id, include_items=True)
        if state["state"] in {"COMPLETE", "CANCELLED", "FAILED"}:
            return None
        pending = [item for item in state["items"] if not _checkpoint_path(run_id, item["item_id"]).is_file()]
        if not pending:
            state["state"] = "AWAITING_FINALIZATION"
            state["active_item_id"] = None
            state["message"] = "All profiles checkpointed"
            state["updated_at"] = utc_now()
            atomic_json(_run_root() / run_id / "run_state.json", state)
            return None
        item = pending[0]
        state["state"] = "RUNNING"
        state["active_item_id"] = item["item_id"]
        state["message"] = f"Running {item['ladder_name']}"
        state["updated_at"] = utc_now()
        atomic_json(_run_root() / run_id / "run_state.json", state)
        return item


def save_profile(run_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    with _LOCK:
        state = get_run(run_id, include_items=True)
        expected_map = {item["item_id"]: item for item in state["items"]}
        item_id = str(payload.get("item_id", ""))
        if item_id not in expected_map:
            raise ValueError("Unknown item_id")
        expected = expected_map[item_id]
        errors = validate_profile_payload(payload, expected)
        if errors:
            failure = {
                "item_id": item_id, "received_at": utc_now(), "errors": errors,
                "payload_summary": {k: payload.get(k) for k in ["engine", "engine_version", "ladder_name", "input_sha256"]},
            }
            atomic_json(_run_root() / run_id / "failures" / f"{hashlib.sha256(item_id.encode()).hexdigest()[:20]}.json", failure)
            state["failed"] = int(state.get("failed", 0)) + 1
            state["message"] = f"Rejected {expected['ladder_name']}: {errors[0]}"
            state["updated_at"] = utc_now()
            atomic_json(_run_root() / run_id / "run_state.json", state)
            raise ValueError("; ".join(errors[:8]))
        checkpoint = {
            "run_id": run_id,
            "received_at": utc_now(),
            "item": expected,
            "measurement": payload,
        }
        path = _checkpoint_path(run_id, item_id)
        if path.is_file():
            old = json.loads(path.read_text(encoding="utf-8"))
            if old["measurement"]["profile"] != payload["profile"]:
                raise ValueError("Checkpoint exists with different profile content")
        else:
            atomic_json(path, checkpoint)
        completed = sum(_checkpoint_path(run_id, item["item_id"]).is_file() for item in state["items"])
        state["completed"] = completed
        state["active_item_id"] = None
        state["state"] = "AWAITING_FINALIZATION" if completed == state["total"] else "RUNNING"
        state["message"] = f"Checkpointed {expected['ladder_name']} ({completed}/{state['total']})"
        state["updated_at"] = utc_now()
        atomic_json(_run_root() / run_id / "run_state.json", state)
        return {"accepted": True, "completed": completed, "total": state["total"]}


def cancel_run(run_id: str) -> dict[str, Any]:
    with _LOCK:
        state = get_run(run_id, include_items=True)
        state["state"] = "CANCELLED"
        state["message"] = "Cancelled by user; checkpoints retained"
        state["updated_at"] = utc_now()
        atomic_json(_run_root() / run_id / "run_state.json", state)
        state.pop("items", None)
        return state


def _export_directory(state: dict[str, Any]) -> Path:
    root = Path(active_roots()["export_root"])
    if state["mode"] == "REPEAT":
        return root / f"repeat_{int(state['repeat_id'][1:]):02d}"
    mapping = {
        "LEAVE_ONE_RADIUS_OUT": Path("deletion"),
        "THINNING_N10_DETERMINISTIC": Path("thinning/deterministic"),
        "THINNING_N10_RANDOM": Path("thinning/random_stratified"),
        "COVERAGE": Path("coverage"),
        "ML_SENSITIVITY": Path("ml_sensitivity"),
        "NULL_GAP_ORDER": Path("null_gap_order"),
        "NULL_UNIFORM_RANGE": Path("null_uniform_range"),
    }
    return root / "gal_fingerprint_i" / mapping[state["mode"]]


def finalize_run(run_id: str) -> dict[str, Any]:
    from .exports import build_exports
    with _LOCK:
        state = get_run(run_id, include_items=True)
        completed = sum(_checkpoint_path(run_id, item["item_id"]).is_file() for item in state["items"])
        state["completed"] = completed
        if completed != state["total"]:
            raise ValueError(f"Cannot finalize incomplete run: {completed}/{state['total']}")
        export_dir = _export_directory(state)
        try:
            result = build_exports(run_id, state, _run_root() / run_id, export_dir)
        except Exception as exc:
            state["state"] = "AWAITING_FINALIZATION"
            state["active_item_id"] = None
            state["updated_at"] = utc_now()
            state["message"] = f"Finalization failed; all {completed} checkpoints retained: {type(exc).__name__}: {exc}"
            atomic_json(_run_root() / run_id / "run_state.json", state)
            raise RuntimeError(state["message"]) from exc
        state["state"] = "COMPLETE"
        state["completed_at"] = utc_now()
        state["updated_at"] = utc_now()
        state["message"] = "Complete; compact direct chamber run package written"
        state["export_paths"] = result
        state["finalizer_version"] = CHAMBER_VERSION
        atomic_json(_run_root() / run_id / "run_state.json", state)
        public = dict(state)
        public.pop("items", None)
        return public


def run_package_path(run_id: str) -> Path:
    state = get_run(run_id)
    path = state.get("export_paths", {}).get("run_package")
    if not path:
        raise FileNotFoundError("Run package not available")
    return Path(path)
