"""CAIN-A75-036 offline verifier (rules embedded verbatim). CAIN-A75-036 threat-investigation rules. Standard library only, no CAIN imports: embedded verbatim in the offline
verifier.

A threat investigation claims it reached evidence-grounded conclusions. The review recomputes the case:

  evidence      every evidence item is hashed, its integrity verified, and its custody chain is unbroken to the case
  grounding     every finding cites existing evidence, meets the minimum count, and its confidence is in range
  hypotheses    confirmed or refuted hypotheses cite evidence; a high-severity case cannot leave one open
  timeline      every timeline entry points at existing evidence
  freshness     evidence is not older than the policy allows
  claims        every boolean the record asserts matches what this review computed

Verdict: SOUND, else UNSOUND with every failing check named. Malformed input is UNSOUND.
It reviews the investigation record; it does not re-collect evidence.
"""
from __future__ import annotations

import math
import re

_ID = re.compile(r"^[A-Za-z0-9_.:\-]{1,80}$")
_HEX = re.compile(r"^[0-9a-f]{64}$")
SEVERITIES = {"low", "medium", "high", "critical"}


class Bad(ValueError):
    pass


def _num(v, what, lo=0.0, hi=1e12):
    if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or not (lo <= v <= hi):
        raise Bad(f"{what} must be a finite number in [{lo}, {hi}]")
    return float(v)


def evaluate(record):
    checks, summary = [], {"evidence": {}, "findings": {}}

    def c(cid, ok, detail=""):
        checks.append({"id": cid, "ok": bool(ok), "detail": detail})

    try:
        if not isinstance(record, dict) or record.get("schema") != "cain42.a75.threat.case.v1":
            raise Bad("not a cain42.a75.threat.case.v1 record")
        case, pol = record.get("case"), record.get("policy")
        if not all(isinstance(x, dict) for x in (case, pol)):
            raise Bad("case and policy must be objects")
        severity = case.get("severity")
        if severity not in SEVERITIES:
            raise Bad(f"case.severity must be one of {sorted(SEVERITIES)}")
        now = _num(case.get("now"), "case.now")
        max_age = _num(pol.get("max_age_days"), "policy.max_age_days", 0.0, 100000.0)
        min_ev = pol.get("min_evidence_per_finding")
        if isinstance(min_ev, bool) or not isinstance(min_ev, int) or min_ev < 1:
            raise Bad("policy.min_evidence_per_finding must be a positive integer")
        c("policy.custody_required", pol.get("require_custody_chain") is True, "custody chain is required")

        evidence = record.get("evidence")
        if not isinstance(evidence, list) or not evidence:
            raise Bad("evidence must be a non-empty list")
        E = {}
        for e in evidence:
            if not isinstance(e, dict) or not isinstance(e.get("id"), str) or not _ID.match(e["id"]):
                raise Bad("an evidence item needs an id")
            if e["id"] in E:
                raise Bad(f"duplicate evidence {e['id']}")
            E[e["id"]] = e
            c(f"evidence.{e['id']}.hash", isinstance(e.get("sha256"), str) and bool(_HEX.match(e["sha256"])), "content hash present")
            c(f"evidence.{e['id']}.integrity", e.get("integrity_verified") is True, "integrity verified")
            collected = _num(e.get("collected_at"), f"evidence.{e['id']}.collected_at")
            c(f"evidence.{e['id']}.fresh", (now - collected) / 86400.0 <= max_age + 1e-9,
              f"{(now - collected) / 86400.0:.1f} days <= {max_age}")
            custody = e.get("custody")
            c(f"evidence.{e['id']}.custody", isinstance(custody, list) and bool(custody)
              and all(isinstance(h, dict) and h.get("to") for h in custody), "custody chain present")
            summary["evidence"][e["id"]] = {"collected_at": collected}

        findings = record.get("findings")
        if not isinstance(findings, list) or not findings:
            raise Bad("findings must be a non-empty list")
        F = {}
        for f in findings:
            if not isinstance(f, dict) or not isinstance(f.get("id"), str) or not _ID.match(f["id"]):
                raise Bad("a finding needs an id")
            if f["id"] in F:
                raise Bad(f"duplicate finding {f['id']}")
            F[f["id"]] = f
            ids = f.get("evidence_ids")
            c(f"{f['id']}.evidence_count", isinstance(ids, list) and len(ids) >= min_ev, f"{len(ids or [])} >= {min_ev}")
            c(f"{f['id']}.evidence_exist", isinstance(ids, list) and all(x in E for x in ids), f"cites {ids!r}")
            _num(f.get("confidence"), f"{f['id']}.confidence", 0.0, 1.0)
            c(f"{f['id']}.technique", isinstance(f.get("mitre_technique"), str) and bool(f["mitre_technique"]), "a technique is named")
            summary["findings"][f["id"]] = {"confidence": f.get("confidence"), "evidence": ids}

        for h in record.get("hypotheses", []) if isinstance(record.get("hypotheses"), list) else []:
            if not isinstance(h, dict) or not isinstance(h.get("id"), str):
                raise Bad("a hypothesis needs an id")
            status = h.get("status")
            c(f"hypothesis.{h['id']}.status", status in ("confirmed", "refuted", "open"), f"status {status!r}")
            if status in ("confirmed", "refuted"):
                ids = h.get("evidence_ids")
                c(f"hypothesis.{h['id']}.grounded", isinstance(ids, list) and bool(ids) and all(x in E for x in ids),
                  "a decided hypothesis cites evidence")
            if severity in ("high", "critical"):
                c(f"hypothesis.{h['id']}.not_open", status != "open", "a high-severity case cannot leave a hypothesis open")

        for k, t in enumerate(record.get("timeline", []) if isinstance(record.get("timeline"), list) else []):
            if not isinstance(t, dict):
                raise Bad("a timeline entry must be an object")
            c(f"timeline.{k}.evidence", t.get("evidence_id") in E, f"evidence {t.get('evidence_id')!r}")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "all_findings_grounded": all(
                isinstance(f.get("evidence_ids"), list) and len(f["evidence_ids"]) >= min_ev
                and all(x in E for x in f["evidence_ids"]) for f in findings),
            "custody_intact": all(bool(e.get("custody")) and e.get("integrity_verified") is True for e in evidence),
            "no_open_high_hypothesis": severity not in ("high", "critical") or all(
                h.get("status") != "open" for h in (record.get("hypotheses") or []) if isinstance(h, dict)),
        }
        for key in sorted(claims):
            c(f"claims.{key}", key in computed and claims[key] == computed[key], f"claimed {claims[key]!r}, computed {computed.get(key)!r}")
        summary["computed"] = computed
    except Bad as e:
        checks.append({"id": "input.well_formed", "ok": False, "detail": str(e)})
        return "UNSOUND", checks, summary
    except Exception as e:  # noqa: BLE001
        checks.append({"id": "input.error", "ok": False, "detail": f"{type(e).__name__}: {e}"[:200]})
        return "UNSOUND", checks, summary
    return ("SOUND" if all(x["ok"] for x in checks) else "UNSOUND"), checks, summary


