from pathlib import Path
import json
STATE_VOCAB={'USE','PILOT','NOT_NOW','UNKNOWN','EVIDENCE_INCOMPLETE','OWNER_MISSING','REVIEW_REQUIRED','HOLD','PARTIAL','DISPUTED'}
POSITIVE={'USE','PILOT'}
AXES={'NATIVE_MOBILE','CUSTOM_BILLING','SAAS_MULTI_TENANCY'}

def _err(errs,rule,msg): errs.append({'rule':rule,'message':msg})
def _bindings(root):
 b=json.loads((root/'fixtures/FIXTURE_BINDINGS.json').read_text(encoding='utf-8')); out={}
 for x in b['fixtures']: out[x['fixture_id']]=json.loads((root/x['path']).read_text(encoding='utf-8'))
 return out

def _visible_rules(root): return json.loads((root/'CHECK_RULES.json').read_text(encoding='utf-8'))
def _classify(value,vocab):
 for state in ('present','absent','unresolved'):
  if value in vocab[state]: return state
 return 'invalid'

def _axis_trigger(axis,e,visible):
 vocabs=visible['trigger_field_vocabularies'][axis]; states={k:_classify(e.get(k),v) for k,v in vocabs.items()}
 invalid=[k for k,v in states.items() if v=='invalid']; contradiction=False
 if axis=='NATIVE_MOBILE':
  gap=states['product_capability_gap']; specifics=[states[k] for k in ('native_only_requirement','device_api_requirement','distribution_requirement','offline_requirement')]
  contradiction=(gap=='present' and all(x=='absent' for x in specifics)) or (gap=='absent' and any(x=='present' for x in specifics))
 elif axis=='CUSTOM_BILLING':
  gap=states['provider_capability_gap']; req=[states['billing_model'],states['invoice_tax_requirement']]
  contradiction=(gap=='absent' and any(x=='present' for x in req)) or (gap=='present' and all(x=='absent' for x in req))
 unresolved=any(v=='unresolved' for v in states.values())
 present=any(v=='present' for v in states.values())
 return {'states':states,'invalid':invalid,'contradiction':contradiction,'unresolved':unresolved,'present':present}

def validate_submission(submission,root=None):
 root=Path(root or Path(__file__).resolve().parent); errs=[]; fixtures=_bindings(root); visible=_visible_rules(root)
 fid=submission.get('fixture_id'); fixture=fixtures.get(fid)
 if fixture is None: _err(errs,'R3','Unknown fixture_id; choose a shipped fixture.'); return {'ok':False,'errors':errs,'fixture_id':fid}
 records=submission.get('records')
 if not isinstance(records,list) or len(records)!=3: _err(errs,'R1','Exactly three decision records are required.'); return {'ok':False,'errors':errs,'fixture_id':fid}
 axes=[r.get('decision_axis') for r in records]
 if set(axes)!=AXES or len(set(axes))!=3: _err(errs,'R1','Decision axes must be exactly NATIVE_MOBILE, CUSTOM_BILLING, SAAS_MULTI_TENANCY.')
 by={r.get('decision_axis'):r for r in records}
 for axis,r in by.items():
  if r.get('decision_state') not in STATE_VOCAB: _err(errs,'R2',f'{axis}: invalid decision_state.')
  if not isinstance(r.get('rationale'),str) or not r.get('rationale','').strip(): _err(errs,'R11',f'{axis}: rationale required.')
  if not r.get('review_due'): _err(errs,'R11',f'{axis}: review_due required.')
  if not r.get('stop_or_pivot_trigger'): _err(errs,'R11',f'{axis}: stop_or_pivot_trigger required.')
 mapping={'NATIVE_MOBILE':'native_mobile','CUSTOM_BILLING':'custom_billing','SAAS_MULTI_TENANCY':'saas_multi_tenancy'}
 for axis,key in mapping.items():
  if axis in by and by[axis].get('evidence') != fixture[key]: _err(errs,'R3',f'{axis}: evidence does not exactly match selected fixture.')
 for axis,key,rule in [('NATIVE_MOBILE','native_mobile','R4'),('CUSTOM_BILLING','custom_billing','R5'),('SAAS_MULTI_TENANCY','saas_multi_tenancy','R6')]:
  if axis not in by: continue
  r=by[axis]; e=r.get('evidence') or {}; st=r.get('decision_state'); trig=_axis_trigger(axis,e,visible)
  if trig['invalid'] or trig['contradiction']:
   why=('unrecognized fields '+','.join(trig['invalid'])) if trig['invalid'] else 'contradictory trigger evidence'
   _err(errs,'R13',f'{axis}: {why}; fail closed.')
  if e.get('authority_refs') != visible['authority_trace_requirements'][axis]: _err(errs,'R14',f'{axis}: authority_refs must exactly match the visible axis trace contract.')
  if st in POSITIVE:
   if trig['unresolved'] or trig['invalid'] or trig['contradiction'] or not trig['present']: _err(errs,rule,f'{axis}: positive state requires a measured present axis trigger with resolved vocabulary.')
   if not e.get('operational_owner'): _err(errs,'R7',f'{axis}: operational owner required for positive state.')
   for k in ['support_burden','migration_cost','rollback_path','evidence_ref']:
    if e.get(k) in (None,'','UNKNOWN'): _err(errs,'R10',f'{axis}: positive state requires {k}.')
   if axis=='CUSTOM_BILLING' and (not e.get('subscription_state_owner') or _classify(e.get('invoice_tax_requirement'),visible['trigger_field_vocabularies'][axis]['invoice_tax_requirement'])=='unresolved'): _err(errs,'R5','CUSTOM_BILLING: resolved state/invoice ownership evidence required.')
   if axis=='SAAS_MULTI_TENANCY' and (not e.get('tenant_definition') or not isinstance(e.get('tenant_count'),(int,float)) or e.get('tenant_count')<=0): _err(errs,'R6','SAAS_MULTI_TENANCY: tenant definition/count evidence required.')
  if trig['unresolved'] and st in POSITIVE|{'NOT_NOW'}: _err(errs,'R8',f'{axis}: unresolved decisive trigger requires typed uncertainty, not success/NOT_NOW.')
  if st=='NOT_NOW':
   if trig['present'] or trig['invalid'] or trig['contradiction']: _err(errs,'R9',f'{axis}: NOT_NOW requires a measured absent, internally consistent trigger.')
   if not e.get('simpler_alternative'): _err(errs,'R9',f'{axis}: NOT_NOW requires a visible simpler alternative.')
 return {'ok':not errs,'errors':errs,'fixture_id':fid,'rules_checked':[f'R{i}' for i in range(1,15)]}
