"""Inspect a Replay dataset locally without accepting it as real evidence."""
from __future__ import annotations
import argparse,hashlib,json,re
from datetime import datetime
from pathlib import Path

HEX=re.compile(r"^[A-Fa-f0-9]{64}$")
QUALITY={"GOOD","STALE","MISSING","ESTIMATED","OUT_OF_RANGE","SOURCE_ERROR"}
STATES={"IDLE","EXECUTING","STOPPED","FAULT"}
REDACTION={"directIdentifiersRemoved","networkAddressesRemoved","usernamesRemoved","filePathsRemoved","freeTextReviewed"}
NOTE_KEYS={"schemaVersion","artifactType","authority","datasetId","declaredOrigin","originPrediction","inspectedAxes","axisObservations","checks","sampleCount","selectedConclusion","studentExplanation","studentTextIncluded","studentAuthorshipProven","supports","doesNotSupport","realDataAccepted","realEvidenceEligible","hardwareAccessed","pcanInitialized","canFramesSent","decision"}
AXIS_NOTE_KEYS={"axisId","firstActual","lastActual","nonGoodCount","observation"}

def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest().upper()
def result(ok,reason,**facts):return {"schemaVersion":"0.1.0","artifactType":"LOCAL_REPLAY_INTAKE_INSPECTION","structureValid":ok,"firstReason":reason,"realDataAccepted":False,"realEvidenceEligible":False,"hardwareAccessed":False,"pcanInitialized":False,"canFramesSent":0,"decision":"NO_GO",**facts}
def load_json(path,label):
 try:value=json.loads(path.read_text(encoding="utf-8"))
 except (OSError,UnicodeDecodeError,json.JSONDecodeError):raise ValueError(label+"无法读取")
 if not isinstance(value,dict):raise ValueError(label+"必须是JSON对象")
 return value
def child(root,name):
 if not isinstance(name,str) or not name or Path(name).name!=name:raise ValueError("数据文件路径必须是目录内文件名")
 path=(root/name).resolve()
 if path.parent!=root.resolve() or not path.is_file():raise ValueError("数据文件不存在："+name)
 return path
