"""Normative arithmetic and eligibility reference for specification 1.0.0.

Validate JSON against 06-Evidence-Pack.schema.json first. This pure evaluator
then applies semantic checks and precedence. It does not authenticate evidence,
grant real permissions, or establish safety. Pass an explicit evaluation time;
historical examples are evaluated at their recorded assessment time.
Run this file directly beside harbor-examples.json and policy-conformance.json to check the conformance cases. Copied from 09-Policy-Reference.py in the Edition 2 development pack; policy.js is the 1:1 port used by the site and the MCP server.
"""
import copy
import json
import math
from datetime import datetime, timedelta
from pathlib import Path

GEARS=('governance','equity','aligned_incentives','resilience','steering')
DIMENSIONS=('severity','irreversibility','scale','autonomy','power_concentration')
def timestamp(value):return datetime.fromisoformat(value.replace('Z','+00:00'))
def normalize_seconds(value,unit):
 factors={'milliseconds':0.001,'seconds':1,'minutes':60,'hours':3600,'days':86400}
 if isinstance(value,bool) or not isinstance(value,(int,float)) or not math.isfinite(value) or value<0 or unit not in factors:raise ValueError('Invalid duration or unit')
 return value*factors[unit]
def valid_range(v,positive=False):
 if not isinstance(v,dict):return False
 a=[v.get(k) for k in ('lower','central','upper')]
 return all(isinstance(x,(int,float)) and not isinstance(x,bool) and math.isfinite(x) and (x>0 if positive else x>=0) for x in a) and a[0]<=a[1]<=a[2]

