from __future__ import annotations
REQUIRED=['source_id','canonical_locator','etag','last_modified','fetch_status','content_hash','normalized_hash','previous_snapshot_ref','current_snapshot_ref','diff_type','semantic_change_class','affected_claim_ids','severity','review_state','review_owner','next_check_at','provenance']
UNKNOWN={'UNKNOWN','STALE','INCONCLUSIVE','UNVERIFIED','MISSING',''}

def empty(v): return v is None or (isinstance(v,str) and v.strip().upper() in UNKNOWN)
def claims(v):
    if v is None: return []
    if isinstance(v,str) and v.strip().upper() in UNKNOWN: return []
    if isinstance(v,list): return [str(x).strip() for x in v if str(x).strip() and str(x).strip().upper() not in UNKNOWN]
    return [x.strip() for x in str(v).split(',') if x.strip() and x.strip().upper() not in UNKNOWN]

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':'BASELINE_DECISION_REUSE_FORBIDDEN'}
    if empty(x.get('source_id')) or empty(x.get('canonical_locator')):return {'state':'HOLD_SOURCE_IDENTITY_UNKNOWN','reason':'SOURCE_ID_AND_LOCATOR_REQUIRED'}
    if empty(x.get('content_hash')) or empty(x.get('normalized_hash')):return {'state':'HOLD_CONTENT_IDENTITY_UNKNOWN','reason':'RAW_AND_NORMALIZED_CONTENT_IDENTITIES_REQUIRED'}
    if empty(x.get('diff_type')):return {'state':'HOLD_DIFF_TYPE_UNKNOWN','reason':'DIFF_TYPE_REQUIRED'}
    rs=str(x.get('review_state','')).upper()
    if rs not in {'REVIEW_REQUIRED','REVIEW_READY','PENDING_REVIEW'}:return {'state':'HOLD_REVIEW_STATE_INVALID','reason':'REVIEW_STATE_MUST_REMAIN_BOUNDED_AND_NON_APPROVING'}
    etag=x.get('etag'); lm=x.get('last_modified')
    if empty(etag) and empty(lm):return {'state':'HOLD_VALIDATOR_EVIDENCE_UNKNOWN','reason':'ETAG_OR_LAST_MODIFIED_REQUIRED'}
    try: status=int(x.get('fetch_status'))
    except Exception:return {'state':'HOLD_FETCH_FAILED','reason':'FETCH_STATUS_INVALID'}
    if status>=400 or status<200:return {'state':'HOLD_FETCH_FAILED','reason':'FETCH_NOT_SUCCESS_OR_NOT_MODIFIED','fetch_status':status}
    prev=x.get('previous_snapshot_ref'); cur=x.get('current_snapshot_ref')
    if empty(prev) or empty(cur) or (prev==cur and status!=304):return {'state':'HOLD_SNAPSHOT_LINEAGE_INVALID','reason':'PREVIOUS_CURRENT_SNAPSHOT_LINEAGE_REQUIRED'}
    if empty(x.get('provenance')):return {'state':'HOLD_PROVENANCE','reason':'PROVENANCE_REQUIRED'}
    owner=str(x.get('review_owner','')).strip().lower()
    if owner in {'','none','unassigned','missing','unknown'}:return {'state':'HOLD_REVIEW_OWNER','reason':'REVIEW_OWNER_REQUIRED'}
    if empty(x.get('next_check_at')):return {'state':'HOLD_NEXT_CHECK','reason':'NEXT_CHECK_REQUIRED'}
    sem=str(x.get('semantic_change_class','')).upper(); diff=str(x.get('diff_type','')).upper(); sev=str(x.get('severity','')).upper(); ac=claims(x.get('affected_claim_ids'))
    raw_changed=bool(x.get('raw_changed', x.get('previous_content_hash') not in (None,'') and x.get('previous_content_hash')!=x.get('content_hash')))
    norm_changed=bool(x.get('normalized_changed', x.get('previous_normalized_hash') not in (None,'') and x.get('previous_normalized_hash')!=x.get('normalized_hash')))
    if status==304:
        if sem not in {'NONE','NO_CHANGE'} or ac:return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'304_NOT_MODIFIED_CANNOT_CARRY_CLAIM_CHANGE'}
        return {'state':'NOT_MODIFIED_REVIEW_READY','reason':'CONDITIONAL_FETCH_CONFIRMED_NOT_MODIFIED'}
    if sem in UNKNOWN:return {'state':'HOLD_SEMANTIC_CLASS_UNKNOWN','reason':'SEMANTIC_CLASS_REQUIRED_FOR_FETCHED_CHANGE'}
    if sem=='CLAIM_AFFECTING':
        if not norm_changed and not x.get('independent_semantic_evidence'):return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'CLAIM_CHANGE_WITHOUT_NORMALIZED_OR_INDEPENDENT_SEMANTIC_EVIDENCE'}
        if not ac:return {'state':'HOLD_AFFECTED_CLAIMS_UNKNOWN','reason':'CLAIM_AFFECTING_REQUIRES_EXPLICIT_CLAIM_IDS'}
        if sev in {'','NONE','UNKNOWN'}:return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'CLAIM_AFFECTING_REQUIRES_NON_NONE_SEVERITY'}
        return {'state':'CLAIM_AFFECTING_CHANGE_REVIEW_REQUIRED','reason':'NORMALIZED_CHANGE_MAPPED_TO_AFFECTED_CLAIMS'}
    if sem in {'NON_SEMANTIC','NONE','NO_CHANGE'}:
        if ac or sev not in {'NONE','LOW'}:return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'NON_SEMANTIC_STATE_CANNOT_CARRY_CLAIM_IMPACT'}
        if norm_changed:return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'NORMALIZED_CONTENT_CHANGED_BUT_CLASSIFIED_NON_SEMANTIC_WITHOUT_EXPLANATION'}
        if empty(etag) and not empty(lm):return {'state':'VALIDATOR_DEGRADED_REVIEW_REQUIRED','reason':'LAST_MODIFIED_ONLY_AND_NO_SEMANTIC_CHANGE'}
        return {'state':'NON_SEMANTIC_CHANGE_REVIEW_REQUIRED','reason':'TRANSPORT_OR_MARKUP_CHANGE_DOES_NOT_CHANGE_RELEVANT_NORMALIZED_CONTENT'}
    if sem=='EQUIVALENT_RELOCATION':
        if ac or sev not in {'NONE','LOW'} or norm_changed:return {'state':'HOLD_SEMANTIC_CONTRADICTION','reason':'EQUIVALENT_RELOCATION_MUST_PRESERVE_RELEVANT_NORMALIZED_SEMANTICS'}
        return {'state':'SOURCE_RELOCATION_EQUIVALENCE_REVIEW_REQUIRED','reason':'LOCATOR_OR_MARKUP_CHANGED_WHILE_RELEVANT_SEMANTICS_REMAIN_EQUIVALENT'}
    return {'state':'HOLD_SEMANTIC_CLASS_UNKNOWN','reason':'UNSUPPORTED_SEMANTIC_CLASS'}