def inspect(root):
 root=root.resolve()
 try:
  manifest=load_json(root/"manifest.json","manifest.json")
  required={"schemaVersion","datasetId","datasetVersion","dataOrigin","title","deviceId","action","feedbackFile","files"}
  if not required<=set(manifest) or manifest.get("schemaVersion")!="0.1.0":raise ValueError("Manifest字段或版本无效")
  origin=manifest.get("dataOrigin")
  if origin not in {"SYNTHETIC","REAL_REDACTED"}:raise ValueError("dataOrigin必须明确")
  files=manifest.get("files")
  if not isinstance(files,list) or not files:raise ValueError("files哈希清单为空")
  hashes={}
  for item in files:
   if not isinstance(item,dict) or set(item)!={"path","sha256"} or not isinstance(item["sha256"],str) or not HEX.fullmatch(item["sha256"]):raise ValueError("files哈希条目无效")
   path=child(root,item["path"]);actual=digest(path)
   if actual!=item["sha256"].upper():raise ValueError("文件哈希不一致："+item["path"])
   hashes[item["path"]]=actual
  feedback_name=manifest.get("feedbackFile")
  if feedback_name not in hashes:raise ValueError("feedbackFile未进入哈希清单")
  samples=[]
  for number,line in enumerate(child(root,feedback_name).read_text(encoding="utf-8").splitlines(),1):
   if not line.strip():continue
   try:sample=json.loads(line)
   except json.JSONDecodeError:raise ValueError(f"反馈第{number}行不是JSON")
   if not isinstance(sample,dict) or set(sample)!={"sampleIndex","monotonicNs","deviceState","axes"}:raise ValueError(f"反馈第{number}行字段无效")
   if sample["sampleIndex"]!=len(samples):raise ValueError("sampleIndex必须从0连续递增")
   if not isinstance(sample["monotonicNs"],int) or isinstance(sample["monotonicNs"],bool) or sample["monotonicNs"]<0:raise ValueError("monotonicNs无效")
   if samples and sample["monotonicNs"]<=samples[-1]["monotonicNs"]:raise ValueError("monotonicNs必须严格递增")
   if sample["deviceState"] not in STATES:raise ValueError("deviceState无效")
   axes=sample["axes"]
   if not isinstance(axes,list) or len(axes)!=6 or sorted(axis.get("axisId") for axis in axes)!=[1,2,3,4,5,6]:raise ValueError("每个样本必须包含A1—A6")
   for axis in axes:
    if not {"axisId","targetPosition","actualPosition","quality"}<=set(axis) or axis["quality"] not in QUALITY:raise ValueError("六轴字段或质量无效")
    for field in ("targetPosition","actualPosition"):
     value=axis[field]
     if value is not None and (not isinstance(value,int) or isinstance(value,bool) or not 0<=value<=4095):raise ValueError(field+"超出0—4095")
   samples.append(sample)
  if not samples:raise ValueError("反馈文件没有样本")
  action=manifest.get("action")
  if not isinstance(action,dict) or not {"name","version","positions"}<=set(action) or not isinstance(action.get("positions"),list) or len(action["positions"])!=6 or any(not isinstance(v,int) or isinstance(v,bool) or not 0<=v<=4095 for v in action["positions"]):raise ValueError("动作必须包含六个0—4095整数目标")
  for sample in samples:
   if [axis["targetPosition"] for axis in sorted(sample["axes"],key=lambda x:x["axisId"])]!=action["positions"]:raise ValueError("反馈目标与Manifest动作不一致")
  provenance_checked=False
  if origin=="REAL_REDACTED":
   provenance_name=manifest.get("provenanceFile")
   if provenance_name not in hashes:raise ValueError("REAL_REDACTED缺少已绑定的provenanceFile")
   provenance=load_json(child(root,provenance_name),"provenance.json")
   if provenance.get("schemaVersion")!="0.1.0" or provenance.get("declaredOrigin")!="REAL_REDACTED" or provenance.get("sourceSystemClass")!="IPC11_PCAN_CAPTURE":raise ValueError("来源声明与IPC11采集类别不一致")
   if not str(provenance.get("sourceDatasetId","")).startswith("REDACTED-") or not str(provenance.get("devicePseudonym","")).startswith("REDACTED-") or not str(provenance.get("authorizationRef","")).startswith("AUTH-"):raise ValueError("匿名数据集、设备或授权引用无效")
   if manifest.get("deviceId")!=provenance.get("devicePseudonym"):raise ValueError("设备匿名ID绑定不一致")
   redaction=provenance.get("redaction")
   if not isinstance(redaction,dict) or set(redaction)!=REDACTION or not all(redaction.values()):raise ValueError("五项脱敏检查尚未全部完成")
   timebase=provenance.get("timebase")
   if not isinstance(timebase,dict) or timebase.get("kind")!="MONOTONIC_NS" or timebase.get("clockRelation") not in {"UTC_SYNCED","UTC_UNAVAILABLE"}:raise ValueError("时间基准证明无效")
   try:captured=datetime.fromisoformat(str(provenance.get("capturedAtUtc","")).replace("Z","+00:00"))
   except ValueError:raise ValueError("采集时间不是带时区的ISO-8601")
   if captured.tzinfo is None:raise ValueError("采集时间必须包含时区")
   provenance_checked=True
  return result(True,"STRUCTURE_AND_HASHES_VALID_NOT_ACCEPTED",datasetId=str(manifest["datasetId"]),declaredOrigin=origin,sampleCount=len(samples),fileCount=len(hashes),provenanceChecked=provenance_checked,inspectionAuthority="LOCAL_INSPECTION_ONLY")
 except (OSError,UnicodeDecodeError,ValueError,TypeError,KeyError) as error:return result(False,str(error),inspectionAuthority="LOCAL_INSPECTION_ONLY")
