from datetime import datetime


AI_RESULT_TYPES = {"reply", "observation", "proposal", "tool_request", "error"}
MEMORY_SCOPES = {"session", "task", "chunk", "tool", "agent"}
TRACE_OUTCOMES = {"success", "denied", "failed"}


def _require_dict(value, field_name):
    if not isinstance(value, dict):
        raise ValueError(f"'{field_name}' must be an object")


def _require_str(value, field_name):
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"'{field_name}' must be a non-empty string")


def _optional_iso8601(value, field_name):
    if value is None:
        return
    _require_str(value, field_name)
    datetime.fromisoformat(value.replace("Z", "+00:00"))


def validate_ai_result(obj):
    _require_dict(obj, "AIResult")
    result_type = obj.get("type")
    if result_type not in AI_RESULT_TYPES:
        raise ValueError(f"'type' must be one of {sorted(AI_RESULT_TYPES)}")

    _require_str(obj.get("agent_id"), "agent_id")
    _require_str(obj.get("run_id"), "run_id")
    _optional_iso8601(obj.get("timestamp"), "timestamp")
    _require_dict(obj.get("content"), "content")

    if "meta" in obj:
        _require_dict(obj["meta"], "meta")

    content = obj["content"]
    if result_type == "reply":
        _require_str(content.get("text"), "content.text")
    elif result_type == "observation":
        _require_str(content.get("label"), "content.label")
    elif result_type == "proposal":
        if content.get("artifact_type") not in {"task_draft", "wrapper_draft", "patch_draft"}:
            raise ValueError("content.artifact_type must be task_draft|wrapper_draft|patch_draft")
        _require_str(content.get("title"), "content.title")
    elif result_type == "tool_request":
        _require_str(content.get("tool_id"), "content.tool_id")
        _require_str(content.get("action"), "content.action")
        if content.get("scope") not in MEMORY_SCOPES:
            raise ValueError(f"content.scope must be one of {sorted(MEMORY_SCOPES)}")
    elif result_type == "error":
        _require_str(content.get("code"), "content.code")
        _require_str(content.get("message"), "content.message")

    return obj


def validate_tool_request(obj):
    _require_dict(obj, "ToolRequest")
    for required in ("request_id", "run_id", "agent_id", "tool_id", "action", "timestamp"):
        _require_str(obj.get(required), required)
    if obj.get("requested_scope") not in MEMORY_SCOPES:
        raise ValueError(f"requested_scope must be one of {sorted(MEMORY_SCOPES)}")
    if "args" in obj:
        _require_dict(obj["args"], "args")
    if "budget_cost" in obj and not isinstance(obj["budget_cost"], (int, float)):
        raise ValueError("budget_cost must be numeric")
    _optional_iso8601(obj.get("timestamp"), "timestamp")
    return obj


def validate_trace_event(obj):
    _require_dict(obj, "TraceEvent")
    for required in ("trace_id", "span_id", "run_id", "event_type", "timestamp"):
        _require_str(obj.get(required), required)
    if obj.get("outcome") not in TRACE_OUTCOMES:
        raise ValueError(f"outcome must be one of {sorted(TRACE_OUTCOMES)}")
    if "payload" in obj:
        _require_dict(obj["payload"], "payload")
    if "timing" in obj:
        _require_dict(obj["timing"], "timing")
    _optional_iso8601(obj.get("timestamp"), "timestamp")
    return obj


def validate_memory_entry(obj):
    _require_dict(obj, "MemoryEntry")
    for required in ("entry_id", "scope", "key", "created_at"):
        _require_str(obj.get(required), required)
    if obj.get("scope") not in MEMORY_SCOPES:
        raise ValueError(f"scope must be one of {sorted(MEMORY_SCOPES)}")
    _require_dict(obj.get("scope_ref", {}), "scope_ref")
    if "inspect" in obj:
        _require_dict(obj["inspect"], "inspect")
    _optional_iso8601(obj.get("created_at"), "created_at")
    _optional_iso8601(obj.get("expires_at"), "expires_at")
    return obj

