from __future__ import annotations
from datetime import date,datetime
REQUIRED=['learner_id','competency_id','evidence_id','event_time','evidence_type','context_id','independence_level','quality_state','mastery_state_before','transition_rule_id','mastery_state_after','confidence_band','contradiction_state','staleness_state','review_due','provenance']
UNKNOWN={'UNKNOWN','MISSING','UNVERIFIED','INCONCLUSIVE',''}
STATE={'UNKNOWN','DEVELOPING','PROVISIONAL','MASTERED','HOLD','INCONCLUSIVE','CONTRADICTED','REVIEW_REQUIRED'}
def empty(v): return v is None or (isinstance(v,str) and v.strip().upper() in UNKNOWN)
def parse_dt(v):
    try: datetime.fromisoformat(str(v).replace('Z','+00:00')); return True
    except Exception:return False
def parse_date(v):
    try: date.fromisoformat(str(v)); return True
    except Exception:return False
def evaluate(x):
    missing=[k for k in REQUIRED if k not in x]
    if missing:return {'state':'HOLD_INSUFFICIENT_EVIDENCE','reason':'MISSING_FIELDS','missing':missing}
    if x.get('baseline_oracle_reuse'):return {'state':'HOLD_BASELINE_ORACLE_REUSE','reason':'TRANSFER_MUST_RECOMPUTE'}
    if empty(x['learner_id']) or empty(x['competency_id']) or empty(x['evidence_id']):return {'state':'HOLD_IDENTITY','reason':'TRACEABLE_IDENTITIES_REQUIRED'}
    if not parse_dt(x['event_time']):return {'state':'HOLD_EVENT_TIME','reason':'VALID_EVENT_TIME_REQUIRED'}
    et=str(x['evidence_type']).upper()
    if et not in {'ACTIVITY_ONLY','ASSESSMENT','TRANSFER_TASK','PROJECT_ARTIFACT','OBSERVATION'}:return {'state':'HOLD_EVIDENCE_TYPE','reason':'TYPED_EVIDENCE_REQUIRED'}
    if empty(x['context_id']):return {'state':'HOLD_CONTEXT','reason':'CONTEXT_REQUIRED'}
    ind=str(x['independence_level']).upper()
    if ind not in {'GUIDED','PARTIAL','INDEPENDENT'}:return {'state':'HOLD_INDEPENDENCE','reason':'KNOWN_INDEPENDENCE_REQUIRED'}
    q=str(x['quality_state']).upper()
    if q not in {'WEAK','MEDIUM','STRONG'}:return {'state':'HOLD_QUALITY','reason':'KNOWN_QUALITY_REQUIRED'}
    sb=str(x['mastery_state_before']).upper(); sa=str(x['mastery_state_after']).upper()
    if sb not in STATE or sa not in STATE:return {'state':'HOLD_MASTERY_STATE','reason':'TYPED_STATE_REQUIRED'}
    if empty(x['transition_rule_id']):return {'state':'HOLD_TRANSITION_RULE','reason':'RULE_ID_REQUIRED'}
    conf=str(x['confidence_band']).upper()
    if conf not in {'LOW','MEDIUM','HIGH'}:return {'state':'HOLD_CONFIDENCE','reason':'KNOWN_CONFIDENCE_REQUIRED'}
    con=str(x['contradiction_state']).upper()
    if con not in {'NONE','OPEN','RESOLVED'}:return {'state':'HOLD_CONTRADICTION_STATE','reason':'TYPED_CONTRADICTION_REQUIRED'}
    stale=str(x['staleness_state']).upper()
    if stale not in {'FRESH','AGING','STALE','MIXED'}:return {'state':'HOLD_STALENESS','reason':'TYPED_STALENESS_REQUIRED'}
    if not parse_date(x['review_due']):return {'state':'HOLD_REVIEW_DUE','reason':'VALID_REVIEW_DATE_REQUIRED'}
    if empty(x['provenance']):return {'state':'HOLD_PROVENANCE','reason':'TRACEABLE_PROVENANCE_REQUIRED'}
    if x.get('transfer_context_changed') and not x.get('decision_recomputed'):return {'state':'HOLD_TRANSFER_NOT_RECOMPUTED','reason':'CHANGED_CONTEXT_REQUIRES_RECOMPUTATION'}
    dims=int(x.get('evidence_dimension_count',1))
    if et=='ACTIVITY_ONLY' and sa in {'PROVISIONAL','MASTERED'}:return {'state':'HOLD_ACTIVITY_ONLY_PROMOTION','reason':'ACTIVITY_EVENT_CANNOT_PROMOTE_MASTERY'}
    if con=='OPEN':
        if sa not in {'CONTRADICTED','REVIEW_REQUIRED','HOLD'}:return {'state':'HOLD_CONTRADICTION_ERASURE','reason':'OPEN_CONTRADICTION_MUST_REMAIN_VISIBLE'}
        return {'state':'MASTERY_REOPEN_CONTRADICTION','reason':'NEW_CONTRADICTORY_EVIDENCE_REOPENS_STATE'}
    if stale=='STALE' or (stale=='MIXED' and x.get('all_core_support_stale')):
        if sa=='MASTERED':return {'state':'HOLD_STALE_MASTERY','reason':'STALE_CORE_EVIDENCE_CANNOT_SILENTLY_PRESERVE_MASTERY'}
        return {'state':'MASTERY_REVIEW_STALE','reason':'CORE_SUPPORT_REQUIRES_REFRESH'}
    if dims < 3 and sa=='MASTERED':return {'state':'HOLD_SPARSE_DIMENSIONS','reason':'ONE_STRONG_ARTIFACT_CANNOT_ERASE_MISSING_DIMENSIONS'}
    if sa=='MASTERED' and (ind!='INDEPENDENT' or q!='STRONG' or conf!='HIGH'):return {'state':'HOLD_MASTERY_OVERCLAIM','reason':'MASTERED_REQUIRES_STRONG_INDEPENDENT_HIGH_CONFIDENCE_EVIDENCE'}
    if et=='ACTIVITY_ONLY':return {'state':'MASTERY_DEVELOPING','reason':'ACTIVITY_SIGNAL_RECORDED_WITHOUT_MASTERY_PROMOTION'}
    if sa in {'DEVELOPING','PROVISIONAL','HOLD','INCONCLUSIVE','REVIEW_REQUIRED'}:return {'state':'MASTERY_DEVELOPING','reason':'BOUNDED_NONFINAL_STATE_PRESERVED'}
    if sa=='MASTERED' and dims>=3:return {'state':'MASTERY_EVIDENCE_REVIEW_READY','reason':'MULTIDIMENSIONAL_EVIDENCE_READY_FOR_SEPARATE_REVIEW'}
    return {'state':'MASTERY_DEVELOPING','reason':'BOUNDED_STATE'}