def verify_note(value):
 def reject(reason):return {"schemaVersion":"0.2.0","artifactType":"LOCAL_REPLAY_INTAKE_NOTE_VERIFICATION","valid":False,"firstReason":reason,"realDataAccepted":False,"realEvidenceEligible":False,"hardwareAccessed":False,"canFramesSent":0,"decision":"NO_GO"}
 if not isinstance(value,dict) or set(value)!=NOTE_KEYS:return reject("学习记录字段集合无效")
 if value["schemaVersion"]!="0.2.0" or value["artifactType"]!="LOCAL_REPLAY_INTAKE_LEARNING_NOTE" or value["authority"]!="LOCAL_LEARNING_DRAFT":return reject("学习记录版本、类型或权限层无效")
 if value["declaredOrigin"] not in {"SYNTHETIC","REAL_REDACTED"} or value["originPrediction"] not in {None,"SYNTHETIC","REAL_REDACTED","REAL_EVIDENCE"}:return reject("来源判断无效")
 if value["inspectedAxes"]!=[1,2,3,4,5,6]:return reject("必须完成A1—A6六轴观察")
 observations=value["axisObservations"]
 if not isinstance(observations,list) or len(observations)!=6:return reject("六轴观察记录必须正好六项")
 for expected,item in enumerate(observations,1):
  if not isinstance(item,dict) or set(item)!=AXIS_NOTE_KEYS or item["axisId"]!=expected or item["observation"]!="EXACT_READING":return reject("六轴观察顺序或结论无效")
  for field in ("firstActual","lastActual"):
   reading=item[field]
   if reading is not None and (not isinstance(reading,int) or isinstance(reading,bool) or not 0<=reading<=4095):return reject("轴首末实际值无效")
  if not isinstance(item["nonGoodCount"],int) or isinstance(item["nonGoodCount"],bool) or item["nonGoodCount"]<0:return reject("非GOOD质量计数无效")
 checks=value["checks"]
 if not isinstance(checks,list) or len(checks)!=5 or any(not isinstance(item,dict) or set(item)!={"status","message"} or item["status"]!="pass" or not isinstance(item["message"],str) or not item["message"] for item in checks):return reject("五道本地检查门尚未全部通过")
 if not isinstance(value["sampleCount"],int) or isinstance(value["sampleCount"],bool) or value["sampleCount"]<1:return reject("样本数无效")
 if value["selectedConclusion"]!="STRUCTURE_ONLY_REALITY_UNPROVEN":return reject("证据结论越界")
 explanation=value["studentExplanation"]
 if not isinstance(explanation,str) or not 36<=len(explanation.strip())<=360:return reject("学生解释必须为36—360字")
 if value["studentTextIncluded"] is not True or value["studentAuthorshipProven"] is not False:return reject("学生文本声明无效")
 if not isinstance(value["supports"],str) or not value["supports"] or not isinstance(value["doesNotSupport"],str) or not value["doesNotSupport"]:return reject("证据支持与边界说明不能为空")
 fixed={"realDataAccepted":False,"realEvidenceEligible":False,"hardwareAccessed":False,"pcanInitialized":False,"canFramesSent":0,"decision":"NO_GO"}
 if any(value[key]!=expected for key,expected in fixed.items()):return reject("学习记录不得获得真实数据或硬件权限")
 return {"schemaVersion":"0.2.0","artifactType":"LOCAL_REPLAY_INTAKE_NOTE_VERIFICATION","valid":True,"firstReason":"STUDENT_OBSERVATION_AND_BOUNDARY_VALID","datasetId":str(value["datasetId"]),"studentTextIncluded":True,"studentAuthorshipProven":False,**fixed}
def main(argv=None):
 parser=argparse.ArgumentParser();group=parser.add_mutually_exclusive_group(required=True);group.add_argument("--dataset",type=Path);group.add_argument("--note",type=Path);args=parser.parse_args(argv)
 if args.note:
  try:value=verify_note(json.loads(args.note.read_text(encoding="utf-8")))
  except (OSError,UnicodeDecodeError,json.JSONDecodeError):value={"schemaVersion":"0.2.0","artifactType":"LOCAL_REPLAY_INTAKE_NOTE_VERIFICATION","valid":False,"firstReason":"学习记录无法读取","realDataAccepted":False,"realEvidenceEligible":False,"hardwareAccessed":False,"canFramesSent":0,"decision":"NO_GO"}
  ok=value["valid"]
 else:value=inspect(args.dataset);ok=value["structureValid"]
 print(json.dumps(value,ensure_ascii=False,sort_keys=True,separators=(",",":")));return 0 if ok else 2
if __name__=="__main__":raise SystemExit(main())
