from __future__ import annotations

import csv
import hashlib
import io
import json
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from .config import BUNDLED_DATA, active_roots, registered_paths, sha256_path


@dataclass(frozen=True)
class ExportBundle:
    source_path: Path
    result_name: str
    result_bytes: bytes
    manifest_name: str | None = None
    manifest_bytes: bytes | None = None
    run_manifest_name: str | None = None
    run_manifest_bytes: bytes | None = None

    @property
    def result_sha256(self) -> str:
        return hashlib.sha256(self.result_bytes).hexdigest()

    def result_json(self) -> dict[str, Any]:
        return json.loads(self.result_bytes.decode("utf-8"))

    def run_manifest_rows(self) -> list[dict[str, str]]:
        if self.run_manifest_bytes is None:
            payload = self.result_json().get("gal_fingerprint_i", {}).get("realizations", [])
            return [{str(k): "" if v is None else str(v) for k, v in row.items()} for row in payload]
        text = self.run_manifest_bytes.decode("utf-8-sig")
        return list(csv.DictReader(io.StringIO(text)))


def _zip_bundle(path: Path, result_pattern: str | None = None) -> ExportBundle:
    with zipfile.ZipFile(path) as archive:
        names = archive.namelist()
        results = [name for name in names if name.endswith("_results.json")]
        if result_pattern:
            results = [name for name in results if result_pattern.lower() in Path(name).name.lower()]
        if len(results) != 1:
            raise ValueError(f"{path}: expected one result JSON, found {len(results)}")
        result_name = results[0]
        manifests = [name for name in names if name.endswith("_manifest.json")]
        run_manifests = [name for name in names if Path(name).name == "run_manifest.csv" or Path(name).name.endswith("_run_manifest.csv")]
        return ExportBundle(
            source_path=path,
            result_name=result_name,
            result_bytes=archive.read(result_name),
            manifest_name=manifests[0] if manifests else None,
            manifest_bytes=archive.read(manifests[0]) if manifests else None,
            run_manifest_name=run_manifests[0] if run_manifests else None,
            run_manifest_bytes=archive.read(run_manifests[0]) if run_manifests else None,
        )


def _directory_bundle(folder: Path, stem_token: str | None = None) -> ExportBundle | None:
    results = sorted(folder.glob("*_results.json"), key=lambda p: p.stat().st_mtime, reverse=True)
    if stem_token:
        filtered = [p for p in results if stem_token.lower() in p.name.lower()]
        results = filtered or results
    if results:
        result = results[0]
        prefix = result.name.removesuffix("_results.json")
        manifest = folder / f"{prefix}_manifest.json"
        run_manifest = folder / "run_manifest.csv"
        if not run_manifest.is_file():
            candidates = sorted(folder.glob(f"{prefix}*_run_manifest.csv"), key=lambda p: p.stat().st_mtime, reverse=True)
            run_manifest = candidates[0] if candidates else run_manifest
        return ExportBundle(
            source_path=result,
            result_name=result.name,
            result_bytes=result.read_bytes(),
            manifest_name=manifest.name if manifest.is_file() else None,
            manifest_bytes=manifest.read_bytes() if manifest.is_file() else None,
            run_manifest_name=run_manifest.name if run_manifest.is_file() else None,
            run_manifest_bytes=run_manifest.read_bytes() if run_manifest.is_file() else None,
        )
    zips = sorted(folder.glob("*RUN_PACKAGE.zip"), key=lambda p: p.stat().st_mtime, reverse=True)
    if zips:
        return _zip_bundle(zips[0], stem_token)
    return None


def gate_b_record() -> tuple[dict[str, Any], Path]:
    candidates = [registered_paths()["gate_b_result"], BUNDLED_DATA / "gate_b/GAL_FINGERPRINT_I_GATE_B_RESULT_RECORD.json"]
    for path in candidates:
        if path.is_file():
            record = json.loads(path.read_text(encoding="utf-8"))
            if record.get("gate") == "GATE_B" and record.get("passed") is True:
                return record, path
    raise FileNotFoundError("Registered Gate B pass record is unavailable")


_GATE_B_PROVENANCE_AMENDMENT = """# GAL-FINGERPRINT-I Gate B provenance amendment

```text
Authoritative measurement kernel       STRUC-I v1.0.4
Successful runtime package              GAL-FINGERPRINT-I Chamber v0.1.3
Legacy exported controller label        0.1.0
Effect on numerical measurements        none
Rerun required                           no
```

The original R2–R5 direct exports and their SHA256 hashes remain unchanged. This amendment records the runtime/controller distinction without rewriting the preserved source files.
"""


