"""Top-level agent config parsing and validation."""

from __future__ import annotations

from typing import Any

from logos_adk.config import (
    AgentConfig,
    LearningConfig,
    ProviderConfig,
    RateLimitConfig,
    ReliabilityConfig,
    RetrievalOpsConfig,
    RoutingConfig,
    ScalingConfig,
    SecurityConfig,
    ContextWindowConfig,
    SelfQAConfig,
    StateConfig,
    TaskQueueConfig,
)
from logos_adk.errors import ConfigError
from logos_adk.guardrails.config import load_guardrails_from_config
from logos_adk.memory.config import load_memories_from_config
from logos_adk.observe.config import load_observers_from_config
from logos_adk.tools.config import load_tools_from_config


SUPPORTED_PROVIDERS = {
    "openai",
    "anthropic",
    "ollama",
    "openai-compatible",
    "openai-responses",
    "gemini",
}
SUPPORTED_LOAD_BALANCERS = {"round_robin", "least_loaded", "random"}
SUPPORTED_STATE_BACKENDS = {None, "in_memory", "memory", "inmemory", "sqlite", "postgres", "postgresql", "sharded"}
SUPPORTED_RATE_LIMIT_BACKENDS = {"memory", "postgres", "postgresql", "redis"}
SUPPORTED_PII_ACTIONS = {"selective", "block", "warn", "allow"}
SUPPORTED_ROUTING_BUDGET_PREFERENCES = {"balanced", "cheap", "quality"}
SUPPORTED_ROUTING_LEDGER_BACKENDS = {
    "sqlite",
    "postgres",
    "postgresql",
    "memory",
    "in_memory",
    "inmemory",
    "none",
    "disabled",
    "off",
}


def _parse_provider_config(data: dict[str, Any]) -> ProviderConfig | None:
    """Parse provider configuration from YAML data."""
    if data is None:
        return None

    if not isinstance(data, dict):
        raise ConfigError("provider config must be a mapping")

    provider_type = data.get("type")
    if not provider_type:
        raise ConfigError("provider config requires 'type' field")

    if provider_type not in SUPPORTED_PROVIDERS:
        raise ConfigError(
            f"Unknown provider type: {provider_type}. Supported: {', '.join(SUPPORTED_PROVIDERS)}"
        )

    extra = {
        k: v
        for k, v in data.items()
        if k not in {"type", "model", "base_url", "proxy", "api_key", "api_key_env", "api-key"}
    }

    api_key = data.get("api_key") or data.get("api-key")
    api_key_env = data.get("api_key_env") or data.get("api-key-env")

    return ProviderConfig(
        type=provider_type,
        model=data.get("model"),
        base_url=data.get("base_url"),
        proxy=data.get("proxy"),
        api_key=api_key,
        api_key_env=api_key_env,
        extra=extra,
    )


