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={'MICROSERVICES','FULL_OFFLINE_STUDIO'}
UNRESOLVED={None,'','UNKNOWN','UNMEASURED','UNRESOLVED','NOT_ASSESSED','INCOMPLETE','CONTRADICTORY'}
ABSENT={'ABSENT','NONE','NOT_REQUIRED','NOT_MEASURED','BRIEF_OUTAGE_ONLY','ABSENT_AUTHORITATIVE_WRITE','WEAK_OR_UNSTABLE'}

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 _ms_trigger(e):
    decisive=[e.get('bounded_context_evidence'),e.get('independent_deploy_need'),e.get('independent_scale_need'),e.get('failure_isolation_need')]
    unresolved=any(v in UNRESOLVED for v in decisive)
    bounded=e.get('bounded_context_evidence')=='CLEAR_STABLE'
    pressure=any(e.get(k)=='PRESENT' for k in ['independent_deploy_need','independent_scale_need','failure_isolation_need'])
    present=bounded and pressure
    measured_absent=(not unresolved and not present and all(e.get(k)=='ABSENT' for k in ['independent_deploy_need','independent_scale_need','failure_isolation_need']))
    return unresolved,present,measured_absent

def _offline_trigger(e):
    decisive=[e.get('offline_duration_requirement'),e.get('offline_write_requirement'),e.get('sync_requirement'),e.get('conflict_policy')]
    unresolved=any(v in UNRESOLVED for v in decisive)
    duration=e.get('offline_duration_requirement') not in {None,'','UNKNOWN','UNMEASURED','BRIEF_OUTAGE_ONLY','NONE','ABSENT'}
    writes=e.get('offline_write_requirement')=='PRESENT_AUTHORITATIVE_LOCAL_EDIT'
    sync=e.get('sync_requirement')=='PRESENT'
    conflict=e.get('conflict_policy') not in UNRESOLVED|{'NOT_REQUIRED_WITHOUT_AUTHORITATIVE_WRITE'}
    present=duration and writes and sync and conflict
    measured_absent=(not unresolved and not present and e.get('offline_write_requirement')=='ABSENT_AUTHORITATIVE_WRITE' and e.get('sync_requirement')=='ABSENT')
    return unresolved,present,measured_absent


def _positive_vocab(root):
    rules=json.loads((root/'CHECK_RULES.json').read_text(encoding='utf-8'))
    return rules.get('positive_evidence_vocabulary',{})

def _allowed(vocab, axis, field, value):
    allowed=vocab.get(axis,{}).get(field,[])
    return value in allowed

def _authority_ok(axis, refs):
    if not isinstance(refs,list) or not refs: return False
    refs=set(refs)
    if axis=='MICROSERVICES':
        return {'source://AWR-004/MSMICRO','lesson://AWR-004#claim/AWR-004-K1','lesson://AWR-004#claim/AWR-004-K4'} <= refs
    return {'source://AWR-004/SW','source://AWR-004/IDB','lesson://AWR-004#claim/AWR-004-K2','lesson://AWR-004#claim/AWR-004-K3','lesson://AWR-004#claim/AWR-004-K4'} <= refs

def validate_submission(submission,root=None):
    root=Path(root or Path(__file__).resolve().parent); errs=[]; fixtures=_bindings(root); positive_vocab=_positive_vocab(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)!=2:
        _err(errs,'R1','Exactly two 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))!=2:
        _err(errs,'R1','Decision axes must be exactly MICROSERVICES and FULL_OFFLINE_STUDIO.')
    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,'R10',f'{axis}: rationale required.')
        if not r.get('review_due'): _err(errs,'R10',f'{axis}: review_due required.')
        if not r.get('stop_or_pivot_trigger'): _err(errs,'R10',f'{axis}: stop_or_pivot_trigger required.')
    mapping={'MICROSERVICES':'microservices','FULL_OFFLINE_STUDIO':'full_offline_studio'}
    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,trig,rule in [('MICROSERVICES','microservices',_ms_trigger,'R4'),('FULL_OFFLINE_STUDIO','full_offline_studio',_offline_trigger,'R5')]:
        if axis not in by: continue
        r=by[axis]; e=r.get('evidence') or {}; st=r.get('decision_state'); unresolved,present,measured_absent=trig(e)
        if st in POSITIVE:
            if unresolved or not present: _err(errs,rule,f'{axis}: positive state requires its own measured present trigger.')
            if axis=='MICROSERVICES':
                if not _allowed(positive_vocab,axis,'distributed_transaction_risk',e.get('distributed_transaction_risk')):
                    _err(errs,'R4','MICROSERVICES: positive state requires an admissible measured distributed_transaction_risk.')
                if not _allowed(positive_vocab,axis,'operational_maturity',e.get('operational_maturity')):
                    _err(errs,'R4','MICROSERVICES: positive state requires visible operational readiness for distributed obligations.')
            if axis=='FULL_OFFLINE_STUDIO':
                for k in ['offline_duration_requirement','offline_write_requirement','sync_requirement','conflict_policy','update_strategy','recovery_strategy','security_boundary']:
                    if not _allowed(positive_vocab,axis,k,e.get(k)):
                        _err(errs,'R5',f'FULL_OFFLINE_STUDIO: positive state requires admissible measured {k}.')
            if not e.get('operational_owner'): _err(errs,'R6',f'{axis}: accountable owner required for positive state.')
            for k in ['migration_cost','failure_surface','chapter_exit_evidence']:
                if e.get(k) in UNRESOLVED: _err(errs,'R9',f'{axis}: positive state requires {k}.')
            if not _authority_ok(axis,e.get('authority_refs')): _err(errs,'R11',f'{axis}: authority_refs do not satisfy required FACT/DESIGN trace.')
        if unresolved and st in POSITIVE|{'NOT_NOW'}:
            _err(errs,'R7',f'{axis}: unresolved decisive trigger requires typed uncertainty, not success/NOT_NOW.')
        if st=='NOT_NOW':
            if not measured_absent: _err(errs,'R8',f'{axis}: NOT_NOW requires measured trigger absence.')
            if not e.get('simpler_alternative'): _err(errs,'R8',f'{axis}: NOT_NOW requires a visible simpler alternative.')
        if st not in POSITIVE and not _authority_ok(axis,e.get('authority_refs')):
            _err(errs,'R11',f'{axis}: authority_refs required for reproducible FACT/DESIGN trace.')
    return {'ok':not errs,'errors':errs,'fixture_id':fid,'rules_checked':[f'R{i}' for i in range(1,13)]}