def gate_b_provenance_amendment() -> str:
    """Return the Gate B amendment without making validation depend on a long bundled path.

    The bundled Markdown remains the preferred human-readable source. On Windows, archive
    extraction into a deeply nested project path can omit this one long-named file. The
    amendment is documentation rather than numerical evidence, so an identical embedded
    fallback is used instead of aborting registered-asset validation.
    """
    bundled = BUNDLED_DATA / "gate_b/GAL_FINGERPRINT_I_GATE_B_PROVENANCE_AMENDMENT.md"
    if bundled.is_file():
        return bundled.read_text(encoding="utf-8")
    return _GATE_B_PROVENANCE_AMENDMENT


def write_gate_b_provenance_amendment() -> Path:
    root = Path(active_roots()["analysis_root"]) / "reports"
    root.mkdir(parents=True, exist_ok=True)
    path = root / "GAL_FINGERPRINT_I_GATE_B_PROVENANCE_AMENDMENT.md"
    content = gate_b_provenance_amendment()
    if not path.is_file() or path.read_text(encoding="utf-8") != content:
        path.write_text(content, encoding="utf-8")
    return path


def repeat_bundle(repeat: str) -> ExportBundle:
    number = int(repeat[1:])
    roots = active_roots()
    candidates: list[Path] = []
    if roots["installed_in_project"]:
        candidates.append(Path(roots["export_root"]) / f"repeat_{number:02d}")
    candidates.append(BUNDLED_DATA / "gate_b" / f"repeat_{number:02d}")
    for folder in candidates:
        if folder.is_dir():
            bundle = _directory_bundle(folder, repeat.lower())
            if bundle is not None:
                return bundle
    raise FileNotFoundError(f"No direct export or run package found for {repeat}")


BRANCH_FOLDERS = {
    "LEAVE_ONE_RADIUS_OUT": Path("gal_fingerprint_i/deletion"),
    "THINNING_N10_DETERMINISTIC": Path("gal_fingerprint_i/thinning/deterministic"),
    "THINNING_N10_RANDOM": Path("gal_fingerprint_i/thinning/random_stratified"),
    "COVERAGE": Path("gal_fingerprint_i/coverage"),
    "ML_SENSITIVITY": Path("gal_fingerprint_i/ml_sensitivity"),
    "NULL_GAP_ORDER": Path("gal_fingerprint_i/null_gap_order"),
    "NULL_UNIFORM_RANGE": Path("gal_fingerprint_i/null_uniform_range"),
}

BRANCH_TOKENS = {
    "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",
}


def branch_export_folder(mode: str) -> Path:
    return Path(active_roots()["export_root"]) / BRANCH_FOLDERS[mode]


def branch_bundle(mode: str) -> ExportBundle:
    mode = mode.upper()
    if mode not in BRANCH_FOLDERS:
        raise ValueError(f"Unknown branch mode: {mode}")
    folder = branch_export_folder(mode)
    bundle = _directory_bundle(folder, BRANCH_TOKENS[mode]) if folder.is_dir() else None
    if bundle is None:
        raise FileNotFoundError(f"No completed direct export package found for {mode} under {folder}")
    return bundle


def gate_b_evidence_report() -> dict[str, Any]:
    try:
        record, record_path = gate_b_record()
    except Exception as exc:
        return {"gate_b_pass": False, "error": str(exc), "repeats": {}}
    expected_hashes = record.get("source_sha256", {})
    repeats: dict[str, Any] = {}
    all_ok = True
    for repeat in ("R2", "R3", "R4", "R5"):
        try:
            bundle = repeat_bundle(repeat)
            expected = expected_hashes.get(repeat)
            ok = expected is None or bundle.result_sha256 == expected
            repeats[repeat] = {
                "found": True,
                "source": str(bundle.source_path.resolve()),
                "result_member": bundle.result_name,
                "result_sha256": bundle.result_sha256,
                "expected_sha256": expected,
                "hash_match": bool(ok),
            }
            all_ok &= bool(ok)
        except Exception as exc:
            repeats[repeat] = {"found": False, "error": str(exc), "hash_match": False}
            all_ok = False
    r1_expected = expected_hashes.get("R1")
    r1_actual = sha256_path(registered_paths()["r1_profiles"])
    r1_ok = r1_expected is None or r1_actual == r1_expected
    all_ok &= r1_ok
    return {
        "gate_b_pass": bool(record.get("passed") and all(record.get("checks", {}).values()) and all_ok),
        "verdict": record.get("verdict"),
        "record_path": str(record_path.resolve()),
        "successful_runtime_package_version": record.get("successful_runtime_package_version", "0.1.3"),
        "legacy_exported_controller_label": record.get("legacy_exported_chamber_version", record.get("chamber_version", "0.1.0")),
        "r1": {"sha256": r1_actual, "expected_sha256": r1_expected, "hash_match": r1_ok},
        "repeats": repeats,
    }