def _parse_scaling_config(data: Any) -> ScalingConfig:
    """Parse scaling configuration from YAML data."""
    if data is None:
        return ScalingConfig()
    if not isinstance(data, dict):
        raise ConfigError("scaling config must be a mapping")

    unknown_keys = set(data) - {"distributed", "replicas", "load_balancer", "session_affinity", "task_queue"}
    if unknown_keys:
        unknown = ", ".join(sorted(unknown_keys))
        raise ConfigError(f"Unknown scaling config fields: {unknown}")

    distributed = data.get("distributed", False)
    replicas = data.get("replicas", 1)
    load_balancer = data.get("load_balancer", "round_robin")
    session_affinity = data.get("session_affinity", False)

    if not isinstance(distributed, bool):
        raise ConfigError("scaling.distributed must be a boolean")
    if not isinstance(replicas, int) or replicas <= 0:
        raise ConfigError("scaling.replicas must be a positive integer")
    if load_balancer not in SUPPORTED_LOAD_BALANCERS:
        valid = ", ".join(sorted(SUPPORTED_LOAD_BALANCERS))
        raise ConfigError(
            f"Unsupported scaling.load_balancer: {load_balancer}. Valid values: {valid}"
        )
    if not isinstance(session_affinity, bool):
        raise ConfigError("scaling.session_affinity must be a boolean")

    task_queue_data = data.get("task_queue")
    if task_queue_data is None:
        task_queue = TaskQueueConfig()
    else:
        if not isinstance(task_queue_data, dict):
            raise ConfigError("scaling.task_queue must be a mapping")
        queue_unknown = set(task_queue_data) - {
            "backend",
            "dsn",
            "path",
            "concurrency",
            "max_retries",
            "idempotency_ttl_seconds",
        }
        if queue_unknown:
            unknown = ", ".join(sorted(queue_unknown))
            raise ConfigError(f"Unknown scaling.task_queue fields: {unknown}")
        concurrency = task_queue_data.get("concurrency", 4)
        if not isinstance(concurrency, int) or concurrency <= 0:
            raise ConfigError("scaling.task_queue.concurrency must be a positive integer")
        max_retries = task_queue_data.get("max_retries", 2)
        if not isinstance(max_retries, int) or max_retries < 0:
            raise ConfigError("scaling.task_queue.max_retries must be a non-negative integer")
        idempotency_ttl_seconds = task_queue_data.get("idempotency_ttl_seconds", 86_400)
        if not isinstance(idempotency_ttl_seconds, int) or idempotency_ttl_seconds <= 0:
            raise ConfigError(
                "scaling.task_queue.idempotency_ttl_seconds must be a positive integer"
            )
        task_queue = TaskQueueConfig(
            backend=task_queue_data.get("backend", "postgresql"),
            dsn=task_queue_data.get("dsn"),
            path=task_queue_data.get("path", ".logos_adk/tasks.db"),
            concurrency=concurrency,
            max_retries=max_retries,
            idempotency_ttl_seconds=idempotency_ttl_seconds,
        )

    return ScalingConfig(
        distributed=distributed,
        replicas=replicas,
        load_balancer=load_balancer,
        session_affinity=session_affinity,
        task_queue=task_queue,
    )


def _parse_state_config(data: Any) -> StateConfig:
    """Parse state persistence configuration from YAML data."""
    if data is None:
        return StateConfig()
    if not isinstance(data, dict):
        raise ConfigError("state config must be a mapping")

    unknown_keys = set(data) - {"backend", "dsn", "path", "table_name", "shards"}
    if unknown_keys:
        unknown = ", ".join(sorted(unknown_keys))
        raise ConfigError(f"Unknown state config fields: {unknown}")

    backend = data.get("backend")
    if backend not in SUPPORTED_STATE_BACKENDS:
        valid = ", ".join(sorted(value for value in SUPPORTED_STATE_BACKENDS if value is not None))
        raise ConfigError(f"Unsupported state.backend: {backend}. Valid values: {valid}")

    shards: list[StateConfig] = []
    shard_configs = data.get("shards")
    if shard_configs is not None:
        if not isinstance(shard_configs, list) or not shard_configs:
            raise ConfigError("state.shards must be a non-empty list")
        shards = [_parse_state_config(shard) for shard in shard_configs]

    path = data.get("path", ".logos_adk/state.db")
    table_name = data.get("table_name", "agent_state_versions")
    if not isinstance(path, str) or not path:
        raise ConfigError("state.path must be a non-empty string")
    if not isinstance(table_name, str) or not table_name:
        raise ConfigError("state.table_name must be a non-empty string")

    return StateConfig(
        backend=backend,
        dsn=data.get("dsn"),
        path=path,
        table_name=table_name,
        shards=shards,
    )


