"""Validate a student-authored report against the signed teaching evidence library."""
import argparse,base64,hashlib,hmac,json,re
from pathlib import Path
PREFIX=bytes.fromhex("3031300D060960864801650304020105000420")
FORBIDDEN=(r"hardwareAccessed\s*[=:]\s*true",r"pcanInitialized\s*[=:]\s*true",r"canFramesSent\s*[=:]\s*[1-9]",r"已(?:经)?(?:真实)?控制(?:了)?灵巧手",r"真机(?:执行|动作)(?:成功|完成)")
REASONING={
 "S01_PURPOSE":("CONNECT_AUTONOMOUS_LOOP","OFFLINE_SCOPE_ONLY","DEFINE_REAL_EVIDENCE_NEXT"),
 "S02_PERCEPTION":("QUALITY_GATED_PERCEPTION","DATA_MODEL_BINDING_ONLY","TEST_REAL_VARIATION_NEXT"),
 "S03_DECISION":("DETERMINISTIC_POLICY_WITH_STOP","SOURCE_PROVES_MAPPING_ONLY","TEST_INVALID_INPUT_NEXT"),
 "S04_EXECUTION_FEEDBACK":("TARGET_FEEDBACK_SEPARATION","TRACE_PROVES_MOCK_SEQUENCE","VERIFY_REAL_AXIS_NEXT"),
 "S05_SAFE_STOP":("TEST_SUCCESS_AND_FAIL_CLOSED","ASSERTIONS_SUPPORT_TWO_PATHS","ADD_BOUNDARY_CASES_NEXT"),
 "S06_REPLAY_TRACE":("COMPARE_ALIGNED_TIME_EVIDENCE","DELAY_IS_OBSERVATION_NOT_CAUSE","CAPTURE_SYNCED_DIAGNOSTICS_NEXT"),
 "S07_INTEGRITY_TIME":("SEPARATE_INTEGRITY_TIME_TRUST","STRUCTURE_IS_NOT_TRUTH","VERIFY_PRODUCTION_TRUST_NEXT"),
 "S08_LIMITS_NEXT":("STATE_LIMIT_AND_NEXT_TEST","CLAIM_ONLY_SYNTHETIC_RESULTS","PLAN_AUTHORIZED_REAL_REPLAY"),
}
def rsa(payload,signature,key):
 try:n=int(key["modulusHex"],16);e=key["exponent"];size=(n.bit_length()+7)//8;v=pow(int.from_bytes(signature,"big"),e,n).to_bytes(size,"big");target=PREFIX+hashlib.sha256(payload).digest();expected=b"\0\1"+b"\xff"*(size-len(target)-3)+b"\0"+target
 except (KeyError,ValueError,TypeError,OverflowError):return False
 return hmac.compare_digest(v,expected)
def detached(payload,sig,key):
 try:return sig["signedArtifactSha256"]==hashlib.sha256(payload).hexdigest().upper() and sig["signerKeyId"]==key["keyId"] and rsa(payload,base64.b64decode(sig["signatureBase64"],validate=True),key)
 except (KeyError,ValueError,TypeError):return False
def out(ok,reason,**facts):return {"schemaVersion":"0.1.0","artifactType":"STUDENT_CAPSTONE_REPORT_CHECK","reportReadyForStudentReview":ok,"firstReason":reason,"studentAuthorshipProven":False,"courseCreditIssued":False,"reservationEligible":False,"teacherApproved":False,"leaseIssued":False,"productionEvidence":False,"hardwareAuthority":0,"actionEligible":False,"decision":"NO_GO",**facts}
def process_binding_valid(value):
 if not isinstance(value,dict) or set(value)!={"evidenceDraft","rehearsal"}:return False
 evidence=value.get("evidenceDraft");rehearsal=value.get("rehearsal")
 if not isinstance(evidence,dict) or set(evidence)!={"artifactType","schemaVersion","sha256","evidenceSourceSha256","sourceLineage"} or evidence.get("artifactType")!="LOCAL_EVIDENCE_TO_REPORT_DRAFT" or evidence.get("schemaVersion")!="0.3.0":return False
 if not isinstance(rehearsal,dict) or set(rehearsal)!={"artifactType","schemaVersion","sha256","reportSha256","evidenceSourceSha256"} or rehearsal.get("artifactType")!="LOCAL_CAPSTONE_DEMO_REHEARSAL" or rehearsal.get("schemaVersion")!="0.3.0":return False
 hashes=(evidence.get("sha256"),evidence.get("evidenceSourceSha256"),rehearsal.get("sha256"),rehearsal.get("reportSha256"),rehearsal.get("evidenceSourceSha256"))
 if not all(isinstance(x,str) and re.fullmatch(r"[0-9A-F]{64}",x) for x in hashes) or rehearsal["reportSha256"]!=evidence["sha256"] or rehearsal["evidenceSourceSha256"]!=evidence["evidenceSourceSha256"]:return False
 lineage=evidence.get("sourceLineage")
 if not isinstance(lineage,dict):return False
 if lineage.get("lineageKind")=="CAMERA_OBSERVATION_ADAPTER_REVIEW":
  if set(lineage)!={"lineageKind","cameraRoleSourceSha256","observationRecordSha256","observationAdapterSha256","multimodalReviewSha256"}:return False
  return lineage["multimodalReviewSha256"]==evidence["evidenceSourceSha256"] and all(isinstance(lineage.get(k),str) and re.fullmatch(r"[0-9A-F]{64}",lineage[k]) for k in ("cameraRoleSourceSha256","observationRecordSha256","observationAdapterSha256"))
 return set(lineage)=={"lineageKind","traceAuditSha256","provenanceVerified"} and lineage.get("lineageKind")=="TRACE_AUDIT" and lineage.get("traceAuditSha256")==evidence["evidenceSourceSha256"] and lineage.get("provenanceVerified") is False