def evaluate(p,as_of):
 now=timestamp(as_of);assessed=timestamp(p['assessed_at'])
 blocks=[];missing=[]
 def need(ok,code):
  if not ok:missing.append(code)
 ev={x['id']:x for x in p['evidence']}
 need(len(ev)==len(p['evidence']),'DUPLICATE_EVIDENCE_ID')
 for collection in ('critical_claims','hazards','breakers','actions'):
  ids=[x['id'] for x in p[collection]]
  need(len(ids)==len(set(ids)),'DUPLICATE_ID:'+collection)
 def evidence(ids,context):
  need(bool(ids),'MISSING_EVIDENCE:'+context)
  for i in ids:
   e=ev.get(i)
   need(e is not None,'UNKNOWN_EVIDENCE_ID:'+i)
   if e:
    need(e['validity_status']=='current','EVIDENCE_NOT_CURRENT:'+i)
    need(timestamp(e['captured_at'])<=assessed,'EVIDENCE_POSTDATES_ASSESSMENT:'+i)
    need(p['illustrative'] or e['source_kind']!='illustrative','ILLUSTRATIVE_EVIDENCE_IN_REAL_ASSESSMENT:'+i)
 need(p['spec_version']=='1.0.0' and p['schema_version']=='1.0.0','UNSUPPORTED_VERSION')
 need(now>=assessed,'ASSESSMENT_IN_FUTURE')
 need(bool(p['system']['version'].strip()) and bool(p['system']['environment'].strip()) and bool(p['system']['allowed_actions']),'SCOPE_MISSING')
 for c in p['critical_claims']:
  if c['status']=='invalidated':blocks.append('CRITICAL_CLAIM_INVALIDATED:'+c['id'])
  elif c['status']=='unknown':missing.append('CRITICAL_CLAIM_UNKNOWN:'+c['id'])
  else:evidence(c['evidence_ids'],c['id'])
 for h in p['hazards']:
  need(bool(h['rationale'].strip()),'HAZARD_RATIONALE_MISSING:'+h['id'])
  if h['unacceptable'] and h['controls_sufficient']=='no':blocks.append('UNCONTROLLED_HAZARD:'+h['id'])
  elif h['controls_sufficient']=='unknown':missing.append('HAZARD_CONTROL_UNKNOWN:'+h['id'])
  else:evidence(h['evidence_ids'],h['id'])
 for k in GEARS:
  g=p['gears'][k]
  if g['rating']=='red':blocks.append('RED_GEAR:'+k)
  if g['rating']=='unknown':missing.append('UNKNOWN_GEAR:'+k)
  else:
   need(bool(g['rationale'].strip()),'GEAR_RATIONALE_MISSING:'+k);evidence(g['evidence_ids'],k)
 for b in p['breakers']:
  if b['required']:
   if b['test_status']=='failed':blocks.append('REQUIRED_CONTROL_FAILED:'+b['id'])
   elif b['test_status']!='passed':missing.append('REQUIRED_CONTROL_UNTESTED:'+b['id'])
   need(b['tested_system_version']==p['system']['version'],'CONTROL_VERSION_MISMATCH:'+b['id'])
   need(all(str(b[k]).strip() for k in ('owner','trigger','action')),'CONTROL_DEFINITION_MISSING:'+b['id'])
   evidence(b['evidence_ids'],b['id'])
 timing=p['timing'];h=timing['h'];parts=list(timing['l_components'].values())
 need(all(timing[k].strip() for k in ('scenario','start_event','end_event')),'TIMING_BOUNDARY_MISSING')
 invalidated=h['status']=='invalidated' or any(c['status']=='invalidated' for c in p['critical_claims'])
 numeric=h['status']=='known' and valid_range(h['quantity']['estimate'],True) and all(valid_range(q['estimate']) for q in parts)
 ratio={'band':'invalidated' if invalidated else 'unknown','lower':None,'central':None,'upper':None,'l_seconds':None}
 if invalidated:blocks.append('OVERSIGHT_INVALIDATED')
 elif numeric:
  l={k:sum(q['estimate'][k] for q in parts) for k in ('lower','central','upper')};hv=h['quantity']['estimate']
  ratio.update(lower=l['lower']/hv['upper'],central=l['central']/hv['central'],upper=l['upper']/hv['lower'],l_seconds=l)
  ratio['band']='green' if ratio['upper']<0.25 else 'amber' if ratio['upper']<1 else 'red'
  if ratio['band']=='red':blocks.append('RATIO_RED')
  need(timestamp(p['review']['valid_until'])<=assessed+timedelta(seconds=hv['lower']),'REVIEW_EXCEEDS_HORIZON')
 else:missing.append('TIMING_UNKNOWN_OR_INVALID')
 for i,q in enumerate(parts+[h['quantity']]):
  if q['estimate'] is not None:
   need(q['basis']!='unknown' and bool(q['rationale'].strip()),'TIMING_BASIS_MISSING:'+str(i));evidence(q['evidence_ids'],'timing'+str(i))
  if q['basis']=='illustrative':need(p['illustrative'],'ILLUSTRATIVE_TIMING_IN_REAL_ASSESSMENT')
 s=p['stakes'];need(all(s[k] is not None for k in DIMENSIONS) and s['rights_affected'] is not None and s['rsi_relevant'] is not None,'STAKES_INCOMPLETE')
 need(bool(s['rationale'].strip()),'STAKES_RATIONALE_MISSING')
 independent=any(s[k] is not None and s[k]>=4 for k in DIMENSIONS) or s['rights_affected'] is True or s['rsi_relevant'] is True
 r=p['review'];need(bool(r['accountable_owner'].strip()) and bool(r['assessor'].strip()),'OWNER_OR_ASSESSOR_MISSING')
 need(r['scope_matches'],'REVIEW_SCOPE_MISMATCH')
 need(timestamp(r['valid_until'])>now,'ASSESSMENT_EXPIRED')
 need(p['reassessment']['due_at']==r['valid_until'],'REASSESSMENT_DATE_MISMATCH')
 if independent:need(r['independent_status']=='completed' and bool(r['independent_reviewer']) and r['independent_reviewer'] not in (r['accountable_owner'],r['assessor']),'INDEPENDENT_REVIEW_REQUIRED')
 human=p['decision']['human']
 if human['status'] in ('granted','granted_with_conditions'):
  need(bool(human['recorded_by']) and bool(human['recorded_at']) and bool(human['expires_at']) and bool(human['rationale'].strip()),'AUTHORIZATION_RECORD_INCOMPLETE')
  if human['expires_at']:need(now<timestamp(human['expires_at'])<=timestamp(r['valid_until']),'AUTHORIZATION_EXPIRY_INVALID')
  if human['recorded_at']:need(assessed<=timestamp(human['recorded_at'])<=now,'AUTHORIZATION_DATE_INVALID')
 conditional=ratio['band']=='amber' or any(p['gears'][k]['rating']=='yellow' for k in GEARS)
 if conditional:
  need(bool(p['actions']),'CONDITIONS_MISSING')
  covered={x for a in p['actions'] for x in a['addresses']}
  for k in GEARS:
   if p['gears'][k]['rating']=='yellow':need('gear:'+k in covered,'CONDITION_MISSING_FOR:'+k)
  if ratio['band']=='amber':need('ratio:amber' in covered,'CONDITION_MISSING_FOR:ratio:amber')
  for a in p['actions']:
   need(all(a[k].strip() for k in ('description','owner','verification_test')),'CONDITION_INCOMPLETE:'+a['id'])
   need(a['completed'] or timestamp(a['due_at'])>now,'CONDITION_OVERDUE:'+a['id'])
 if blocks:status='blocked'
 elif missing:status='insufficient_evidence'
 elif conditional:status='eligible_with_conditions'
 else:status='eligible_for_scoped_permission'
 if human['status'] in ('granted','granted_with_conditions') and status in ('blocked','insufficient_evidence'):missing.append('RECORDED_PERMISSION_UNSUPPORTED')
 if conditional and human['status']=='granted':missing.append('CONDITIONAL_PERMISSION_REQUIRED');status='blocked' if blocks else 'insufficient_evidence'
 return {'advisory':status,'ratio':ratio,'reason_codes':sorted(set(blocks+missing)),'independent_review_required':independent,'human_authorization':human['status'],'illustrative':p['illustrative']}

