from __future__ import annotations

import argparse
import socket
import threading
import webbrowser
from pathlib import Path
from typing import Any

import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from .config import CHAMBER_VERSION, PACKAGE_ROOT, active_roots, ensure_runtime_dirs, load_config, runtime_storage_probe
from .corpus import build_realizations, registered_asset_report
from .evidence import gate_b_evidence_report, write_gate_b_provenance_amendment
from .gate_c import analyze_gate_c, gate_c_readiness
from .run_store import cancel_run, create_run, finalize_run, get_run, list_runs, next_item, run_package_path, save_profile

STATIC = PACKAGE_ROOT / "web" / "static"
app = FastAPI(title="GAL-FINGERPRINT-I", version=CHAMBER_VERSION)
app.mount("/static", StaticFiles(directory=STATIC), name="static")


@app.middleware("http")
async def no_cache_static(request: Request, call_next):
    response = await call_next(request)
    if request.url.path == "/" or request.url.path.startswith("/static/"):
        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
        response.headers["Pragma"] = "no-cache"
    return response


class CreateRunRequest(BaseModel):
    mode: str
    execution_scope: str = "REGISTERED_EXECUTION"


class ProfilePayload(BaseModel):
    run_id: str = ""
    item_id: str
    ladder_name: str
    input_sha256: str
    engine: str
    engine_version: str
    settings: dict[str, Any]
    started_at: str
    completed_at: str
    profile: list[dict[str, Any]]
    classification: dict[str, Any]


@app.get("/", response_class=HTMLResponse)
def index() -> str:
    return (STATIC / "index.html").read_text(encoding="utf-8")


@app.get("/api/status")
def status() -> dict[str, Any]:
    ensure_runtime_dirs()
    config = load_config()
    roots = active_roots()
    return {
        "chamber": config["chamber"],
        "version": config["version"],
        "protocol_version": config["protocol_version"],
        "registered_question": config["registered_question"],
        "engine": config["engine"],
        "controller_provenance": config["controller_provenance"],
        "gate_authorization": config["gate_authorization"],
        "installed_in_project": roots["installed_in_project"],
        "project_root": str(roots["project_root"]),
        "run_root": str(Path(roots["run_root"]).resolve()),
        "export_root": str(Path(roots["export_root"]).resolve()),
        "analysis_root": str(Path(roots["analysis_root"]).resolve()),
        "canonical_run_root": str(Path(roots["canonical_run_root"]).resolve()),
        "runtime_storage_mode": roots["runtime_storage_mode"],
        "gate_b": gate_b_evidence_report(),
        "runs": list_runs(),
    }


@app.post("/api/validate")
def validate_registered() -> dict[str, Any]:
    report = registered_asset_report()
    config = load_config()
    expected_primary = "a2cff5356218021afe284f90926623fbcd6329e7b830b3cdea6c4bc41ec56b64"
    report["primary_hash_frozen"] = report["primary_sha256"] == expected_primary
    report["registered_branch_counts"] = {
        "LEAVE_ONE_RADIUS_OUT": len(build_realizations("LEAVE_ONE_RADIUS_OUT")),
        "THINNING_N10_DETERMINISTIC": len(build_realizations("THINNING_N10_DETERMINISTIC")),
        "COVERAGE": len(build_realizations("COVERAGE")),
        "ML_SENSITIVITY": len(build_realizations("ML_SENSITIVITY")),
    }
    report["locked_sensitivity_counts"] = {
        "THINNING_N10_RANDOM": len(build_realizations("THINNING_N10_RANDOM")),
        "NULL_GAP_ORDER": len(build_realizations("NULL_GAP_ORDER")),
        "NULL_UNIFORM_RANGE": len(build_realizations("NULL_UNIFORM_RANGE")),
    }
    report["authorized_modes"] = config["gate_authorization"]["authorized_modes"]
    report["gate_b_evidence"] = gate_b_evidence_report()
    report["gate_b_provenance_amendment"] = str(write_gate_b_provenance_amendment().resolve())
    report.update(runtime_storage_probe())
    report["controller_version"] = CHAMBER_VERSION
    report["controller_provenance_correct"] = config["controller_provenance"]["controller_version"] == CHAMBER_VERSION
    report["deterministic_random_thinning_split"] = (
        report["registered_branch_counts"]["THINNING_N10_DETERMINISTIC"] == 121
        and report["locked_sensitivity_counts"]["THINNING_N10_RANDOM"] == 363
    )
    report["validation_pass"] = bool(
        report["primary_hash_frozen"]
        and report["primary_count"] == 121
        and not report["missing_ladders"]
        and report["ladder_file_hashes_matching_manifest"] == 121
        and report["runtime_write_test_pass"]
        and report["gate_b_evidence"].get("gate_b_pass")
        and report["controller_provenance_correct"]
        and report["deterministic_random_thinning_split"]
        and report["registered_branch_counts"] == config["gate_c_expected_counts"]
        and report["locked_sensitivity_counts"] == config["locked_sensitivity_counts"]
    )
    return report