def _parse_security_config(data: Any) -> SecurityConfig:
    """Parse security policy configuration from YAML data."""
    if data is None:
        return SecurityConfig()
    if not isinstance(data, dict):
        raise ConfigError("security config must be a mapping")

    unknown_keys = set(data) - {
        "input_validation_enabled",
        "prompt_shield_enabled",
        "prompt_injection_threshold",
        "prompt_injection_patterns",
        "pii_detection_enabled",
        "pii_blocked_types",
        "audit_logging_enabled",
        "input_max_length",
        "input_min_length",
        "output_max_length",
        "reject_null_bytes",
        "input_pii_action",
        "output_pii_action",
        "audit_redact_pii",
        "audit_retention_days",
        "rate_limit",
    }
    if unknown_keys:
        raise ConfigError(f"Unknown security config fields: {', '.join(sorted(unknown_keys))}")

    rate_limit_data = data.get("rate_limit")
    if rate_limit_data is None:
        rate_limit = RateLimitConfig()
    else:
        if not isinstance(rate_limit_data, dict):
            raise ConfigError("security.rate_limit must be a mapping")
        rate_unknown = set(rate_limit_data) - {
            "enabled",
            "backend",
            "dsn",
            "namespace",
            "table_name",
            "limit_per_minute",
            "burst",
            "window_seconds",
        }
        if rate_unknown:
            raise ConfigError(f"Unknown security.rate_limit fields: {', '.join(sorted(rate_unknown))}")
        backend = rate_limit_data.get("backend", "memory")
        if backend not in SUPPORTED_RATE_LIMIT_BACKENDS:
            valid = ", ".join(sorted(SUPPORTED_RATE_LIMIT_BACKENDS))
            raise ConfigError(f"Unsupported security.rate_limit.backend: {backend}. Valid values: {valid}")
        rate_limit = RateLimitConfig(
            enabled=bool(rate_limit_data.get("enabled", True)),
            backend=backend,
            dsn=rate_limit_data.get("dsn"),
            namespace=str(rate_limit_data.get("namespace", "logos_adk_rate_limit")),
            table_name=str(rate_limit_data.get("table_name", "rate_limit_events")),
            limit_per_minute=int(rate_limit_data.get("limit_per_minute", 120)),
            burst=int(rate_limit_data.get("burst", 30)),
            window_seconds=int(rate_limit_data.get("window_seconds", 60)),
        )

    patterns = data.get("prompt_injection_patterns", [])
    blocked_types = data.get("pii_blocked_types", ["ssn", "credit_card"])
    if not isinstance(patterns, list):
        raise ConfigError("security.prompt_injection_patterns must be a list")
    if not isinstance(blocked_types, list):
        raise ConfigError("security.pii_blocked_types must be a list")
    input_pii_action = str(data.get("input_pii_action", "selective"))
    output_pii_action = str(data.get("output_pii_action", "selective"))
    if input_pii_action not in SUPPORTED_PII_ACTIONS:
        raise ConfigError(
            "security.input_pii_action must be one of: allow, block, selective, warn"
        )
    if output_pii_action not in SUPPORTED_PII_ACTIONS:
        raise ConfigError(
            "security.output_pii_action must be one of: allow, block, selective, warn"
        )

    return SecurityConfig(
        input_validation_enabled=bool(data.get("input_validation_enabled", True)),
        prompt_shield_enabled=bool(data.get("prompt_shield_enabled", True)),
        prompt_injection_threshold=float(data.get("prompt_injection_threshold", 0.5)),
        prompt_injection_patterns=[str(item) for item in patterns],
        pii_detection_enabled=bool(data.get("pii_detection_enabled", True)),
        pii_blocked_types=[str(item) for item in blocked_types],
        audit_logging_enabled=bool(data.get("audit_logging_enabled", True)),
        input_max_length=int(data.get("input_max_length", 20_000)),
        input_min_length=int(data.get("input_min_length", 1)),
        output_max_length=int(data.get("output_max_length", 20_000)),
        reject_null_bytes=bool(data.get("reject_null_bytes", True)),
        input_pii_action=input_pii_action,
        output_pii_action=output_pii_action,
        audit_redact_pii=bool(data.get("audit_redact_pii", True)),
        audit_retention_days=int(data.get("audit_retention_days", 30)),
        rate_limit=rate_limit,
    )


