#!/usr/bin/env python3
import argparse, datetime, hashlib, json, re, sys
from pathlib import Path

SCHEMA = "CMMX_EXTERNAL_EVALUATION_CONTRACT_V1"
REJECT_KEYS = {"stale", "missing", "identity_mismatch", "verifier_fail", "claim_boundary_breach"}
REQUIRED_FORBIDDEN = {"direction", "entry/exit", "recommendation"}
PLACEHOLDER_MARKERS = ("<required>", "<set ", "<fixed ", "<external", "<predeclared", "<integer")


def missing(value):
    if value is None: return True
    if isinstance(value, str):
        s = value.strip().lower()
        return not s or any(m in s for m in PLACEHOLDER_MARKERS) or (s.startswith("<") and s.endswith(">"))
    return False


def add(errs, cond, code):
    if not cond: errs.append(code)


def get(d, *path):
    cur = d
    for key in path:
        if not isinstance(cur, dict): return None
        cur = cur.get(key)
    return cur

def validate(d):
    e = []
    add(e, d.get("schema") == SCHEMA, "SCHEMA_MISMATCH")
    add(e, d.get("CONTRACT_STATE") == "FROZEN", "CONTRACT_NOT_FROZEN")
    add(e, not missing(d.get("EVALUATION_ID")), "EVALUATION_ID_MISSING")
    for k in ("organisation", "team", "role", "workflow"):
        add(e, not missing(get(d, "CONSUMER", k)), f"CONSUMER_{k.upper()}_MISSING")
    for k in ("BUSINESS_CONTEXT", "EVALUATION_GOAL", "EXPECTED_OUTPUT", "MEASUREMENT_OWNER", "FROZEN_AT_UTC"):
        add(e, not missing(d.get(k)), f"{k}_MISSING")
    rid = get(d, "RELEASE", "release_id")
    rh = get(d, "RELEASE", "release_manifest_sha256")
    add(e, not missing(rid), "RELEASE_ID_MISSING")
    add(e, isinstance(rh, str) and re.fullmatch(r"[0-9a-f]{64}", rh or "") is not None, "RELEASE_HASH_INVALID")
    for k in ("state_time_semantics",):
        add(e, not missing(get(d, "TEMPORAL_REQUIREMENT", k)), f"TEMPORAL_{k.upper()}_MISSING")
    for k in ("max_freshness_seconds", "max_delivery_latency_seconds"):
        v = get(d, "TEMPORAL_REQUIREMENT", k)
        add(e, isinstance(v, int) and v > 0, f"TEMPORAL_{k.upper()}_INVALID")
    for k in ("method", "independent_environment", "retention_requirement"):
        add(e, not missing(get(d, "VERIFICATION", k)), f"VERIFICATION_{k.upper()}_MISSING")
    fs = d.get("FAILURE_SEMANTICS") or {}
    add(e, isinstance(fs, dict), "FAILURE_SEMANTICS_MISSING")
    for k in REJECT_KEYS:
        add(e, fs.get(k) == "REJECT", f"FAILURE_SEMANTICS_{k.upper()}_NOT_REJECT")
    ac = d.get("ACCEPTANCE_CRITERIA")
    add(e, isinstance(ac, list) and len(ac) > 0, "ACCEPTANCE_CRITERIA_MISSING")
    if isinstance(ac, list):
        add(e, all(isinstance(x, str) and not missing(x) for x in ac), "ACCEPTANCE_CRITERIA_PLACEHOLDER")
    fi = d.get("FORBIDDEN_INTERPRETATIONS") or []
    add(e, isinstance(fi, list) and REQUIRED_FORBIDDEN.issubset(set(fi)), "FORBIDDEN_INTERPRETATIONS_INCOMPLETE")
    try:
        dt = datetime.datetime.fromisoformat(str(d.get("FROZEN_AT_UTC", "")).replace("Z", "+00:00"))
        add(e, dt.tzinfo is not None, "FROZEN_AT_UTC_NOT_OFFSET_AWARE")
    except Exception:
        e.append("FROZEN_AT_UTC_INVALID")
    if d.get("MARKET_GO") not in (None, False): e.append("MARKET_GO_FORBIDDEN")
    if d.get("ACTION_GATE") not in (None, 0): e.append("ACTION_GATE_FORBIDDEN")
    if d.get("predictive_claim") not in (None, "NONE"): e.append("PREDICTIVE_CLAIM_FORBIDDEN")
    return sorted(set(e))

def main():
    ap = argparse.ArgumentParser(description="Validate one frozen CMM.X external evaluation contract.")
    ap.add_argument("contract")
    args = ap.parse_args()
    p = Path(args.contract)
    try:
        raw = p.read_bytes()
        d = json.loads(raw)
    except Exception as ex:
        print(json.dumps({"verdict":"FAIL","errors":[f"INPUT_READ_ERROR:{type(ex).__name__}"]}, sort_keys=True))
        return 2
    errors = validate(d)
    out = {
        "schema":"CMMX_EXTERNAL_EVALUATION_CONTRACT_VALIDATION_V1",
        "contract_path":str(p),
        "contract_sha256":hashlib.sha256(raw).hexdigest(),
        "verdict":"PASS" if not errors else "FAIL",
        "errors":errors,
        "d2_evidence_created":False,
        "note":"PASS means the evaluation contract is structurally frozen before measurement; it is not pilot evidence."
    }
    print(json.dumps(out, sort_keys=True, ensure_ascii=False))
    return 0 if not errors else 1

if __name__ == "__main__":
    sys.exit(main())
