from __future__ import annotations
from datetime import datetime, timezone
REQUIRED=['job_id','schedule_type','schedule_expression','timezone','last_run_at','next_run_at','missed_run_policy','jitter_window','lease_id','lease_owner','lease_expires_at','idempotency_key','attempt','retry_budget','backoff_state','run_state','result_ref','review_owner']
UNKNOWN={'UNKNOWN','STALE','INCONCLUSIVE','UNVERIFIED','MISSING',''}

def dt(v):
    if not v or str(v).upper() in UNKNOWN: return None
    return datetime.fromisoformat(str(v).replace('Z','+00:00'))

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 str(x.get('schedule_type','')).upper() in UNKNOWN or str(x.get('schedule_expression','')).upper() in UNKNOWN or str(x.get('timezone','')).upper() in UNKNOWN:
        return {'state':'HOLD_SCHEDULE_IDENTITY_UNKNOWN','reason':'SCHEDULE_TYPE_EXPRESSION_OR_TIMEZONE_UNKNOWN'}
    if str(x.get('missed_run_policy','')).upper() in UNKNOWN:
        return {'state':'HOLD_MISSED_RUN_POLICY_UNKNOWN','reason':'MISSED_RUN_BEHAVIOR_MUST_BE_EXPLICIT'}
    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 not str(x.get('idempotency_key','')).strip() or str(x.get('idempotency_key','')).upper() in UNKNOWN:
        return {'state':'HOLD_IDEMPOTENCY_KEY','reason':'IDEMPOTENCY_EVIDENCE_REQUIRED'}
    claims=int(x.get('lease_acquire_claims',1) or 0)
    if claims!=1:return {'state':'HOLD_CONCURRENT_LEASE_CLAIM','reason':'EXACTLY_ONE_ACTIVE_LEASE_CLAIM_REQUIRED','claims':claims}
    now=dt(x.get('now'))
    lexp=dt(x.get('lease_expires_at'))
    if not lexp:return {'state':'HOLD_LEASE_EVIDENCE_UNKNOWN','reason':'LEASE_EXPIRY_REQUIRED'}
    prior=str(x.get('previous_outcome_state','NO_COMMITTED_RESULT')).upper()
    if now and lexp<=now:
        if prior in UNKNOWN:return {'state':'HOLD_LEASE_EXPIRED_OUTCOME_UNKNOWN','reason':'LEASE_EXPIRED_WHILE_PREVIOUS_OUTCOME_UNKNOWN'}
        return {'state':'HOLD_LEASE_EXPIRED','reason':'CURRENT_EXECUTION_LEASE_IS_NOT_VALID'}
    if prior in UNKNOWN:
        return {'state':'HOLD_PREVIOUS_OUTCOME_UNKNOWN','reason':'RECONCILE_PREVIOUS_RESULT_BEFORE_NEW_ATTEMPT'}
    attempt=int(x.get('attempt',0) or 0); budget=int(x.get('retry_budget',0) or 0)
    if attempt>budget or bool(x.get('retry_budget_exhausted',False)):
        return {'state':'HOLD_RETRY_BUDGET_EXHAUSTED','reason':'ATTEMPT_EXCEEDS_BOUNDED_RETRY_BUDGET'}
    if attempt>1 and str(x.get('backoff_state','')).upper() in UNKNOWN|{'NONE'}:
        return {'state':'HOLD_BACKOFF_UNKNOWN','reason':'RETRY_REQUIRES_EXPLICIT_BACKOFF_STATE'}
    last=dt(x.get('last_run_at')); nxt=dt(x.get('next_run_at'))
    if not last or not nxt or nxt<=last:return {'state':'HOLD_RUN_TIMELINE_INVALID','reason':'NEXT_RUN_MUST_BE_AFTER_LAST_RUN'}
    actual=float(x.get('actual_jitter_sec',0) or 0); bound=float(x.get('jitter_limit_sec',0) or 0)
    if actual<0 or bound<0 or actual>bound:return {'state':'HOLD_JITTER_OUTSIDE_BOUND','reason':'ACTUAL_JITTER_EXCEEDS_DECLARED_LIMIT'}
    st=str(x.get('schedule_type','')).upper(); missed=int(x.get('missed_occurrence_count',0) or 0)
    if st=='CALENDAR' and missed>0:return {'state':'BOUNDED_CATCH_UP_REVIEW_REQUIRED','reason':'MISSED_CALENDAR_OCCURRENCE_RECONCILED_WITH_SINGLE_OWNER'}
    if st=='INTERVAL':return {'state':'INTERVAL_RUN_REVIEW_REQUIRED','reason':'INTERVAL_OCCURRENCE_HAS_BOUNDED_LEASE_IDEMPOTENCY_AND_RETRY_EVIDENCE'}
    return {'state':'SCHEDULED_RUN_REVIEW_REQUIRED','reason':'SCHEDULED_OCCURRENCE_HAS_BOUNDED_EXECUTION_EVIDENCE'}
