#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
from urllib.parse import urlparse
import hashlib, json, os, re, secrets, shutil, subprocess, sys, tempfile

ROOT=Path(__file__).resolve().parents[2]
LEARNER=ROOT/'learner'
RUNTIME_DIR=LEARNER/'evidence'/'runtime'
STATUS_PATH=LEARNER/'evidence'/'runtime-status.json'
POLICY_PATH=LEARNER/'toolchain-policy.json'
REQUIRED_MARKERS=[
    'DAT008_EVIDENCE_VECTORS','DAT008_EVIDENCE_QUERIES','DAT008_EVIDENCE_MATCH_RANK',
    'DAT008_EVIDENCE_INDEX_DEFINITION','DAT008_EVIDENCE_EXPLAIN_Q1','DAT008_FAILURE_PROBE'
]
TODO_RE=re.compile(r'__TODO_[A-Z0-9_]+__')

class SafetyError(RuntimeError): pass
class SubmissionError(RuntimeError): pass

def sha256(path:Path)->str:
    return hashlib.sha256(path.read_bytes()).hexdigest()

def validate_target(url:str, confirmation:str, policy:dict|None=None)->dict:
    policy=policy or json.loads(POLICY_PATH.read_text())
    if confirmation!='YES':
        raise SafetyError('explicit disposable confirmation missing')
    u=urlparse(url)
    if u.scheme not in {'postgresql','postgres'}:
        raise SafetyError('unsupported database URL scheme')
    host=(u.hostname or '').lower()
    if host not in set(policy['allowed_hosts']):
        raise SafetyError('database host is not loopback-only')
    db=(u.path or '').lstrip('/')
    if not db.startswith(policy['database_name_prefix']) or db==policy['database_name_prefix']:
        raise SafetyError('database name is outside the disposable prefix contract')
    if db.lower() in {'postgres','template0','template1'}:
        raise SafetyError('default/system database forbidden')
    return {'host_class':'LOOPBACK_ONLY','database_identity':db}

def unresolved_submission(paths:list[Path])->list[str]:
    bad=[]
    for p in paths:
        if not p.exists():
            bad.append(f'MISSING:{p.name}'); continue
        hits=TODO_RE.findall(p.read_text())
        if hits: bad.append(f'UNRESOLVED:{p.name}:{sorted(set(hits))}')
    return bad

def base_status(status:str, reason:str, **extra)->dict:
    out={
      'schema':'DAT008_RUNTIME_STATUS_v2','status':status,
      'actual_postgresql_runtime_evidence_claimed':False,'actual_match_rank_plan_evidence_claimed':False,
      'runtime_claimable':False,'postgresql_version':None,'database_identity':None,'host_class':None,
      'generated_schema':None,'input_sha256':{},'runtime_receipt':None,'cleanup_status':'NOT_RUN','reason':reason
    }
    out.update(extra); return out

def write_status(obj:dict)->None:
    STATUS_PATH.write_text(json.dumps(obj,indent=2)+"\n")

def run_cmd(cmd:list[str], env:dict, timeout:int=20)->subprocess.CompletedProcess:
    return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env, timeout=timeout)