def verify(report_payload,library_payload,signature,key,root):
 try:report=json.loads(report_payload);library=json.loads(library_payload)
 except (UnicodeDecodeError,json.JSONDecodeError):return out(False,"INPUT_INVALID")
 if not detached(library_payload,signature,key):return out(False,"EVIDENCE_LIBRARY_SIGNATURE_INVALID")
 facts={"evidenceLibrarySignatureValid":True};entries={item["evidenceId"]:item for item in library.get("entries",[])}
 if len(entries)!=13:return out(False,"EVIDENCE_LIBRARY_SHAPE_INVALID",**facts)
 for item in entries.values():
  try:raw=(root/item["file"]).read_bytes()
  except (KeyError,OSError):return out(False,"CITED_EVIDENCE_FILE_MISSING",**facts)
  if len(raw)!=item.get("size") or hashlib.sha256(raw).hexdigest().upper()!=item.get("sha256"):return out(False,"CITED_EVIDENCE_HASH_MISMATCH",**facts)
 facts["evidenceFilesBound"]=13
 if set(report)!={"artifactType","evidenceLibrarySha256","hardwareClaims","learningProcessBinding","projectTitle","reportId","schemaVersion","sections","sourceOrigin","studentAuthored"} or report.get("artifactType")!="STUDENT_CAPSTONE_REPORT_DRAFT" or report.get("schemaVersion")!="0.3.0" or report.get("sourceOrigin")!="TEACHING_CANDIDATE" or report.get("studentAuthored") is not True or report.get("evidenceLibrarySha256")!=hashlib.sha256(library_payload).hexdigest().upper():return out(False,"REPORT_LIBRARY_BINDING_INVALID",**facts)
 if not process_binding_valid(report.get("learningProcessBinding")):return out(False,"LEARNING_PROCESS_BINDING_INVALID",**facts)
 facts["learningProcessBindingValid"]=True
 if report.get("hardwareClaims")!={"canFramesSent":0,"hardwareAccessed":False,"pcanInitialized":False}:return out(False,"HARDWARE_CLAIM_FORBIDDEN",**facts)
 expected=library.get("sections",[]);sections=report.get("sections")
 if not isinstance(sections,list) or [x.get("sectionId") for x in sections]!=[x.get("sectionId") for x in expected]:return out(False,"REPORT_SECTION_ORDER_INVALID",**facts)
 cited=set()
 for rule,section in zip(expected,sections):
  text=section.get("studentText");refs=section.get("citations")
  if set(section)!={"sectionId","studentReasoning","studentText","citations"}:return out(False,"STUDENT_REASONING_INVALID",**facts,failedSection=rule["sectionId"])
  reasoning=section.get("studentReasoning");expected_reasoning=REASONING[rule["sectionId"]]
  if not isinstance(reasoning,dict) or set(reasoning)!={"developmentDecision","evidenceInterpretation","boundaryPlan"} or (reasoning.get("developmentDecision"),reasoning.get("evidenceInterpretation"),reasoning.get("boundaryPlan"))!=expected_reasoning:return out(False,"STUDENT_REASONING_INVALID",**facts,failedSection=rule["sectionId"])
  if not isinstance(text,str) or not 48<=len(text.strip())<=600 or any(token in text for token in ("待填写","TODO","这里写","示例文字")):return out(False,"STUDENT_EXPLANATION_INCOMPLETE",**facts,failedSection=rule["sectionId"])
  if not isinstance(refs,list) or len(refs)!=len(set(refs)) or len(refs)<rule["minCitations"] or not set(refs)<=set(rule["allowedEvidence"]):return out(False,"SECTION_EVIDENCE_CITATION_INVALID",**facts,failedSection=rule["sectionId"])
  cited.update(refs)
 joined="\n".join(section["studentText"] for section in sections)
 if any(re.search(pattern,joined,re.I) for pattern in FORBIDDEN):return out(False,"HARDWARE_CLAIM_FORBIDDEN",**facts)
 return out(True,"STUDENT_REPORT_STRUCTURE_AND_CITATIONS_VALID",**facts,sectionCount=8,citedEvidenceCount=len(cited),sourceOrigin="TEACHING_CANDIDATE")
def main(argv=None):
 p=argparse.ArgumentParser();p.add_argument("--report",type=Path,required=True);p.add_argument("--library",type=Path,required=True);p.add_argument("--library-signature",type=Path,required=True);p.add_argument("--root-key",type=Path,required=True);a=p.parse_args(argv)
 try:r=verify(a.report.read_bytes(),a.library.read_bytes(),json.loads(a.library_signature.read_text(encoding="utf-8")),json.loads(a.root_key.read_text(encoding="utf-8")),a.library.parent.parent)
 except (OSError,UnicodeDecodeError,json.JSONDecodeError):r=out(False,"INPUT_INVALID")
 print(json.dumps(r,ensure_ascii=False,sort_keys=True,separators=(",",":")));return 0 if r["reportReadyForStudentReview"] else 2
if __name__=="__main__":raise SystemExit(main())