def _parse_reliability_config(data: Any) -> ReliabilityConfig:
    """Parse reliability policy configuration from YAML data."""
    if data is None:
        return ReliabilityConfig()
    if not isinstance(data, dict):
        raise ConfigError("reliability config must be a mapping")

    unknown_keys = set(data) - {
        "timeout_seconds",
        "retry_max_attempts",
        "retry_base_delay",
        "retry_backoff",
        "circuit_breaker_failure_threshold",
        "circuit_breaker_recovery_timeout",
        "fallback_models",
    }
    if unknown_keys:
        raise ConfigError(f"Unknown reliability config fields: {', '.join(sorted(unknown_keys))}")

    fallback_models = data.get("fallback_models", [])
    if not isinstance(fallback_models, list):
        raise ConfigError("reliability.fallback_models must be a list")
    retry_backoff = str(data.get("retry_backoff", "exponential"))
    if retry_backoff not in {"fixed", "exponential"}:
        raise ConfigError("reliability.retry_backoff must be 'fixed' or 'exponential'")

    return ReliabilityConfig(
        timeout_seconds=float(data.get("timeout_seconds", 60.0)),
        retry_max_attempts=int(data.get("retry_max_attempts", 3)),
        retry_base_delay=float(data.get("retry_base_delay", 0.25)),
        retry_backoff=retry_backoff,
        circuit_breaker_failure_threshold=int(data.get("circuit_breaker_failure_threshold", 5)),
        circuit_breaker_recovery_timeout=float(data.get("circuit_breaker_recovery_timeout", 30.0)),
        fallback_models=[str(item) for item in fallback_models],
    )


def _parse_learning_config(data: Any) -> LearningConfig:
    """Parse learning-loop configuration from YAML data."""
    if data is None:
        return LearningConfig()
    if not isinstance(data, dict):
        raise ConfigError("learning config must be a mapping")

    unknown_keys = set(data) - {"retrieval"}
    if unknown_keys:
        raise ConfigError(f"Unknown learning config fields: {', '.join(sorted(unknown_keys))}")

    retrieval_data = data.get("retrieval")
    if retrieval_data is None:
        return LearningConfig()
    if not isinstance(retrieval_data, dict):
        raise ConfigError("learning.retrieval must be a mapping")

    retrieval_unknown = set(retrieval_data) - {
        "dsn",
        "path",
        "table_prefix",
        "source_document_ttl_days",
        "max_source_documents_per_source",
        "dedupe_window_seconds",
    }
    if retrieval_unknown:
        raise ConfigError(
            f"Unknown learning.retrieval fields: {', '.join(sorted(retrieval_unknown))}"
        )

    dsn = retrieval_data.get("dsn")
    path = retrieval_data.get("path", ".logos_adk/retrieval_learning.db")
    table_prefix = retrieval_data.get("table_prefix", "retrieval_learning")
    ttl_days = int(retrieval_data.get("source_document_ttl_days", 30))
    max_per_source = int(retrieval_data.get("max_source_documents_per_source", 20))
    dedupe_window_seconds = int(retrieval_data.get("dedupe_window_seconds", 600))
    if dsn is not None and (not isinstance(dsn, str) or not dsn):
        raise ConfigError("learning.retrieval.dsn must be a non-empty string when provided")
    if not isinstance(path, str) or not path:
        raise ConfigError("learning.retrieval.path must be a non-empty string")
    if not isinstance(table_prefix, str) or not table_prefix:
        raise ConfigError("learning.retrieval.table_prefix must be a non-empty string")
    if ttl_days <= 0:
        raise ConfigError("learning.retrieval.source_document_ttl_days must be a positive integer")
    if max_per_source <= 0:
        raise ConfigError(
            "learning.retrieval.max_source_documents_per_source must be a positive integer"
        )
    if dedupe_window_seconds < 0:
        raise ConfigError("learning.retrieval.dedupe_window_seconds must be zero or greater")

    return LearningConfig(
        retrieval=RetrievalOpsConfig(
            dsn=dsn,
            path=path,
            table_prefix=table_prefix,
            source_document_ttl_days=ttl_days,
            max_source_documents_per_source=max_per_source,
            dedupe_window_seconds=dedupe_window_seconds,
        )
    )


