#!/usr/bin/env python3
from pathlib import Path
import argparse, subprocess, shutil, sys, json, re, os
root=Path(__file__).resolve().parents[1]
p=argparse.ArgumentParser()
p.add_argument('--dsn',default=os.environ.get('MAKNOW_PG_DSN') or os.environ.get('MAKNOW_DATABASE_URL'))
p.add_argument('--patch',default=str(root/'sql/constraint_patch.sql'))
p.add_argument('--validate',default=str(root/'sql/post_repair_validate.sql'))
p.add_argument('--report',default=str(root/'evidence/runtime-report.json'))
args=p.parse_args()
status_path=root/'evidence/runtime-status.json'; report_path=Path(args.report)
psql=shutil.which('psql')
if not psql or not args.dsn:
    out={'schema':'DAT002_RUNTIME_STATUS_v1','status':'NOT_RUN_MISSING_REAL_POSTGRESQL_RUNTIME','postgresql_version':None,'fabricated_runtime_evidence':False,'required_for_learner_completion':True}
    status_path.write_text(json.dumps(out,indent=2)+'\n'); print(json.dumps(out,indent=2)); raise SystemExit(3)
patch=Path(args.patch); validate=Path(args.validate)
for path in [patch,validate]:
    if not path.exists(): raise SystemExit(f'Missing learner SQL file: {path}')
base=[psql,args.dsn,'-X','-v','ON_ERROR_STOP=1']
env=dict(os.environ); env.setdefault('PGAPPNAME','maknow-dat002-lab')
def run_file(path): return subprocess.run(base+['-f',str(path)],text=True,capture_output=True,env=env)
def run_sql(sql): return subprocess.run(base+['-c',sql],text=True,capture_output=True,env=env)
def snap(r): return {'returncode':r.returncode,'stdout':r.stdout.strip(),'stderr':r.stderr.strip()}
steps=[]
# Deterministic disposable fixture.
for name,path in [('starter_schema',root/'sql/starter_schema.sql'),('seed_valid',root/'sql/seed_valid.sql'),('seed_with_violation',root/'sql/seed_with_violation.sql'),('constraint_patch',patch)]:
    r=run_file(path); steps.append({'step':name,'expected':'ACCEPT','observed':'ACCEPT' if r.returncode==0 else 'REJECT',**snap(r)})
    if r.returncode:
        report={'schema':'DAT002_RUNTIME_PROBE_REPORT_v2','fabricated_runtime_evidence':False,'status':'FAIL_SETUP','steps':steps,'probes':[]}
        report_path.write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n'); print(json.dumps(report,indent=2)); raise SystemExit(r.returncode)
# A NOT VALID CHECK must already reject a new violating row.
pre_new=run_sql("INSERT INTO orders(id,customer_id,external_ref,amount,status) VALUES (1900,1,'ORD-1900',-9.00,'pending');")
steps.append({'step':'not_valid_enforces_new_bad_amount','expected':'REJECT','observed':'ACCEPT' if pre_new.returncode==0 else 'REJECT',**snap(pre_new)})
# Existing deliberate violation must make amount validation fail before repair.
pre_val=run_sql('ALTER TABLE orders VALIDATE CONSTRAINT orders_amount_positive_ck;')
steps.append({'step':'pre_repair_amount_validation','expected':'REJECT','observed':'ACCEPT' if pre_val.returncode==0 else 'REJECT',**snap(pre_val)})
# Repair only the known fixture row, then execute the learner validation phase.
repair=run_sql('UPDATE orders SET amount=5.00 WHERE id=102 AND amount<=0;')
steps.append({'step':'repair_deliberate_fixture_row','expected':'ACCEPT','observed':'ACCEPT' if repair.returncode==0 else 'REJECT',**snap(repair)})
post=run_file(validate)
steps.append({'step':'post_repair_validate_file','expected':'ACCEPT','observed':'ACCEPT' if post.returncode==0 else 'REJECT',**snap(post)})
vr=subprocess.run(base+['-At','-c','SHOW server_version;'],text=True,capture_output=True,env=env)
version=vr.stdout.strip() if vr.returncode==0 else 'UNAVAILABLE'
# Named probes are independent autocommit statements so expected failures do not hide later evidence.
text=(root/'sql/probes.sql').read_text(); parts=re.split(r'(?m)^-- @probe ',text)[1:]
probes=[]
for part in parts:
    first,*rest=part.splitlines(); m=re.fullmatch(r'(\S+) expected=(ACCEPT|REJECT) dimension=(\S+)',first.strip())
    if not m: raise SystemExit(f'Invalid probe marker: {first!r}')
    pid,expected,dimension=m.groups(); sql='\n'.join(rest).strip(); r=run_sql(sql); observed='ACCEPT' if r.returncode==0 else 'REJECT'
    probes.append({'id':pid,'dimension':dimension,'expected':expected,'observed':observed,'pass':observed==expected,'sql':sql,**snap(r)})
step_pass=all(x['observed']==x['expected'] for x in steps)
probe_pass=bool(probes) and all(x['pass'] for x in probes)
report={'schema':'DAT002_RUNTIME_PROBE_REPORT_v2','postgresql_version':version,'fabricated_runtime_evidence':False,'steps':steps,'probe_count':len(probes),'passed':sum(x['pass'] for x in probes),'total':len(probes),'status':'PASS' if step_pass and probe_pass else 'FAIL','probes':probes}
report_path.parent.mkdir(parents=True,exist_ok=True); report_path.write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
status={'schema':'DAT002_RUNTIME_STATUS_v1','status':'REAL_POSTGRESQL_RUN_'+report['status'],'postgresql_version':version,'fabricated_runtime_evidence':False,'required_for_learner_completion':True,'report':str(report_path)}
status_path.write_text(json.dumps(status,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'status':report['status'],'postgresql_version':version,'steps_passed':sum(x['observed']==x['expected'] for x in steps),'steps_total':len(steps),'probes_passed':report['passed'],'probes_total':report['total'],'report':str(report_path)},indent=2))
raise SystemExit(0 if report['status']=='PASS' else 1)