@app.get("/api/runs")
def runs() -> list[dict[str, Any]]:
    return list_runs()


@app.post("/api/runs")
def new_run(request: CreateRunRequest) -> dict[str, Any]:
    try:
        return create_run(request.mode, "", request.execution_scope, "")
    except PermissionError as exc:
        raise HTTPException(403, str(exc)) from exc
    except Exception as exc:
        raise HTTPException(400, str(exc)) from exc


@app.get("/api/runs/{run_id}")
def run_status(run_id: str) -> dict[str, Any]:
    try:
        return get_run(run_id)
    except FileNotFoundError as exc:
        raise HTTPException(404, "Unknown run") from exc


@app.post("/api/runs/{run_id}/next")
def run_next(run_id: str) -> dict[str, Any]:
    try:
        item = next_item(run_id)
        return {"item": item, "run": get_run(run_id)}
    except FileNotFoundError as exc:
        raise HTTPException(404, "Unknown run") from exc
    except Exception as exc:
        raise HTTPException(400, str(exc)) from exc


@app.post("/api/runs/{run_id}/profiles")
def profile_result(run_id: str, payload: ProfilePayload) -> dict[str, Any]:
    try:
        if payload.run_id and payload.run_id != run_id:
            raise ValueError(f"Run link mismatch: URL={run_id} payload={payload.run_id}")
        return save_profile(run_id, payload.model_dump())
    except FileNotFoundError as exc:
        raise HTTPException(404, "Unknown run") from exc
    except Exception as exc:
        raise HTTPException(400, str(exc)) from exc


@app.post("/api/runs/{run_id}/finalize")
def run_finalize(run_id: str) -> dict[str, Any]:
    try:
        get_run(run_id)
    except FileNotFoundError as exc:
        raise HTTPException(404, "Unknown run") from exc
    try:
        return finalize_run(run_id)
    except Exception as exc:
        raise HTTPException(400, f"Finalization failed with checkpoints retained: {exc}") from exc


@app.post("/api/runs/{run_id}/cancel")
def run_cancel(run_id: str) -> dict[str, Any]:
    try:
        return cancel_run(run_id)
    except FileNotFoundError as exc:
        raise HTTPException(404, "Unknown run") from exc


@app.get("/api/runs/{run_id}/download")
def run_download(run_id: str):
    try:
        path = run_package_path(run_id)
    except FileNotFoundError as exc:
        raise HTTPException(404, str(exc)) from exc
    return FileResponse(path, filename=path.name)


@app.get("/api/gate-c/readiness")
def gate_c_ready() -> dict[str, Any]:
    return gate_c_readiness()


@app.post("/api/gate-c/analyze")
def gate_c_analyze() -> dict[str, Any]:
    try:
        return analyze_gate_c()
    except Exception as exc:
        raise HTTPException(400, str(exc)) from exc


def _available_port(host: str, preferred: int) -> int:
    for port in range(preferred, preferred + 20):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                sock.bind((host, port))
            except OSError:
                continue
            return port
    raise RuntimeError(f"No available local port in range {preferred}-{preferred + 19}")


def main() -> None:
    parser = argparse.ArgumentParser(description=f"Run GAL-FINGERPRINT-I Chamber v{CHAMBER_VERSION}")
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=8771)
    parser.add_argument("--open-browser", action="store_true")
    args = parser.parse_args()
    ensure_runtime_dirs()
    selected_port = _available_port(args.host, args.port)
    url = f"http://{args.host}:{selected_port}"
    print(f"GAL-FINGERPRINT-I Chamber v{CHAMBER_VERSION}: {url}", flush=True)
    if selected_port != args.port:
        print(f"Port {args.port} was occupied; using {selected_port} instead.", flush=True)
    if args.open_browser:
        threading.Timer(1.2, lambda: webbrowser.open(url)).start()
    uvicorn.run("gal_fingerprint_i.web:app", host=args.host, port=selected_port, reload=False)


if __name__ == "__main__":
    main()