def _parse_routing_config(data: Any) -> RoutingConfig:
    """Parse autonomous model routing configuration from YAML data."""
    if data is None:
        return RoutingConfig()
    if isinstance(data, bool):
        return RoutingConfig(enabled=data)
    if not isinstance(data, dict):
        raise ConfigError("routing config must be a mapping or boolean")

    unknown_keys = set(data) - {
        "enabled",
        "budget_preference",
        "task_budget_usd",
        "task_budget_tokens",
        "fail_on_budget_exhausted",
        "ledger_backend",
        "ledger_path",
        "ledger_dsn",
        "workspace_soft_limit_usd",
        "session_soft_limit_usd",
        "threshold_ratio",
        "simple_task_types",
        "critical_task_types",
        "simple_prompt_markers",
        "critical_prompt_markers",
        "medium_length_threshold",
        "high_length_threshold",
    }
    if unknown_keys:
        raise ConfigError(f"Unknown routing config fields: {', '.join(sorted(unknown_keys))}")

    budget_preference = str(data.get("budget_preference", "balanced"))
    if budget_preference not in SUPPORTED_ROUTING_BUDGET_PREFERENCES:
        valid = ", ".join(sorted(SUPPORTED_ROUTING_BUDGET_PREFERENCES))
        raise ConfigError(f"routing.budget_preference must be one of: {valid}")
    task_budget_usd = data.get("task_budget_usd")
    task_budget_tokens = data.get("task_budget_tokens")
    fail_on_budget_exhausted = data.get("fail_on_budget_exhausted", False)
    if not isinstance(fail_on_budget_exhausted, bool):
        raise ConfigError("routing.fail_on_budget_exhausted must be a boolean")
    ledger_backend = str(data.get("ledger_backend", "sqlite"))
    if ledger_backend == "postgres":
        ledger_backend = "postgresql"
    if ledger_backend not in SUPPORTED_ROUTING_LEDGER_BACKENDS:
        valid = ", ".join(sorted(SUPPORTED_ROUTING_LEDGER_BACKENDS))
        raise ConfigError(f"routing.ledger_backend must be one of: {valid}")
    ledger_path = str(data.get("ledger_path", ".logos_adk/spend_ledger.db"))
    ledger_dsn = data.get("ledger_dsn")
    if ledger_dsn is not None and not isinstance(ledger_dsn, str):
        raise ConfigError("routing.ledger_dsn must be a string")
    workspace_soft_limit_usd = data.get("workspace_soft_limit_usd")
    session_soft_limit_usd = data.get("session_soft_limit_usd")
    threshold_ratio = float(data.get("threshold_ratio", 0.8))
    if task_budget_usd is not None and float(task_budget_usd) < 0:
        raise ConfigError("routing.task_budget_usd must be zero or greater")
    if task_budget_tokens is not None and int(task_budget_tokens) < 0:
        raise ConfigError("routing.task_budget_tokens must be zero or greater")
    if workspace_soft_limit_usd is not None and float(workspace_soft_limit_usd) < 0:
        raise ConfigError("routing.workspace_soft_limit_usd must be zero or greater")
    if session_soft_limit_usd is not None and float(session_soft_limit_usd) < 0:
        raise ConfigError("routing.session_soft_limit_usd must be zero or greater")
    if threshold_ratio < 0:
        raise ConfigError("routing.threshold_ratio must be zero or greater")

    simple_task_types = data.get("simple_task_types", [])
    critical_task_types = data.get("critical_task_types", [])
    simple_prompt_markers = data.get("simple_prompt_markers", [])
    critical_prompt_markers = data.get("critical_prompt_markers", [])
    if not isinstance(simple_task_types, list):
        raise ConfigError("routing.simple_task_types must be a list")
    if not isinstance(critical_task_types, list):
        raise ConfigError("routing.critical_task_types must be a list")
    if not isinstance(simple_prompt_markers, list):
        raise ConfigError("routing.simple_prompt_markers must be a list")
    if not isinstance(critical_prompt_markers, list):
        raise ConfigError("routing.critical_prompt_markers must be a list")

    medium_length_threshold = int(data.get("medium_length_threshold", 180))
    high_length_threshold = int(data.get("high_length_threshold", 600))
    if medium_length_threshold <= 0:
        raise ConfigError("routing.medium_length_threshold must be a positive integer")
    if high_length_threshold < medium_length_threshold:
        raise ConfigError(
            "routing.high_length_threshold must be an integer >= routing.medium_length_threshold"
        )

    return RoutingConfig(
        enabled=bool(data.get("enabled", True)),
        budget_preference=budget_preference,
        task_budget_usd=(
            None if task_budget_usd is None else float(task_budget_usd)
        ),
        task_budget_tokens=(
            None if task_budget_tokens is None else int(task_budget_tokens)
        ),
        fail_on_budget_exhausted=fail_on_budget_exhausted,
        ledger_backend=ledger_backend,
        ledger_path=ledger_path,
        ledger_dsn=ledger_dsn,
        workspace_soft_limit_usd=(
            float(workspace_soft_limit_usd)
            if workspace_soft_limit_usd is not None
            else None
        ),
        session_soft_limit_usd=(
            float(session_soft_limit_usd)
            if session_soft_limit_usd is not None
            else None
        ),
        threshold_ratio=threshold_ratio,
        simple_task_types=[str(item) for item in simple_task_types] or RoutingConfig().simple_task_types,
        critical_task_types=[str(item) for item in critical_task_types] or RoutingConfig().critical_task_types,
        simple_prompt_markers=[str(item) for item in simple_prompt_markers] or RoutingConfig().simple_prompt_markers,
        critical_prompt_markers=[str(item) for item in critical_prompt_markers] or RoutingConfig().critical_prompt_markers,
        medium_length_threshold=medium_length_threshold,
        high_length_threshold=high_length_threshold,
    )


