#!/usr/bin/env python3
"""Cross-check response truth files against their RFP truth file.

Run: python3 corpus/validate-responses.py corpus/friendship-pcs
Hand-authored ground truth — nothing else checks these.
"""
import json, sys, glob, os

root = sys.argv[1] if len(sys.argv) > 1 else 'friendship-pcs'
rfp = json.load(open(os.path.join(root, 'rfp.truth.json')))
reqs = {r['id']: r for r in rfp['requirements']}
gates = {r['id'] for r in rfp['requirements'] if r['kind'] == 'mandatory_gate'}
# A gate whose satisfaction is invisible to the response document cannot be scored
# from it. Excluded from the silent-gate check; see requirementNotes in the RFP truth.
undeterminable = {r['id'] for r in rfp['requirements']
                  if r.get('determinableFromResponse') is False}
COVERAGE = {'answered', 'not_answered', 'indeterminate'}

errs, warns = [], []
print(f"RFP: {len(reqs)} requirements, {len(gates)} gates\n")

for path in sorted(glob.glob(os.path.join(root, 'responses', '*.truth.json'))):
    t = json.load(open(path))
    v = t['vendor']['id']
    answered = {}
    for a in t['answers']:
        rid = a['requirementId']
        if rid not in reqs:
            errs.append(f"{v}: answer -> unknown requirement {rid}")
            continue
        if a['coverage'] not in COVERAGE:
            errs.append(f"{v}: {rid} bad coverage '{a['coverage']}'")
        if rid in answered:
            errs.append(f"{v}: duplicate answer for {rid}")
        answered[rid] = a

    missing = sorted(set(reqs) - set(answered))
    if missing:
        warns.append(f"{v}: {len(missing)} requirements have no answer entry: {', '.join(missing[:4])}{'...' if len(missing) > 4 else ''}")

    # gate failures must agree with the declared outcome
    declared = set(t['expectedOutcome'].get('failedGates', []))
    actual = {r for r, a in answered.items()
              if r in gates and (a.get('isGateFailure') or a['coverage'] == 'not_answered')}
    flagged = {r for r, a in answered.items() if a.get('isGateFailure')}
    if not declared <= flagged:
        errs.append(f"{v}: declared failedGates {sorted(declared - flagged)} not marked isGateFailure on the answer")
    if t['expectedOutcome']['gatesPassed'] and declared:
        errs.append(f"{v}: gatesPassed=true but failedGates is non-empty")
    if not t['expectedOutcome']['gatesPassed'] and not declared:
        errs.append(f"{v}: gatesPassed=false but no failedGates declared")

    # unflagged gate misses are a real signal, not necessarily an error
    silent = actual - declared - set(t['expectedOutcome'].get('atRiskGates', [])) - undeterminable
    if silent:
        warns.append(f"{v}: gate(s) not_answered but not declared failed or at-risk: {sorted(silent)}")

    for e in t.get('exceptions', []):
        rid = e.get('requirementId')
        if rid is not None and rid not in reqs:
            errs.append(f"{v}: exception {e['id']} -> unknown requirement {rid}")

    counts = {}
    for a in answered.values():
        counts[a['coverage']] = counts.get(a['coverage'], 0) + 1
    exc = len(t.get('exceptions', []))
    labelled = sum(1 for e in t.get('exceptions', []) if e.get('labelled'))
    print(f"  {v:11} binding={t['bindingMechanism']['level']:34} "
          f"answered={counts.get('answered',0):2} indet={counts.get('indeterminate',0)} "
          f"absent={counts.get('not_answered',0)}  exceptions={exc} ({labelled} labelled)  "
          f"gates={'PASS' if t['expectedOutcome']['gatesPassed'] else 'FAIL'}")

# corpus-level coverage of the stress-case checklist
files = [json.load(open(p)) for p in glob.glob(os.path.join(root, 'responses', '*.truth.json'))]
checks = {
  'tabbed compliance matrix':      any(f['bindingMechanism']['level'] == 'tabbed_restatement' for f in files),
  'free prose, no anchors':        any(f['bindingMechanism']['level'] == 'free_prose' for f in files),
  'rate function, not scalar':     len({f['priceQuote']['rateStructure']['kind'] for f in files}) >= 3,
  'partially non-responsive':      any(not f['expectedOutcome']['gatesPassed'] for f in files),
  'exceptions with price deltas':  any(e.get('priceDeltaIfDeclined') for f in files for e in f.get('exceptions', [])),
  'unlabelled exception':          any(e.get('labelled') is False for f in files for e in f.get('exceptions', [])),
  'labelled known-misses in RFP':  len(rfp.get('knownMisses', [])) >= 3,
  'miskinded-as-informational':    any(k['type'] == 'miskinded_as_informational' for k in rfp['knownMisses']),
  'answered-but-negative':         any('negative' in str(a.get('$comment','')).lower() for f in files for a in f['answers']),
}
print("\n  stress-case checklist")
for k, ok in checks.items():
    print(f"    {'x' if ok else ' '} {k}")
    if not ok: warns.append(f"stress case not covered: {k}")

for w in warns: print(f"\n  WARN {w}")
for e in errs: print(f"\n  ERR  {e}")
print("\nVALID" if not errs else "\nINVALID")
sys.exit(1 if errs else 0)