def main()->int:
    policy=json.loads(POLICY_PATH.read_text())
    url=os.environ.get(policy['database_url_env'],'')
    confirmation='YES' if os.environ.get('MAKNOW_DAT008_CONFIRM_DISPOSABLE')=='YES' else ''
    try:
        target=validate_target(url,confirmation,policy)
    except Exception as e:
        write_status(base_status('REFUSED_BY_SAFETY_GATE',str(e)))
        print(f'REFUSED: {e}')
        return 2
    psql=shutil.which('psql')
    if not psql:
        write_status(base_status('NOT_RUN_MISSING_PSQL','psql binary not available',**target))
        print('NOT RUN: psql binary not available')
        return 3
    submissions=[LEARNER/'submission/03_search_workbench.sql',LEARNER/'submission/04_failure_probe.sql']
    bad=unresolved_submission(submissions)
    if bad:
        write_status(base_status('REFUSED_INCOMPLETE_SUBMISSION','; '.join(bad),**target))
        print('REFUSED: incomplete submission')
        return 4
    if RUNTIME_DIR.exists(): shutil.rmtree(RUNTIME_DIR)
    RUNTIME_DIR.mkdir(parents=True)
    env=os.environ.copy()
    env['PGCONNECT_TIMEOUT']=str(policy['connect_timeout_seconds'])
    env['PGOPTIONS']=f"-c statement_timeout={policy['statement_timeout_ms']} -c lock_timeout=3000"
    probe=run_cmd([psql,'-X','-At',url,'-v','ON_ERROR_STOP=1','-c',"SELECT current_database() || '|' || current_setting('server_version_num');"],env)
    if probe.returncode:
        write_status(base_status('RUN_FAILED_NONCLAIMABLE','server identity probe failed',**target))
        return 5
    line=probe.stdout.strip().splitlines()[-1] if probe.stdout.strip() else ''
    if '|' not in line:
        write_status(base_status('RUN_FAILED_NONCLAIMABLE','server identity probe malformed',**target))
        return 6
    db,version_num=line.split('|',1)
    if db!=target['database_identity'] or not version_num.startswith('18'):
        write_status(base_status('REFUSED_VERSION_OR_IDENTITY_MISMATCH','database identity or PostgreSQL major mismatch',**target))
        return 7
    schema='maknow_dat008_'+secrets.token_hex(6)
    inputs=[LEARNER/'sql/01_schema.sql',LEARNER/'sql/02_seed.sql',*submissions]
    hashes={p.relative_to(ROOT).as_posix():sha256(p) for p in inputs}
    combined="\\set ON_ERROR_STOP on\n"+f'CREATE SCHEMA "{schema}";\nSET search_path TO "{schema}", public;\n'
    for p in inputs:
        combined += f"\n-- BEGIN {p.name}\n"+p.read_text()+f"\n-- END {p.name}\n"
    runtime_sql=RUNTIME_DIR/'runtime.sql'; runtime_sql.write_text(combined)
    stdout_path=RUNTIME_DIR/'postgresql.stdout.txt'; stderr_path=RUNTIME_DIR/'postgresql.stderr.txt'
    exec_result=None; cleanup=None
    try:
        exec_result=run_cmd([psql,'-X',url,'-v','ON_ERROR_STOP=1','-f',str(runtime_sql)],env,timeout=30)
        stdout_path.write_text(exec_result.stdout); stderr_path.write_text(exec_result.stderr)
    except subprocess.TimeoutExpired as e:
        stdout_path.write_text(e.stdout or ''); stderr_path.write_text((e.stderr or '')+'\nTIMEOUT\n')
    finally:
        cleanup=run_cmd([psql,'-X',url,'-v','ON_ERROR_STOP=1','-c',f'DROP SCHEMA IF EXISTS "{schema}" CASCADE;'],env,timeout=15)
    stdout=stdout_path.read_text()
    markers_ok=all(x in stdout for x in REQUIRED_MARKERS)
    execution_ok=exec_result is not None and exec_result.returncode==0
    cleanup_ok=cleanup.returncode==0
    receipt={
      'schema':'DAT008_RUNTIME_RECEIPT_v1','postgresql_version_num':version_num,'database_identity':db,
      'host_class':target['host_class'],'generated_schema':schema,'input_sha256':hashes,
      'execution_returncode':None if exec_result is None else exec_result.returncode,
      'required_markers':{m:(m in stdout) for m in REQUIRED_MARKERS},'cleanup_returncode':cleanup.returncode,
      'cleanup_status':'PASS' if cleanup_ok else 'FAIL','runtime_sql_sha256':sha256(runtime_sql),
      'stdout_sha256':sha256(stdout_path),'stderr_sha256':sha256(stderr_path)
    }
    receipt_path=RUNTIME_DIR/'runner-receipt.json'; receipt_path.write_text(json.dumps(receipt,indent=2)+"\n")
    claimable=execution_ok and cleanup_ok and markers_ok
    st=base_status('RAN_REAL_POSTGRESQL' if claimable else 'RUN_FAILED_NONCLAIMABLE',
                   'all runtime gates passed' if claimable else 'execution, evidence-marker, or cleanup gate failed',
                   postgresql_version=version_num,database_identity=db,host_class=target['host_class'],generated_schema=schema,
                   input_sha256=hashes,runtime_receipt='learner/evidence/runtime/runner-receipt.json',
                   cleanup_status='PASS' if cleanup_ok else 'FAIL')
    st['runtime_claimable']=claimable
    st['actual_postgresql_runtime_evidence_claimed']=claimable
    st['actual_match_rank_plan_evidence_claimed']=claimable
    write_status(st)
    print('PASS REAL POSTGRESQL RUNTIME' if claimable else 'NONCLAIMABLE RUNTIME FAILURE')
    return 0 if claimable else 8

if __name__=='__main__':
    raise SystemExit(main())