def _parse_self_qa_config(data: Any) -> SelfQAConfig:
    """Parse autonomous self-QA configuration from YAML data."""
    if data is None:
        return SelfQAConfig()
    if isinstance(data, bool):
        return SelfQAConfig(enabled=data)
    if not isinstance(data, dict):
        raise ConfigError("self_qa config must be a mapping or boolean")

    unknown_keys = set(data) - {
        "enabled",
        "min_assertions",
        "max_assertions",
        "max_retries",
        "fail_closed",
    }
    if unknown_keys:
        raise ConfigError(f"Unknown self_qa config fields: {', '.join(sorted(unknown_keys))}")

    enabled = data.get("enabled", True)
    min_assertions = data.get("min_assertions", 2)
    max_assertions = data.get("max_assertions", 3)
    max_retries = data.get("max_retries", 1)
    fail_closed = data.get("fail_closed", True)

    if not isinstance(enabled, bool):
        raise ConfigError("self_qa.enabled must be a boolean")
    if not isinstance(min_assertions, int) or min_assertions < 1:
        raise ConfigError("self_qa.min_assertions must be a positive integer")
    if not isinstance(max_assertions, int) or max_assertions < min_assertions:
        raise ConfigError("self_qa.max_assertions must be an integer >= self_qa.min_assertions")
    if not isinstance(max_retries, int) or max_retries < 0:
        raise ConfigError("self_qa.max_retries must be a non-negative integer")
    if not isinstance(fail_closed, bool):
        raise ConfigError("self_qa.fail_closed must be a boolean")

    return SelfQAConfig(
        enabled=enabled,
        min_assertions=min_assertions,
        max_assertions=max_assertions,
        max_retries=max_retries,
        fail_closed=fail_closed,
    )


def _parse_context_window_config(data: Any) -> ContextWindowConfig:
    """Parse context window configuration from YAML data."""
    if data is None:
        return ContextWindowConfig()
    if not isinstance(data, dict):
        raise ConfigError("context_window config must be a mapping")

    known_keys = {
        "max_context_tokens", "reserved_output_tokens", "strategy",
        "chunk_size", "summary_max_tokens", "max_summary_chain",
        "keep_recent_messages", "summary_model", "summary_system_prompt",
        "memory_max_tokens",
        "smart_tool_selection",
        "semantic_cache_max_entries",
        "auto_detect_model_limit",
    }
    unknown_keys = set(data.keys()) - known_keys
    if unknown_keys:
        raise ConfigError(f"Unknown context_window config fields: {', '.join(sorted(unknown_keys))}")

    strategy = data.get("strategy", "chunked_summary")
    if strategy not in ("truncate", "sliding_window", "chunked_summary"):
        raise ConfigError("context_window.strategy must be 'truncate', 'sliding_window', or 'chunked_summary'")

    return ContextWindowConfig(
        max_context_tokens=int(data.get("max_context_tokens", 0)),
        reserved_output_tokens=int(data.get("reserved_output_tokens", 4096)),
        strategy=strategy,
        chunk_size=int(data.get("chunk_size", 10)),
        summary_max_tokens=int(data.get("summary_max_tokens", 300)),
        max_summary_chain=int(data.get("max_summary_chain", 5)),
        keep_recent_messages=int(data.get("keep_recent_messages", 6)),
        summary_model=data.get("summary_model"),
        summary_system_prompt=data.get("summary_system_prompt"),
        memory_max_tokens=int(data.get("memory_max_tokens", 0)),
        smart_tool_selection=bool(data.get("smart_tool_selection", False)),
        semantic_cache_max_entries=int(data.get("semantic_cache_max_entries", 0)),
        auto_detect_model_limit=bool(data.get("auto_detect_model_limit", False)),
    )


