import copy
REQUIRED_EVIDENCE_FIELDS = ["data_class","storage_boundary","ciphertext_location","key_handle","kek_authority","decrypting_workload","rotation_state","recovery_evidence","observed_evidence","residual_risk"]

def _ctx(name, policy):
    return policy.get("contexts",{}).get(name)

def evaluate_boundary(context_name, policy):
    ctx=_ctx(context_name,policy)
    if not ctx: return {"decision":"DENY_UNKNOWN_CONTEXT"}
    if any(not ctx.get(k) for k in ["storage_boundary","ciphertext_location","key_handle","kek_authority"]):
        return {"decision":"DENY_KEY_BOUNDARY_METADATA_INCOMPLETE"}
    if ctx.get("key_storage_model") in set(policy.get("forbidden_key_storage_models",[])):
        return {"decision":"DENY_KEY_STORAGE_BOUNDARY"}
    if ctx.get("same_compromise_path") is True or ctx.get("kek_authority")==ctx.get("storage_boundary"):
        return {"decision":"BOUNDARY_WEAKENED_KEY_AUTHORITY_COLLAPSED"}
    if ctx.get("recovery_required") and not ctx.get("recovery_metadata"):
        return {"decision":"DENY_RECOVERY_METADATA_MISSING"}
    if not ctx.get("key_handle") or not ctx.get("kek_authority"):
        return {"decision":"DENY_KEY_BOUNDARY_METADATA_INCOMPLETE"}
    return {"decision":"BOUNDARY_OK","storage_boundary":ctx["storage_boundary"],"key_handle":ctx["key_handle"],"kek_authority":ctx["kek_authority"]}

def evaluate_decrypt(context_name, workload, policy):
    ctx=_ctx(context_name,policy)
    if not ctx: return {"decision":"DENY_UNKNOWN_CONTEXT"}
    b=evaluate_boundary(context_name,policy)
    if b["decision"]!="BOUNDARY_OK": return b
    if workload not in ctx.get("authorized_workloads",[]):
        return {"decision":"DENY_DECRYPT_AUTHORITY"}
    return {"decision":"ALLOW_DECRYPT_AUTHORIZED","key_handle":ctx["key_handle"],"workload":workload}

def evaluate_rotation(context_name, rotation_event, objects, policy):
    ctx=_ctx(context_name,policy)
    if not ctx: return {"decision":"DENY_UNKNOWN_CONTEXT"}
    old_key_id=rotation_event.get("old_key_id")
    if not rotation_event.get("new_key_id") or rotation_event.get("new_key_id")==old_key_id:
        return {"decision":"DENY_ROTATION_NEW_KEY_INVALID"}
    known_old={o.get("key_id") for o in objects if o.get("context")==context_name and o.get("key_id")}
    if not old_key_id or old_key_id not in known_old:
        return {"decision":"DENY_ROTATION_OLD_KEY_UNKNOWN"}
    if rotation_event.get("new_key_id")!=ctx.get("current_key_id"):
        return {"decision":"DENY_ROTATION_CURRENT_KEY_MISMATCH"}
    historical={o.get("key_id") for o in objects if o.get("context")==context_name and o.get("historical")}
    accounted=set(rotation_event.get("historical_key_ids_accounted",[]))
    if policy.get("rotation",{}).get("historical_path_must_be_accounted") and not historical.issubset(accounted):
        return {"decision":"DENY_ROTATION_HISTORICAL_PATH_UNACCOUNTED","missing":sorted(historical-accounted)}
    if ctx.get("recovery_required") and not rotation_event.get("recovery_metadata"):
        return {"decision":"DENY_RECOVERY_METADATA_MISSING"}
    return {"decision":"ROTATION_ACCOUNTED","current_key_id":ctx["current_key_id"],"historical_key_ids":sorted(historical)}

def evidence_row_complete(row):
    if not isinstance(row,dict): return False
    if set(row)!=set(REQUIRED_EVIDENCE_FIELDS): return False
    return all(k in row and row[k] not in (None,"",[],{}) for k in REQUIRED_EVIDENCE_FIELDS)

def build_evidence_row(context_name, workload, policy):
    ctx=_ctx(context_name,policy); b=evaluate_boundary(context_name,policy); d=evaluate_decrypt(context_name,workload,policy)
    if not ctx: return {}
    rotation_state="CURRENT_%s_HISTORICAL_%s"%(ctx.get("current_key_id","UNKNOWN"),",".join(ctx.get("historical_key_ids",[])) or "NONE")
    return {"data_class":ctx.get("data_class",""),"storage_boundary":ctx.get("storage_boundary",""),"ciphertext_location":ctx.get("ciphertext_location",""),"key_handle":ctx.get("key_handle",""),"kek_authority":ctx.get("kek_authority",""),"decrypting_workload":workload,"rotation_state":rotation_state,"recovery_evidence":ctx.get("recovery_metadata","") or "MISSING","observed_evidence":b.get("decision","")+" / "+d.get("decision",""),"residual_risk":"Synthetic/local boundary only; compromise of the authorized decrypting workload remains outside this at-rest proof."}

def classify_transfer(context_name):
    if context_name=="backup_archive": return {"status":"REBUILD_KEY_BOUNDARY","copy_primary_boundary":False,"reason":"different_recovery_and_storage_context"}
    if context_name=="primary_database": return {"status":"REBUILD_KEY_BOUNDARY","copy_primary_boundary":False,"reason":"independent_primary_context"}
    return {"status":"BOUNDED_DEFER","reason":"unknown_changed_context"}
