from __future__ import annotations
from datetime import date,timedelta
import random, math

def _valid_tri(d):
    return isinstance(d,dict) and d.get('type')=='triangular' and all(isinstance(d.get(k),(int,float)) for k in ('min','mode','max')) and d['min']<=d['mode']<=d['max'] and d['max']>d['min']

def _percentile(vals,q):
    s=sorted(vals); idx=max(0,min(len(s)-1,math.ceil(q*len(s))-1)); return s[idx]

def _simulate(x):
    seed=1601 + int(x.get('forecast_revision',0))*100
    rng=random.Random(seed); runs=int(x['simulation_runs']); vals=[]
    for _ in range(runs):
        days=sum(rng.triangular(d['min'],d['max'],d['mode']) for d in x['duration_distribution'])
        for r in x['discrete_risk_ref']:
            if rng.random() < r['probability']:
                imp=r['impact']; days += rng.triangular(imp['min'],imp['max'],imp['mode'])
        vals.append(days)
    p50=_percentile(vals,0.50); p80=_percentile(vals,0.80)
    start=date.fromisoformat(x['forecast_start'])
    def dstr(v): return (start+timedelta(days=math.ceil(v))).isoformat()
    # Simple transparent sensitivity proxy: uncertainty span weighted by risk probability for risk items.
    drivers=[]
    for d in x['duration_distribution']: drivers.append((d['max']-d['min'],d['task']))
    for r in x['discrete_risk_ref']:
        imp=r['impact']; drivers.append((r['probability']*(imp['max']-imp['min']+imp['mode']),r['risk']))
    drivers.sort(reverse=True)
    return {'runs':runs,'p50_days':round(p50,3),'p80_days':round(p80,3),'p50_date':dstr(p50),'p80_date':dstr(p80),'schedule_margin_days':round(max(0,p80-p50),3),'sensitivity_driver':drivers[0][1] if drivers else None,'distribution_summary':{'min_days':round(min(vals),3),'max_days':round(max(vals),3)}}

def evaluate(x):
    if x.get('baseline_oracle_reuse') or not x.get('recomputed_from_changed_evidence',True): return {'state':'HOLD_BASELINE_ORACLE_REUSE'}
    if not x.get('forecast_id') or not x.get('baseline_ref') or not x.get('work_package_ref'): return {'state':'HOLD_SCOPE_IDENTITY'}
    ds=x.get('duration_distribution') or []
    if not ds or any(not _valid_tri(d) for d in ds): return {'state':'HOLD_UNCERTAINTY_MODEL'}
    if not x.get('distribution_basis'): return {'state':'HOLD_DISTRIBUTION_BASIS'}
    taskids={d['task'] for d in ds}
    if not x.get('dependency_integrity',False): return {'state':'HOLD_DEPENDENCY'}
    for e in x.get('dependency_ref',[]):
        if e.get('from') not in taskids or e.get('to') not in taskids: return {'state':'HOLD_DEPENDENCY'}
    risks=x.get('discrete_risk_ref') or []
    for r in risks:
        if not isinstance(r.get('probability'),(int,float)) or not (0<=r['probability']<=1) or not _valid_tri(r.get('impact')): return {'state':'HOLD_RISK_MODEL'}
    if not x.get('simulation_model_ref') or not isinstance(x.get('simulation_runs'),int) or x['simulation_runs']<1000: return {'state':'HOLD_MODEL_EVIDENCE'}
    if x.get('deterministic_date_only'): return {'state':'HOLD_OUTPUT_CONTRACT'}
    if not x.get('sensitivity_candidates'): return {'state':'HOLD_SENSITIVITY'}
    if x.get('evidence_change_requires_revision') and not x.get('reforecast_performed'): return {'state':'REFORECAST_REQUIRED'}
    if not x.get('review_owner') or not x.get('provenance'): return {'state':'HOLD_AUDIT_PROVENANCE'}
    result=_simulate(x)
    if result['p80_date'] < result['p50_date']: return {'state':'HOLD_OUTPUT_CONTRACT'}
    return {'state':'FORECAST_REVIEW_REQUIRED',**result}

def inspect_transfer(x):
    out=[]
    if x.get('baseline_oracle_reuse'): out.append('BASELINE_ORACLE_REUSE_FORBIDDEN')
    if not x.get('recomputed_from_changed_evidence'): out.append('TRANSFER_MUST_RECOMPUTE')
    return out