def parse_agent_config_data(data: Any) -> AgentConfig:
    """Parse a raw YAML mapping into a typed AgentConfig."""
    if not isinstance(data, dict):
        raise ConfigError("agent config must be a mapping")

    unknown_keys = set(data) - {
        "name",
        "agent_class",
        "agent_module",
        "agent_factory",
        "model",
        "provider",
        "system_prompt",
        "role",
        "goal",
        "backstory",
        "tools",
        "memory",
        "observe",
        "guardrails",
        "cache",
        "knowledge",
        "learning",
        "scaling",
        "state",
        "security",
        "reliability",
        "routing",
        "self_qa",
        "context_window",
    }
    if unknown_keys:
        unknown = ", ".join(sorted(unknown_keys))
        raise ConfigError(f"Unknown agent config fields: {unknown}")

    name = data.get("name")
    if not isinstance(name, str) or not name:
        raise ConfigError("agent config requires a non-empty string 'name'")

    agent_class = data.get("agent_class")
    if agent_class is not None and not isinstance(agent_class, str):
        raise ConfigError("agent config field 'agent_class' must be a string")

    agent_module = data.get("agent_module")
    if agent_module is not None and not isinstance(agent_module, str):
        raise ConfigError("agent config field 'agent_module' must be a string")

    agent_factory = data.get("agent_factory")
    if agent_factory is not None and not isinstance(agent_factory, str):
        raise ConfigError("agent config field 'agent_factory' must be a string")
    if agent_class is not None and agent_factory is not None:
        raise ConfigError("agent config may not set both 'agent_class' and 'agent_factory'")

    model = data.get("model")
    if model is not None and not isinstance(model, str):
        raise ConfigError("agent config field 'model' must be a string")

    system_prompt = data.get("system_prompt")
    if system_prompt is not None and not isinstance(system_prompt, str):
        raise ConfigError("agent config field 'system_prompt' must be a string")
    role = data.get("role")
    if role is not None and not isinstance(role, str):
        raise ConfigError("agent config field 'role' must be a string")
    goal = data.get("goal")
    if goal is not None and not isinstance(goal, str):
        raise ConfigError("agent config field 'goal' must be a string")
    backstory = data.get("backstory")
    if backstory is not None and not isinstance(backstory, str):
        raise ConfigError("agent config field 'backstory' must be a string")

    provider = _parse_provider_config(data.get("provider"))

    input_guardrails, output_guardrails = load_guardrails_from_config(data.get("guardrails"))

    return AgentConfig(
        name=name,
        agent_class=agent_class,
        agent_module=agent_module,
        agent_factory=agent_factory,
        model=model,
        provider=provider,
        system_prompt=system_prompt,
        role=role,
        goal=goal,
        backstory=backstory,
        tools=load_tools_from_config(data.get("tools")),
        memories=load_memories_from_config(data.get("memory")),
        observers=load_observers_from_config(data.get("observe")),
        input_guardrails=input_guardrails,
        output_guardrails=output_guardrails,
        learning=_parse_learning_config(data.get("learning")),
        scaling=_parse_scaling_config(data.get("scaling")),
        state=_parse_state_config(data.get("state")),
        security=_parse_security_config(data.get("security")),
        reliability=_parse_reliability_config(data.get("reliability")),
        routing=_parse_routing_config(data.get("routing")),
        self_qa=_parse_self_qa_config(data.get("self_qa")),
        context_window=_parse_context_window_config(data.get("context_window")),
    )
