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'}
UNCERTAINTY = {'UNKNOWN','EVIDENCE_INCOMPLETE','OWNER_MISSING','REVIEW_REQUIRED','HOLD','PARTIAL','DISPUTED'}
AXES = {'PORTFOLIO_CONTROL','CREATOR_MARKETPLACE'}


def _load_bindings(root: Path):
    bindings=json.loads((root/'fixtures/FIXTURE_BINDINGS.json').read_text(encoding='utf-8'))
    out={}
    for item in bindings['fixtures']:
        p=root/item['path']
        out[item['fixture_id']]=json.loads(p.read_text(encoding='utf-8'))
    return out


def _err(errors, rule, message):
    errors.append({'rule':rule,'message':message})


def validate_submission(submission, root=None):
    root=Path(root or Path(__file__).resolve().parent)
    errors=[]
    fixtures=_load_bindings(root)
    fixture_id=submission.get('fixture_id')
    fixture=fixtures.get(fixture_id)
    if fixture is None:
        _err(errors,'R3','Unknown fixture_id; choose a shipped fixture.')
        return {'ok':False,'errors':errors,'fixture_id':fixture_id}

    records=submission.get('records')
    if not isinstance(records,list) or len(records)!=2:
        _err(errors,'R1','Exactly two decision records are required.')
        return {'ok':False,'errors':errors,'fixture_id':fixture_id}
    axes=[r.get('decision_axis') for r in records]
    if set(axes)!=AXES or len(set(axes))!=2:
        _err(errors,'R1','Decision axes must be exactly PORTFOLIO_CONTROL and CREATOR_MARKETPLACE.')
    by_axis={r.get('decision_axis'):r for r in records}

    for axis,record in by_axis.items():
        state=record.get('decision_state')
        if state not in STATE_VOCAB:
            _err(errors,'R2',f'{axis}: decision_state is outside the visible vocabulary.')
        if not record.get('review_due'):
            _err(errors,'R13',f'{axis}: review_due is required.')
        if not record.get('stop_or_pivot_trigger'):
            _err(errors,'R13',f'{axis}: stop_or_pivot_trigger is required.')
        rationale=record.get('rationale')
        if not isinstance(rationale,str) or not rationale.strip():
            _err(errors,'R15',f'{axis}: a non-empty learner-authored rationale is required.')

    # R3 canonical fixture schema + exact fixture binding.
    fixture_p=fixture.get('portfolio_control') or {}
    fixture_m=fixture.get('creator_marketplace') or {}
    portfolio_required=('problem','revision_id','evidence_ref')
    marketplace_required=('problem','evidence_ref')
    bad_p=[k for k in portfolio_required if fixture_p.get(k) in (None,'')]
    bad_m=[k for k in marketplace_required if fixture_m.get(k) in (None,'')]
    legacy=[]
    for axis_name,obj in (('PORTFOLIO_CONTROL',fixture_p),('CREATOR_MARKETPLACE',fixture_m)):
        if 'evidence_refs' in obj: legacy.append(f'{axis_name}.evidence_refs')
    if 'representative_revision_id' in fixture_p: legacy.append('PORTFOLIO_CONTROL.representative_revision_id')
    if bad_p or bad_m or legacy:
        detail=[]
        if bad_p: detail.append('PORTFOLIO_CONTROL missing '+', '.join(bad_p))
        if bad_m: detail.append('CREATOR_MARKETPLACE missing '+', '.join(bad_m))
        if legacy: detail.append('legacy aliases '+', '.join(legacy))
        _err(errors,'R3','Selected shipped fixture does not implement the canonical evidence schema: '+'; '.join(detail))

    # R3 exact fixture binding: evidence is copied from the selected shipped fixture, decisions are learner-authored.
    p_rec=by_axis.get('PORTFOLIO_CONTROL')
    m_rec=by_axis.get('CREATOR_MARKETPLACE')
    if p_rec is not None and p_rec.get('evidence') != fixture['portfolio_control']:
        _err(errors,'R3','PORTFOLIO_CONTROL evidence does not exactly match the selected shipped fixture.')
    if m_rec is not None and m_rec.get('evidence') != fixture['creator_marketplace']:
        _err(errors,'R3','CREATOR_MARKETPLACE evidence does not exactly match the selected shipped fixture.')

    if p_rec is not None:
        p=p_rec.get('evidence') or {}; st=p_rec.get('decision_state')
        if st in POSITIVE:
            required=['problem','revision_id','provenance_ref','approval_state','promotion_need','operational_owner','operational_ownership_cost','simpler_alternative','evidence_ref']
            missing=[k for k in required if p.get(k) in (None,'','UNKNOWN')]
            if missing: _err(errors,'R4','Positive Portfolio Control state lacks: '+', '.join(missing))
        if st=='NOT_NOW':
            promotion_need=p.get('promotion_need')
            unresolved={None,'','UNKNOWN','UNMEASURED','UNRESOLVED'}
            measured_absent={'NONE','ABSENT','NO_MEASURED_NEED','NO_CURRENT_NEED','NOT_REQUIRED'}
            if promotion_need in unresolved:
                _err(errors,'R12','PORTFOLIO_CONTROL NOT_NOW requires a measured absent promotion/control trigger; unresolved trigger evidence needs a typed uncertainty state.')
            elif promotion_need not in measured_absent:
                _err(errors,'R12','PORTFOLIO_CONTROL NOT_NOW is invalid when the promotion/control trigger is measured present.')

    if m_rec is not None:
        m=m_rec.get('evidence') or {}; st=m_rec.get('decision_state')
        role=m.get('marketplace_role')
        transactional=role=='PROPOSED_TRANSACTIONAL_MARKETPLACE'
        decisive_unknown=(m.get('buyer_requests_90d') is None or m.get('buyer_demand') in (None,'UNKNOWN') or m.get('transaction_frequency') in (None,'UNKNOWN') or m.get('transaction_need') in (None,'UNKNOWN'))
        trigger_present=(m.get('buyer_demand') in {'STRONG_MEASURED','MODERATE_MEASURED'} and m.get('transaction_frequency') in {'HIGH_REPEATED','MODERATE'} and m.get('transaction_need')=='MEASURED')
        if decisive_unknown and st in POSITIVE:
            _err(errors,'R11','Missing decisive demand/transaction evidence cannot produce USE or PILOT.')
        if st=='NOT_NOW':
            if decisive_unknown:
                _err(errors,'R12','CREATOR_MARKETPLACE NOT_NOW requires measured absence of the buyer/transaction trigger; unresolved trigger evidence needs a typed uncertainty state.')
            elif trigger_present:
                _err(errors,'R12','CREATOR_MARKETPLACE NOT_NOW is invalid when buyer demand and repeated transaction need are measured present; missing readiness evidence must remain typed rather than being collapsed to NOT_NOW.')
        if st in POSITIVE and transactional:
            creator_count=m.get('creator_count')
            creator_supply_ok=isinstance(creator_count,(int,float)) and not isinstance(creator_count,bool) and creator_count>0
            if m.get('buyer_demand') not in {'STRONG_MEASURED','MODERATE_MEASURED'} or m.get('transaction_frequency') not in {'HIGH_REPEATED','MODERATE'} or m.get('transaction_need') not in {'MEASURED'} or not creator_supply_ok:
                _err(errors,'R5','Positive transactional Marketplace state requires measured buyer demand, repeated transaction need, and a known positive creator_count.')
            if not m.get('payment_flow') or not m.get('payout_owner'):
                _err(errors,'R6','Positive transactional Marketplace state requires payment_flow and payout_owner.')
            rights=m.get('rights_evidence')
            if rights != 'COMPLETE':
                _err(errors,'R7','Positive transactional Marketplace state requires rights_evidence exactly COMPLETE for the proposed transaction role.')
            if not m.get('moderation_owner') or not m.get('dispute_owner'):
                _err(errors,'R8','Positive transactional Marketplace state requires moderation_owner and dispute_owner.')
            if m.get('fraud_risk') in (None,'','UNASSESSED','UNKNOWN') or m.get('trust_signal') in (None,'','LIMITED','UNKNOWN') or not m.get('operational_owner') or not m.get('operational_ownership_cost'):
                _err(errors,'R9','Positive transactional Marketplace state requires fraud/trust evidence plus operational owner/cost.')
            if st=='USE' and m.get('simpler_alternative_serves_core_demand') is not False:
                _err(errors,'R10','Transactional USE requires evidence that the simpler alternative no longer serves core demand.')
        if st=='USE' and role=='INVITE_ONLY_REFERRAL':
            _err(errors,'R10','Invite-only referral is a bounded alternative/pilot, not full transactional Marketplace USE.')

    return {'ok':not errors,'errors':errors,'fixture_id':fixture_id,'rules_checked':[f'R{i}' for i in range(1,16)]}
