"""CAIN-A75-041 offline verifier (rules embedded verbatim). CAIN-A75-041 creative-workflow rules. Standard library only, no CAIN imports: embedded verbatim in the offline verifier.

A multistep creative agent produced an asset. The review recomputes whether the provenance chain holds and whether every
input was licensed and consented:

  steps       a DAG with existing parents; every step hashes its input and output and names its model and licence
  chain       each step's declared input equals the output of its parent step (no silent substitution)
  assets      every asset names a source; a generated asset names its model licence; a stock asset must be licensed
  likeness    an asset containing a person needs recorded likeness consent
  edits       an edit references an asset, hashes its output, and is reversible with the original preserved
  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 provenance record; it does not inspect the pixels.
"""
from __future__ import annotations

import re

_ID = re.compile(r"^[A-Za-z0-9_.:\-]{1,80}$")
_HEX = re.compile(r"^[0-9a-f]{64}$")
SOURCES = {"generated", "stock", "user"}
MEDIA = {"image", "video", "audio", "3d", "text"}


class Bad(ValueError):
    pass


def evaluate(record):
    checks, summary = [], {"steps": {}, "assets": {}}

    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.creative.provenance.v1":
            raise Bad("not a cain42.a75.creative.provenance.v1 record")
        project, steps, assets, pol = record.get("project"), record.get("steps"), record.get("assets"), record.get("policy")
        if not all(isinstance(x, dict) for x in (project, pol)):
            raise Bad("project and policy must be objects")
        c("project.media", project.get("media") in MEDIA, f"media {project.get('media')!r}")
        max_steps = pol.get("max_steps")
        if isinstance(max_steps, bool) or not isinstance(max_steps, int) or max_steps < 1:
            raise Bad("policy.max_steps must be a positive integer")
        c("policy.chain_required", pol.get("require_chain") is True, "chain is required")

        if not isinstance(steps, list) or not steps:
            raise Bad("steps must be a non-empty list")
        S = {}
        for s in steps:
            if not isinstance(s, dict) or not isinstance(s.get("id"), str) or not _ID.match(s["id"]):
                raise Bad("a step needs an id")
            if s["id"] in S:
                raise Bad(f"duplicate step {s['id']}")
            S[s["id"]] = s
        c("steps.count", len(steps) <= max_steps, f"{len(steps)} <= {max_steps}")
        for s in steps:
            c(f"{s['id']}.hashes", isinstance(s.get("input_sha256"), str) and bool(_HEX.match(s["input_sha256"]))
              and isinstance(s.get("output_sha256"), str) and bool(_HEX.match(s["output_sha256"])),
              "input and output hashes present")
            c(f"{s['id']}.model", isinstance(s.get("model"), str) and bool(s["model"]), f"model {s.get('model')!r}")
            c(f"{s['id']}.license", isinstance(s.get("license"), str) and bool(s["license"].strip()) or pol.get("require_model_license") is not True,
              f"license {s.get('license')!r}")

        # DAG + parent linkage
        parents = {s["id"]: (s.get("parent_steps") or []) for s in steps}
        c("steps.parents_exist", all(isinstance(p, str) and p in S for ps in parents.values() for p in ps),
          "every parent exists")
        state = {}

        def visit(x):
            if state.get(x) == 1:
                return True
            if state.get(x) == 2:
                return False
            state[x] = 1
            for p in parents.get(x, []):
                if p in S and visit(p):
                    return True
            state[x] = 2
            return False

        c("steps.acyclic", not any(visit(s["id"]) for s in steps if s["id"] not in state))
        for s in steps:
            ps = parents[s["id"]]
            if ps:
                c(f"{s['id']}.chain", any(s.get("input_sha256") == S[p].get("output_sha256") for p in ps if p in S),
                  "input matches a parent output")
            summary["steps"][s["id"]] = {"model": s.get("model"), "parents": ps}

        if not isinstance(assets, list) or not assets:
            raise Bad("assets must be a non-empty list")
        for a in assets:
            if not isinstance(a, dict) or not isinstance(a.get("id"), str) or not _ID.match(a["id"]):
                raise Bad("an asset needs an id")
            c(f"{a['id']}.hash", isinstance(a.get("sha256"), str) and bool(_HEX.match(a["sha256"])), "asset hash present")
            c(f"{a['id']}.source", a.get("source") in SOURCES, f"source {a.get('source')!r}")
            licensed = isinstance(a.get("license"), str) and bool(a["license"].strip())
            if a.get("source") == "stock" and pol.get("forbid_unlicensed_stock") is True:
                c(f"{a['id']}.licensed_stock", licensed, "stock asset is licensed")
            elif a.get("source") == "generated" and pol.get("require_model_license") is True:
                c(f"{a['id']}.model_license", licensed, "generated asset carries a model licence")
            if a.get("contains_person") is True and pol.get("require_likeness_consent") is True:
                c(f"{a['id']}.likeness_consent", a.get("consent_for_likeness") is True, "likeness consent recorded")
            summary["assets"][a["id"]] = {"source": a.get("source"), "licensed": licensed}

        edits = record.get("edits") if isinstance(record.get("edits"), list) else []
        for e in edits:
            if not isinstance(e, dict) or not isinstance(e.get("id"), str) or not _ID.match(e["id"]):
                raise Bad("an edit needs an id")
            c(f"edit.{e['id']}.asset", e.get("asset_id") in {a["id"] for a in assets}, f"asset {e.get('asset_id')!r}")
            c(f"edit.{e['id']}.output", isinstance(e.get("output_sha256"), str) and bool(_HEX.match(e["output_sha256"])),
              "edit output hash")
            c(f"edit.{e['id']}.reversible", e.get("reversible") is True, "the edit is reversible")
            c(f"edit.{e['id']}.original", e.get("original_preserved") is True, "the original is preserved")

        claims = record.get("claims") if isinstance(record.get("claims"), dict) else {}
        computed = {
            "chain_intact": all(not parents[s["id"]] or any(s.get("input_sha256") == S[p].get("output_sha256")
                                                               for p in parents[s["id"]] if p in S) for s in steps),
            "all_assets_licensed": all(
                (a.get("source") == "stock" and isinstance(a.get("license"), str) and bool(a["license"].strip()))
                or (a.get("source") == "generated" and (isinstance(a.get("license"), str) and bool(a["license"].strip()) or pol.get("require_model_license") is not True))
                or a.get("source") == "user" for a in assets),
            "likeness_consented": all(a.get("contains_person") is not True or a.get("consent_for_likeness") is True for a in assets),
        }
        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.creative.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-041, {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))