def set_path(p,path,value):
 keys=path.split('.');cur=p
 for k in keys[:-1]:cur=cur[int(k)] if isinstance(cur,list) else cur[k]
 if isinstance(cur,list):cur[int(keys[-1])]=value
 else:cur[keys[-1]]=value

def run_fixtures():
 root=Path(__file__).parent;examples=json.loads((root/'harbor-examples.json').read_text());by_id={x['assessment_id']:x for x in examples};suite=json.loads((root/'policy-conformance.json').read_text())
 for c in suite['cases']:
  p=copy.deepcopy(by_id[c['base']])
  for path,v in c['set'].items():set_path(p,path,v)
  actual=evaluate(p,c['as_of']);e=c['expected']
  assert actual['advisory']==e['advisory'],(c['id'],actual,e)
  assert actual['ratio']['band']==e['band'],(c['id'],actual,e)
  if 'central' in e:assert math.isclose(actual['ratio']['central'],e['central'],abs_tol=1e-12),(c['id'],actual,e)
 for c in suite['unit_cases']:assert normalize_seconds(c['value'],c['unit'])==c['expected_seconds']
 for value,unit in [(-1,'seconds'),(float('nan'),'hours'),(float('inf'),'minutes'),(True,'seconds'),(1,'fortnights')]:
  try:normalize_seconds(value,unit)
  except ValueError:pass
  else:raise AssertionError('Invalid duration accepted')
 print(json.dumps({'policy_cases_passed':len(suite['cases']),'unit_cases_passed':len(suite['unit_cases']),'invalid_duration_cases_passed':5}))
 return examples

if __name__=='__main__':run_fixtures()
