from __future__ import annotations
from datetime import date

REQUIRED=['memory_id', 'memory_class', 'owner_id', 'agent_id', 'purpose_id', 'data_class', 'source_ref', 'provenance_ref', 'read_scope', 'write_scope', 'cross_agent_share', 'session_conversation_ref', 'retention_rule_ref', 'expires_at', 'clear_trigger', 'deletion_state', 'compaction_state', 'sensitive_trace_capture', 'authority_ref', 'status', 'review_owner']
MISSING={None,"","UNKNOWN","MISSING","UNSET"}
ALLOWED_CLASSES={"RUN_CONTEXT","SESSION_HISTORY","DURABLE_MEMORY","EXTERNAL_RESOURCE_REF","TRACE_DATA"}
ALLOWED_STATUS={"ACTIVE","RESTRICTED","EXPIRED","DELETED","ABSENT","HOLD"}

def _csvset(v):
    if isinstance(v,list): return {str(x).strip() for x in v if str(x).strip()}
    return {x.strip() for x in str(v or '').split(',') if x.strip()}

def evaluate(row):
    missing=[k for k in REQUIRED if row.get(k) in MISSING]
    if missing: return {"state":"HOLD_EVIDENCE_MISSING","missing":missing}
    if row['memory_class'] not in ALLOWED_CLASSES: return {"state":"HOLD_MEMORY_CLASS_INVALID"}
    if row['status'] not in ALLOWED_STATUS: return {"state":"HOLD_STATUS_INVALID"}
    reads=_csvset(row['read_scope']); writes=_csvset(row['write_scope'])
    req=str(row.get('requesting_agent_id') or row['agent_id'])
    req_purpose=str(row.get('requested_purpose_id') or row['purpose_id'])
    # purpose is not inherited merely from workflow membership
    if req_purpose != str(row['purpose_id']): return {"state":"HOLD_PURPOSE_MISMATCH"}
    # cross-agent access must be explicit
    if req != str(row['agent_id']) and req in reads and str(row['cross_agent_share']).lower() not in {'true','1','yes'}:
        return {"state":"HOLD_CROSS_AGENT_SHARE_PROHIBITED"}
    if req != str(row['agent_id']) and req in reads and 'SHARE' not in str(row['authority_ref']).upper():
        return {"state":"HOLD_SHARE_AUTHORITY_MISSING"}
    # durable model inference needs provenance
    if row['memory_class']=='DURABLE_MEMORY' and str(row.get('inferred_content','false')).lower() in {'true','1','yes'} and str(row['provenance_ref']).upper() in {'NONE','UNTRACED','INFERENCE_UNTRACED'}:
        return {"state":"HOLD_DURABLE_INFERENCE_PROVENANCE"}
    # indefinite retention is never defaulted from convenience
    if str(row['retention_rule_ref']).upper() in {'INDEFINITE','FOREVER','NONE'} or str(row['expires_at']).upper() in {'NEVER','NONE'}:
        if 'INDEFINITE' not in str(row['authority_ref']).upper(): return {"state":"HOLD_RETENTION_AUTHORITY_MISSING"}
    # expiry/deletion/clear are state transitions, not advisory labels
    now=str(row.get('now','2026-09-14'))
    exp=str(row['expires_at'])
    if exp not in {'N/A','NA','NONE','NEVER'}:
        try:
            if date.fromisoformat(exp) < date.fromisoformat(now) and row['status'] in {'ACTIVE','RESTRICTED'}:
                return {"state":"HOLD_EXPIRED_MEMORY_ACTIVE"}
        except ValueError:
            return {"state":"HOLD_EXPIRY_INVALID"}
    if str(row['deletion_state']).upper()=='DELETED' and row['status'] not in {'DELETED','ABSENT'}:
        return {"state":"HOLD_DELETED_MEMORY_ACTIVE"}
    if str(row.get('clear_event_occurred','false')).lower() in {'true','1','yes'} and row['memory_class']=='SESSION_HISTORY' and row['status'] not in {'DELETED','ABSENT','EXPIRED'}:
        return {"state":"HOLD_CLEAR_TRIGGER_NOT_APPLIED"}
    if str(row.get('linked_sensitive_trace_active','false')).lower() in {'true','1','yes'} and str(row.get('clear_event_occurred','false')).lower() in {'true','1','yes'}:
        return {"state":"HOLD_TRACE_COPY_OUTSIDE_CLEAR_BOUNDARY"}
    # sensitive traces are a retained class of their own
    if row['memory_class']=='TRACE_DATA' and str(row['sensitive_trace_capture']).lower() in {'true','1','yes'}:
        if 'TRACE' not in str(row['authority_ref']).upper(): return {"state":"HOLD_SENSITIVE_TRACE_AUTHORITY"}
        if str(row['retention_rule_ref']).upper() in {'INDEFINITE','NONE'}: return {"state":"HOLD_SENSITIVE_TRACE_RETENTION"}
    if str(row['compaction_state']).upper()=='COMPACTED' and str(row['provenance_ref']).upper() in {'NONE','UNTRACED'}:
        return {"state":"HOLD_COMPACTION_PROVENANCE"}
    if str(row.get('baseline_decision_reused','false')).lower() in {'true','1','yes'}:
        return {"state":"HOLD_BASELINE_ORACLE_REUSE"}
    if str(row.get('transfer_case','false')).lower() in {'true','1','yes'}:
        if req != str(row['agent_id']):
            return {"state":"TRANSFER_RESTRICTED_RECOMPUTED"}
    if row['status']=='ABSENT': return {"state":"EXPLICIT_ABSENT"}
    if row['status']=='RESTRICTED': return {"state":"RESTRICTED_SHARE_REVIEW_READY"}
    if row['status'] in {'EXPIRED','DELETED'}: return {"state":"BOUNDED_NONACTIVE_STATE"}
    return {"state":"BASELINE_BOUNDED_ACTIVE"}