# ---------------------------------------------------------------------------- offline verifier (no CAIN imports)
# Every name below is prefixed _kit_verifier_ / _b64 / _hl / _json / _sys so it cannot shadow a name in the rules above.
import base64 as _b64, hashlib as _hl, json as _json, sys as _sys
_KIT_VERIFIER_GENESIS = _hl.sha256(b"cain42.a75.threat.genesis").hexdigest()


def _kit_verifier_canon(o):
    return _json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()


def _kit_verifier_sha(o):
    return _hl.sha256(_kit_verifier_canon(o)).hexdigest()


def _kit_verifier_main(argv):
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    if len(argv) < 2:
        print("usage: verify_offline.py run.json --pinned-key BASE64"); return 2
    run = _json.load(open(argv[1]))
    pin = argv[argv.index("--pinned-key") + 1] if "--pinned-key" in argv else None
    problems, prev, recs = [], _KIT_VERIFIER_GENESIS, run.get("records") or []
    for i, r in enumerate(recs):
        body = {k: v for k, v in r.items() if k not in ("record_hash", "signature_b64")}
        if _kit_verifier_sha(body) != r.get("record_hash"): problems.append(f"record {i}: record_hash does not match")
        if body.get("subject_sha256") != _kit_verifier_sha(body.get("subject")): problems.append(f"record {i}: subject_sha256 does not match")
        if pin and r.get("public_key_b64") != pin: problems.append(f"record {i}: different key than the pinned one")
        try:
            Ed25519PublicKey.from_public_bytes(_b64.b64decode(pin or r.get("public_key_b64"))).verify(
                _b64.b64decode(r.get("signature_b64", "")), str(r.get("record_hash")).encode())
        except Exception:
            problems.append(f"record {i}: signature does not verify")
        if r.get("prev_record_hash") != prev: problems.append(f"record {i}: chain link broken")
        v, checks, _ = evaluate(r.get("subject"))
        if v != r.get("verdict") or checks != r.get("checks"):
            problems.append(f"record {i}: re-running the rules gives {v}, the record says {r.get('verdict')}")
        prev = r.get("record_hash")
    if run.get("run_hash") != _kit_verifier_sha([r.get("record_hash") for r in recs]): problems.append("run_hash does not match")
    for p in problems: print("  -", p)
    if not pin: print("WARNING: no --pinned-key given; the signature was checked against the key inside the file")
    print("VERDICT:", "VERIFIED" if not problems else "TAMPERED OR INVALID", f"(CAIN-A75-036, {len(recs)} record(s), verdicts {run.get('verdicts')})")
    return 0 if not problems else 1


if __name__ == "__main__":
    _sys.exit(_kit_verifier_main(_sys.argv))
