CRITICAL=['lead_id','fit_score','intent_score','authority_signal','timing_days','serviceability_state','score_version','evidence_ref']
ALLOWED={'QUALIFIED','NURTURE','DISQUALIFIED','HOLD','PARTIAL','DISPUTED','STALE','BLOCKED'}
WEIGHTS={'fit':0.40,'intent':0.30,'authority':0.20,'timing':0.10}

def evaluate(case):
    missing=[k for k in CRITICAL if case.get(k) in (None,'',[],'UNKNOWN')]
    version_valid=not bool(case.get('weights_changed_without_version'))
    stale=case.get('evidence_age_days',0)>case.get('max_evidence_age_days',30)
    service=case.get('serviceability_state')
    hard=bool(case.get('hard_disqualifier'))
    authority=case.get('authority_signal')
    fit=float(case.get('fit_score') or 0); intent=float(case.get('intent_score') or 0); timing=float(case.get('timing_days') or 999)
    auth_numeric=100.0 if authority=='YES' else 0.0
    timing_numeric=100.0 if timing<=90 else (60.0 if timing<=180 else 20.0)
    advisory=round(fit*WEIGHTS['fit']+intent*WEIGHTS['intent']+auth_numeric*WEIGHTS['authority']+timing_numeric*WEIGHTS['timing'],2)
    gate_pass=(not hard and service=='SERVICEABLE')
    confidence='LOW' if missing else ('MEDIUM' if case.get('confidence')=='MEDIUM' else 'HIGH')
    state='HOLD'; reason='INSUFFICIENT_EVIDENCE'
    if case.get('blocked_by_owner_policy'):
        state='BLOCKED';reason='OWNER_OR_POLICY_BLOCK'
    elif missing:
        state='HOLD';reason='CRITICAL_EVIDENCE_MISSING'
    elif stale:
        state='STALE';reason='EVIDENCE_STALE'
    elif not version_valid:
        state='DISPUTED';reason='WEIGHT_CHANGE_UNVERSIONED'
    elif case.get('material_change_not_recomputed'):
        state='PARTIAL';reason='MATERIAL_CHANGE_RECOMPUTE_REQUIRED'
    elif hard:
        state='DISQUALIFIED';reason='HARD_DISQUALIFIER'
    elif service!='SERVICEABLE':
        state='DISQUALIFIED';reason='SERVICEABILITY_GATE_FAIL'
    elif authority=='NO':
        state='NURTURE';reason='BUYING_AUTHORITY_NOT_PRESENT'
    elif fit<70:
        state='NURTURE';reason='FIT_BELOW_DECLARED_MINIMUM'
    elif intent<60:
        state='NURTURE';reason='INTENT_BELOW_DECLARED_MINIMUM'
    elif timing>90:
        state='NURTURE';reason='TIMING_OUTSIDE_READY_WINDOW'
    else:
        state='QUALIFIED';reason='VERSIONED_CRITERIA_MET'
    recompute_required=bool(case.get('material_change_not_recomputed')) or not version_valid or stale
    return {'qualification_state':state,'reason_code':reason,'advisory_score':advisory,'hard_gate_pass':gate_pass,'missing_critical_count':len(missing),'confidence':confidence,'score_version_valid':version_valid,'recompute_required':recompute_required}
