"""Config service with multi-level resolution chain and typed key registry.

Provides a resolution layer on top of ``Settings`` (pydantic-settings) that
manages TOML file persistence and implements a five-level precedence chain:

    CLI flag  >  environment variable  >  project-scoped  >  global config  >  default

Each ``get`` call returns the winning value **and** the source level that
provided it, enabling transparent debugging of configuration provenance.
"""

from __future__ import annotations

import os
import tomllib
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any

import structlog
import tomlkit

from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType
from cleveragents.shared.redaction import REDACTED, is_sensitive_key

if TYPE_CHECKING:
    from cleveragents.infrastructure.events.protocol import EventBus

_logger = structlog.get_logger(__name__)

# ---------------------------------------------------------------------------
# Public types
# ---------------------------------------------------------------------------


class ConfigLevel(Enum):
    """Precedence levels from highest to lowest priority."""

    CLI_FLAG = "cli_flag"
    ENV_VAR = "env_var"
    LOCAL = "local"
    PROJECT = "project"
    GLOBAL = "global"
    DEFAULT = "default"


class ConfigScope(Enum):
    """File-based configuration scopes for ``--scope`` CLI flag."""

    GLOBAL = "global"
    PROJECT = "project"
    LOCAL = "local"


@dataclass(frozen=True, slots=True)
class ConfigEntry:
    """Metadata for a registered configuration key."""

    key: str
    python_type: type[Any]
    default: Any
    env_var: str
    project_scopable: bool
    description: str
    section: str


@dataclass(frozen=True, slots=True)
class ResolvedValue:
    """Result of resolving a configuration key through the precedence chain."""

    key: str
    value: Any
    source: ConfigLevel
    chain: list[dict[str, Any]] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Config key registry catalog
# ---------------------------------------------------------------------------

_REGISTRY: dict[str, ConfigEntry] = {}


def _env_name(section: str, key: str) -> str:
    """Build ``CLEVERAGENTS_<SECTION>_<KEY>`` environment variable name.

    Dots and dashes in *section* and *key* are normalised to underscores
    so that ``core.log.level`` becomes ``CLEVERAGENTS_CORE_LOG_LEVEL``.
    """
    raw = f"CLEVERAGENTS_{section}_{key}"
    return raw.upper().replace(".", "_").replace("-", "_")


def _register(
    section: str,
    name: str,
    python_type: type[Any],
    default: Any,
    *,
    project_scopable: bool = True,
    description: str = "",
    env_var: str | None = None,
) -> None:
    full_key = f"{section}.{name}"
    resolved_env = env_var if env_var is not None else _env_name(section, name)
    _REGISTRY[full_key] = ConfigEntry(
        key=full_key,
        python_type=python_type,
        default=default,
        env_var=resolved_env,
        project_scopable=project_scopable,
        description=description,
        section=section,
    )


def _build_catalog() -> None:
    """Register the complete configuration catalog per specification.

    All 103 configuration keys are registered here (102 spec-defined +
    1 skills extension).  Each entry carries the exact dotted-dash key
    name, Python type, default value, explicit environment-variable name
    (per spec), project-scopability flag, and human-readable description.
    """

    # ── core.* (14 keys) ────────────────────────────────────────────────
    _register(
        "core",
        "data-dir",
        str,
        "~/.cleveragents",
        project_scopable=False,
        env_var="CLEVERAGENTS_DATA_DIR",
        description=("Root directory for all CleverAgents persistent state."),
    )
    _register(
        "core",
        "config-path",
        str,
        "~/.cleveragents/config.toml",
        project_scopable=False,
        env_var="CLEVERAGENTS_CONFIG_PATH",
        description="Path to the global configuration file.",
    )
    _register(
        "core",
        "format",
        str,
        "rich",
        project_scopable=False,
        env_var="CLEVERAGENTS_FORMAT",
        description=("Default output format: rich, color, table, plain, json, yaml."),
    )
    _register(
        "core",
        "namespace",
        str,
        "local",
        project_scopable=False,
        env_var="CLEVERAGENTS_NAMESPACE",
        description="Default namespace prefix for entities.",
    )
    _register(
        "core",
        "automation-profile",
        str,
        "supervised",
        project_scopable=True,
        env_var="CLEVERAGENTS_AUTOMATION_PROFILE",
        description="Default automation profile for new plans.",
    )
    _register(
        "core",
        "log.level",
        str,
        "FATAL",
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_LEVEL",
        description=("Logging verbosity: FATAL, ERROR, WARN, INFO, DEBUG, TRACE."),
    )
    _register(
        "core",
        "log.dir",
        str,
        "~/.cleveragents/logs",
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_DIR",
        description="Directory for log file output.",
    )
    _register(
        "core",
        "log.file-enabled",
        bool,
        True,
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_FILE_ENABLED",
        description="Whether log output is written to files.",
    )
    _register(
        "core",
        "log.terminal",
        str,
        "auto",
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_TERMINAL",
        description="Terminal log display: auto, always, never.",
    )
    _register(
        "core",
        "log.terminal-stream",
        str,
        "stderr",
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_TERMINAL_STREAM",
        description="Stream for terminal log output: stderr, stdout, or path.",
    )
    _register(
        "core",
        "log.retention-days",
        int,
        30,
        project_scopable=False,
        env_var="CLEVERAGENTS_LOG_RETENTION_DAYS",
        description="Days to retain log files before cleanup.",
    )
    _register(
        "core",
        "backup.dir",
        str,
        "~/.cleveragents/backups",
        project_scopable=False,
        env_var="CLEVERAGENTS_BACKUP_DIR",
        description="Directory for backup snapshots.",
    )
    _register(
        "core",
        "backup.retention-days",
        int,
        7,
        project_scopable=False,
        env_var="CLEVERAGENTS_BACKUP_RETENTION_DAYS",
        description="Days to retain backup snapshots.",
    )
    _register(
        "core",
        "cache.dir",
        str,
        "~/.cleveragents/cache",
        project_scopable=False,
        env_var="CLEVERAGENTS_CACHE_DIR",
        description="Directory for transient caches.",
    )

    # ── server.* (4 keys) ───────────────────────────────────────────────
    _register(
        "server",
        "url",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_URL",
        description="CleverAgents server URL for collaborative mode.",
    )
    _register(
        "server",
        "token",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_TOKEN",
        description="Authentication token for server mode.",
    )
    _register(
        "server",
        "sync.auto",
        bool,
        True,
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_SYNC_AUTO",
        description="Auto-sync entity definitions with the server.",
    )
    _register(
        "server",
        "sync.interval",
        int,
        300,
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_SYNC_INTERVAL",
        description="Seconds between automatic background syncs.",
    )
    _register(
        "server",
        "tls-verify",
        bool,
        True,
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_TLS_VERIFY",
        description="Verify TLS certificates when connecting to the server.",
    )
    _register(
        "server",
        "namespace",
        str,
        "default",
        project_scopable=False,
        env_var="CLEVERAGENTS_SERVER_NAMESPACE",
        description="Default namespace on the remote server.",
    )

    # ── actor.* (5 keys) ────────────────────────────────────────────────
    _register(
        "actor",
        "default.strategy",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_DEFAULT_STRATEGY_ACTOR",
        description="Default strategy actor for plans.",
    )
    _register(
        "actor",
        "default.execution",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_DEFAULT_EXECUTION_ACTOR",
        description="Default execution actor for plans.",
    )
    _register(
        "actor",
        "default.estimation",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR",
        description="Default estimation actor for cost estimation.",
    )
    _register(
        "actor",
        "default.invariant",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_DEFAULT_INVARIANT_ACTOR",
        description="Default invariant reconciliation actor.",
    )
    _register(
        "actor",
        "default.orchestrator",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_DEFAULT_ORCHESTRATOR",
        description="Default orchestrator actor for sessions.",
    )

    # ── plan.* (8 keys) ─────────────────────────────────────────────────
    _register(
        "plan",
        "concurrency",
        int,
        4,
        project_scopable=True,
        env_var="CLEVERAGENTS_PLAN_CONCURRENCY",
        description="Maximum concurrent plan executions.",
    )
    _register(
        "plan",
        "max-child-depth",
        int,
        5,
        project_scopable=True,
        env_var="CLEVERAGENTS_PLAN_MAX_CHILD_DEPTH",
        description="Maximum child plan nesting depth.",
    )
    _register(
        "plan",
        "budget.per-plan",
        float,
        None,
        project_scopable=True,
        env_var="CLEVERAGENTS_PLAN_BUDGET",
        description="Max estimated API cost (USD) per plan.",
    )
    _register(
        "plan",
        "budget.per-session",
        float,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_SESSION_BUDGET",
        description="Max estimated API cost (USD) per session.",
    )
    _register(
        "plan",
        "budget.warn-threshold",
        float,
        0.8,
        project_scopable=False,
        env_var="CLEVERAGENTS_PLAN_BUDGET_WARN",
        description="Fraction of per-plan budget for warning.",
    )
    _register(
        "plan",
        "tool.max-calls-per-step",
        int,
        25,
        project_scopable=False,
        env_var="CLEVERAGENTS_TOOL_MAX_CALLS",
        description="Max tool invocations per actor step.",
    )
    _register(
        "plan",
        "tool.max-retries",
        int,
        3,
        project_scopable=False,
        env_var="CLEVERAGENTS_TOOL_MAX_RETRIES",
        description="Max retries for transient tool failures.",
    )
    _register(
        "plan",
        "tool.retry-backoff",
        str,
        "exponential",
        project_scopable=False,
        env_var="CLEVERAGENTS_TOOL_RETRY_BACKOFF",
        description="Backoff strategy: exponential, linear, none.",
    )

    # ── sandbox.* (5 keys) ──────────────────────────────────────────────
    _register(
        "sandbox",
        "strategy",
        str,
        "git_worktree",
        project_scopable=True,
        env_var="CLEVERAGENTS_SANDBOX_STRATEGY",
        description="Default sandbox isolation strategy.",
    )
    _register(
        "sandbox",
        "cleanup",
        str,
        "on_apply",
        project_scopable=False,
        env_var="CLEVERAGENTS_SANDBOX_CLEANUP",
        description="When to clean up sandbox: on_apply, on_terminal, manual.",
    )
    _register(
        "sandbox",
        "checkpoint.enabled",
        bool,
        True,
        project_scopable=True,
        env_var="CLEVERAGENTS_CHECKPOINT_ENABLED",
        description="Whether checkpointing is enabled.",
    )
    _register(
        "sandbox",
        "checkpoint.dir",
        str,
        "~/.cleveragents/checkpoints",
        project_scopable=False,
        env_var="CLEVERAGENTS_CHECKPOINT_DIR",
        description="Directory for plan execution checkpoints.",
    )
    _register(
        "sandbox",
        "checkpoint.max-per-plan",
        int,
        50,
        project_scopable=False,
        env_var="CLEVERAGENTS_CHECKPOINT_MAX",
        description="Max checkpoints retained per plan.",
    )
    _register(
        "core",
        "checkpoints.auto_create_on",
        str,
        "before_tool_execute,after_tool_execute,on_subplan_spawn,on_error",
        project_scopable=True,
        env_var="CLEVERAGENTS_CHECKPOINT_AUTO_CREATE_ON",
        description=(
            "Comma-separated list of automatic checkpoint triggers. "
            "Valid values: before_tool_execute, after_tool_execute, "
            "on_subplan_spawn, on_error. "
            "Set to empty string to disable all automatic triggers."
        ),
    )

    # ── index.* (12 keys) ───────────────────────────────────────────────
    _register(
        "index",
        "text.backend",
        str,
        "tantivy",
        project_scopable=False,
        env_var="CLEVERAGENTS_INDEX_TEXT_BACKEND",
        description="Full-text search backend: tantivy, sqlite_fts.",
    )
    _register(
        "index",
        "text.dir",
        str,
        "~/.cleveragents/index/text",
        project_scopable=False,
        env_var="CLEVERAGENTS_INDEX_TEXT_DIR",
        description="Full-text search index directory.",
    )
    _register(
        "index",
        "vector.backend",
        str,
        "faiss",
        project_scopable=False,
        env_var="CLEVERAGENTS_INDEX_VECTOR_BACKEND",
        description="Vector search backend: faiss, qdrant, none.",
    )
    _register(
        "index",
        "vector.dir",
        str,
        "~/.cleveragents/index/vector",
        project_scopable=False,
        env_var="CLEVERAGENTS_INDEX_VECTOR_DIR",
        description="Vector index directory (for local backends).",
    )
    _register(
        "index",
        "vector.qdrant-url",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_QDRANT_URL",
        description="Qdrant server URL (required for qdrant backend).",
    )
    _register(
        "index",
        "graph.backend",
        str,
        "none",
        project_scopable=False,
        env_var="CLEVERAGENTS_INDEX_GRAPH_BACKEND",
        description="Knowledge graph backend: neo4j, rdflib, none.",
    )
    _register(
        "index",
        "graph.neo4j-url",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_NEO4J_URL",
        description="Neo4j server URL (required for neo4j backend).",
    )
    _register(
        "index",
        "graph.neo4j-auth",
        str,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_NEO4J_AUTH",
        description="Neo4j auth in user:password format.",
    )
    _register(
        "index",
        "embedding.provider",
        str,
        "openai",
        project_scopable=False,
        env_var="CLEVERAGENTS_EMBEDDING_PROVIDER",
        description="Embedding provider: openai, anthropic, local.",
    )
    _register(
        "index",
        "embedding.model",
        str,
        "text-embedding-3-small",
        project_scopable=False,
        env_var="CLEVERAGENTS_EMBEDDING_MODEL",
        description="Model for generating vector embeddings.",
    )
    _register(
        "index",
        "embedding.dimensions",
        int,
        None,
        project_scopable=False,
        env_var="CLEVERAGENTS_EMBEDDING_DIMENSIONS",
        description="Embedding vector dimensions (None = provider default).",
    )
    _register(
        "index",
        "auto-reindex",
        bool,
        True,
        project_scopable=False,
        env_var="CLEVERAGENTS_AUTO_REINDEX",
        description="Auto re-index on file changes.",
    )

    # ── context.* (43 keys, all project-scopable) ───────────────────────
    _ctx = "context"
    _ps = True  # all context keys are project-scopable

    _register(
        _ctx,
        "hot.max-tokens",
        int,
        16000,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_HOT_TOKENS",
        description="Max token budget for hot context.",
    )
    _register(
        _ctx,
        "warm.max-decisions",
        int,
        100,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_WARM_DECISIONS",
        description="Max recent decisions in warm context.",
    )
    _register(
        _ctx,
        "cold.max-decisions",
        int,
        500,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_COLD_DECISIONS",
        description="Max historical decisions in cold context.",
    )
    _register(
        _ctx,
        "query.limit",
        int,
        20,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_QUERY_LIMIT",
        description="Max retrieval results per context query.",
    )
    _register(
        _ctx,
        "query.min-relevance",
        float,
        0.3,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_QUERY_MIN_RELEVANCE",
        description="Minimum relevance score (0.0-1.0) for results.",
    )
    _register(
        _ctx,
        "file.max-size",
        int,
        1048576,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_MAX_FILE_SIZE",
        description="Max file size (bytes) for context inclusion.",
    )
    _register(
        _ctx,
        "file.max-total-size",
        int,
        52428800,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_MAX_TOTAL_SIZE",
        description="Max total file size (bytes) across all context files.",
    )
    _register(
        _ctx,
        "summarize.enabled",
        bool,
        True,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_SUMMARIZE",
        description="Summarize large context segments instead of truncating.",
    )
    _register(
        _ctx,
        "summarize.max-tokens",
        int,
        1000,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_SUMMARY_TOKENS",
        description="Max tokens in generated context summaries.",
    )
    _register(
        _ctx,
        "summarize.model",
        str,
        None,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_SUMMARY_MODEL",
        description="Model for summarization (None = actor's model).",
    )
    _register(
        _ctx,
        "strategies.enabled",
        str,
        "simple-keyword,semantic-embedding,breadth-depth-navigator",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_STRATEGIES",
        description="Comma-separated ACMS context strategies.",
    )
    _register(
        _ctx,
        "strategies.arce.model",
        str,
        None,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_ARCE_MODEL",
        description="Model for ARCE strategy reasoning loop.",
    )
    _register(
        _ctx,
        "strategies.arce.max-rounds",
        int,
        3,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_ARCE_ROUNDS",
        description="Max search-refine rounds for ARCE strategy.",
    )
    _register(
        _ctx,
        "strategies.breadth-depth-navigator.max-hops",
        int,
        4,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_BDN_HOPS",
        description="Max graph traversal depth for BDN strategy.",
    )
    _register(
        _ctx,
        "budget.response-reserve-tokens",
        int,
        4096,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_RESPONSE_RESERVE",
        description="Tokens reserved from context window for response.",
    )
    _register(
        _ctx,
        "budget.tool-definition-estimate",
        int,
        None,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_TOOL_ESTIMATE",
        description="Estimated tokens for tool definitions (None = auto).",
    )
    _register(
        _ctx,
        "budget.skeleton-ratio",
        float,
        0.15,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_SKELETON_RATIO",
        description="Fraction of budget for inherited plan skeleton.",
    )
    _register(
        _ctx,
        "budget.refresh-threshold",
        float,
        0.30,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_REFRESH_THRESHOLD",
        description="Budget change fraction triggering re-assembly.",
    )
    _register(
        _ctx,
        "budget.min-useful-budget",
        int,
        500,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_MIN_BUDGET",
        description="Min useful context budget in tokens.",
    )
    _register(
        _ctx,
        "tiers.warm.retention-hours",
        int,
        24,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_WARM_HOURS",
        description="Hours before warm-tier demotion to cold.",
    )
    _register(
        _ctx,
        "tiers.cold.retention-days",
        int,
        90,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_COLD_DAYS",
        description="Days before cold-tier expiry.",
    )
    _register(
        _ctx,
        "uko.default-analyzers",
        str,
        "python,typescript,rust,java,markdown,json-schema",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_UKO_ANALYZERS",
        description="Comma-separated default UKO analyzers.",
    )
    _register(
        _ctx,
        "pipeline.strategy-selector",
        str,
        "builtin:ConfidenceWeightedSelector",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SELECTOR",
        description="StrategySelector implementation class.",
    )
    _register(
        _ctx,
        "pipeline.budget-allocator",
        str,
        "builtin:ProportionalBudgetAllocator",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_ALLOCATOR",
        description="BudgetAllocator implementation class.",
    )
    _register(
        _ctx,
        "pipeline.strategy-executor",
        str,
        "builtin:ParallelStrategyExecutor",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_EXECUTOR",
        description="StrategyExecutor implementation class.",
    )
    _register(
        _ctx,
        "pipeline.fragment-deduplicator",
        str,
        "builtin:ContentHashDeduplicator",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_DEDUP",
        description="FragmentDeduplicator implementation class.",
    )
    _register(
        _ctx,
        "pipeline.detail-depth-resolver",
        str,
        "builtin:MaxDepthResolver",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_DEPTH",
        description="DetailDepthResolver implementation class.",
    )
    _register(
        _ctx,
        "pipeline.fragment-scorer",
        str,
        "builtin:WeightedCompositeScorer",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SCORER",
        description="FragmentScorer implementation class.",
    )
    _register(
        _ctx,
        "pipeline.budget-packer",
        str,
        "builtin:GreedyKnapsackPacker",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PACKER",
        description="BudgetPacker implementation class.",
    )
    _register(
        _ctx,
        "pipeline.fragment-orderer",
        str,
        "builtin:RelevanceCoherenceOrderer",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_ORDERER",
        description="FragmentOrderer implementation class.",
    )
    _register(
        _ctx,
        "pipeline.preamble-generator",
        str,
        "builtin:ProvenancePreambleGenerator",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PREAMBLE",
        description="PreambleGenerator implementation class.",
    )
    _register(
        _ctx,
        "pipeline.skeleton-compressor",
        str,
        "builtin:DepthReductionCompressor",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_COMPRESSOR",
        description="SkeletonCompressor implementation class.",
    )
    _register(
        _ctx,
        "pipeline.strategy-executor.timeout-seconds",
        int,
        30,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_EXEC_TIMEOUT",
        description="Timeout (s) for each strategy execution.",
    )
    _register(
        _ctx,
        "pipeline.strategy-executor.max-workers",
        int,
        4,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_EXEC_WORKERS",
        description="Max parallel workers for strategy execution.",
    )
    _register(
        _ctx,
        "pipeline.strategy-executor.circuit-breaker-threshold",
        int,
        3,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_EXEC_CB",
        description="Failures before circuit breaker opens.",
    )
    _register(
        _ctx,
        "pipeline.fragment-scorer.relevance-weight",
        float,
        0.4,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SCORER_REL",
        description="Relevance weight in composite fragment scoring.",
    )
    _register(
        _ctx,
        "pipeline.fragment-scorer.hierarchy-weight",
        float,
        0.3,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SCORER_HIER",
        description="Hierarchy weight in composite fragment scoring.",
    )
    _register(
        _ctx,
        "pipeline.fragment-scorer.quality-weight",
        float,
        0.2,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SCORER_QUAL",
        description="Quality weight in composite fragment scoring.",
    )
    _register(
        _ctx,
        "pipeline.fragment-scorer.recency-weight",
        float,
        0.1,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_SCORER_REC",
        description="Recency weight in composite fragment scoring.",
    )
    _register(
        _ctx,
        "pipeline.budget-packer.depth-fallback-steps",
        str,
        "9,4,2,0",
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PACKER_STEPS",
        description="Comma-separated depth fallback steps.",
    )
    _register(
        _ctx,
        "pipeline.budget-packer.min-fragment-tokens",
        int,
        10,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PACKER_MIN",
        description="Min token size for fragment inclusion.",
    )
    _register(
        _ctx,
        "pipeline.preamble-generator.enabled",
        bool,
        True,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PREAMBLE_ON",
        description="Whether to produce a context preamble.",
    )
    _register(
        _ctx,
        "pipeline.preamble-generator.max-tokens",
        int,
        200,
        project_scopable=_ps,
        env_var="CLEVERAGENTS_CTX_PIPELINE_PREAMBLE_TOK",
        description="Max tokens for the context preamble.",
    )

    # ── provider.* (11 keys, custom env vars) ───────────────────────────
    _register(
        "provider",
        "openai.api-key",
        str,
        None,
        project_scopable=False,
        env_var="OPENAI_API_KEY",
        description="OpenAI API key.",
    )
    _register(
        "provider",
        "openai.org-id",
        str,
        None,
        project_scopable=False,
        env_var="OPENAI_ORG_ID",
        description="OpenAI organization ID.",
    )
    _register(
        "provider",
        "openai.base-url",
        str,
        None,
        project_scopable=False,
        env_var="OPENAI_BASE_URL",
        description="Custom base URL for OpenAI-compatible APIs.",
    )
    _register(
        "provider",
        "anthropic.api-key",
        str,
        None,
        project_scopable=False,
        env_var="ANTHROPIC_API_KEY",
        description="Anthropic API key.",
    )
    _register(
        "provider",
        "google.api-key",
        str,
        None,
        project_scopable=False,
        env_var="GOOGLE_API_KEY",
        description="Google AI API key.",
    )
    _register(
        "provider",
        "azure.endpoint",
        str,
        None,
        project_scopable=False,
        env_var="AZURE_OPENAI_ENDPOINT",
        description="Azure OpenAI endpoint URL.",
    )
    _register(
        "provider",
        "azure.api-key",
        str,
        None,
        project_scopable=False,
        env_var="AZURE_OPENAI_API_KEY",
        description="Azure OpenAI API key.",
    )
    _register(
        "provider",
        "azure.api-version",
        str,
        "2024-02-01",
        project_scopable=False,
        env_var="AZURE_OPENAI_API_VERSION",
        description="Azure OpenAI API version.",
    )
    _register(
        "provider",
        "google.gemini-api-key",
        str,
        None,
        project_scopable=False,
        env_var="GEMINI_API_KEY",
        description="Google Gemini API key.",
    )
    _register(
        "provider",
        "huggingface.token",
        str,
        None,
        project_scopable=False,
        env_var="HF_TOKEN",
        description="Hugging Face access token.",
    )
    _register(
        "provider",
        "openrouter.api-key",
        str,
        None,
        project_scopable=False,
        env_var="OPENROUTER_API_KEY",
        description="OpenRouter API key.",
    )

    # ── skills.* (1 key) ────────────────────────────────────────────────
    _register(
        "skills",
        "agent_skills_paths",
        str,
        "",
        project_scopable=True,
        env_var="CLEVERAGENTS_SKILLS_AGENT_SKILLS_PATHS",
        description=("Comma-separated filesystem paths for Agent Skills discovery."),
    )


# Build catalog on module import
_build_catalog()


# ---------------------------------------------------------------------------
# ConfigService
# ---------------------------------------------------------------------------

_DEFAULT_CONFIG_DIR: Path = Path.home() / ".cleveragents"
_DEFAULT_CONFIG_PATH: Path = _DEFAULT_CONFIG_DIR / "config.toml"

# Markers that identify a CleverAgents project root directory.
_PROJECT_ROOT_MARKERS: tuple[str, ...] = ("cleveragents.toml", ".cleveragents")


def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    """Recursively merge *override* into *base* (override wins on conflict).

    Both *base* and *override* remain unmodified; a new dict is returned.
    """
    merged: dict[str, Any] = dict(base)
    for key, value in override.items():
        if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
            merged[key] = _deep_merge(merged[key], value)
        else:
            merged[key] = value
    return merged


def discover_project_root(start: Path | None = None) -> Path | None:
    """Walk up from *start* (default ``Path.cwd()``) to find the project root.

    The project root is the nearest ancestor directory that contains one
    of the marker files/directories defined in ``_PROJECT_ROOT_MARKERS``
    (``cleveragents.toml`` or ``.cleveragents/``).

    Returns ``None`` when no project root is found.
    """
    current = (start or Path.cwd()).resolve()
    for directory in (current, *current.parents):
        for marker in _PROJECT_ROOT_MARKERS:
            if (directory / marker).exists():
                return directory
    return None


class ConfigService:
    """Service providing multi-level config resolution with TOML persistence.

    Parameters
    ----------
    config_dir:
        Override for the configuration directory (default ``~/.cleveragents``).
    config_path:
        Override for the TOML file path (default ``config_dir / config.toml``).
    project_root:
        Override for the project root directory.  When ``None`` the service
        will auto-discover the project root by walking up from ``Path.cwd()``.
    """

    def __init__(
        self,
        *,
        config_dir: Path | None = None,
        config_path: Path | None = None,
        event_bus: EventBus | None = None,
        project_root: Path | None = ...,  # type: ignore[assignment]
    ) -> None:
        self._config_dir: Path = config_dir or _DEFAULT_CONFIG_DIR
        self._config_path: Path = config_path or (self._config_dir / "config.toml")
        self._event_bus = event_bus
        # ``...`` means "auto-discover"; ``None`` means "no project root".
        if project_root is ...:
            self._project_root: Path | None = discover_project_root()
        else:
            self._project_root = project_root

    # -- public registry API --------------------------------------------------

    @staticmethod
    def registry() -> dict[str, ConfigEntry]:
        """Return a copy of the full key registry."""
        return dict(_REGISTRY)

    @staticmethod
    def get_entry(key: str) -> ConfigEntry | None:
        """Look up a registry entry by dotted key (e.g. ``core.log_level``)."""
        return _REGISTRY.get(key)

    @staticmethod
    def registered_keys() -> list[str]:
        """Return all registered key names sorted alphabetically."""
        return sorted(_REGISTRY.keys())

    # -- path accessors -------------------------------------------------------

    @property
    def project_root(self) -> Path | None:
        """Return the discovered (or overridden) project root directory."""
        return self._project_root

    @property
    def project_config_path(self) -> Path | None:
        """Return the path to the project-scoped ``config.toml``."""
        if self._project_root is None:
            return None
        return self._project_root / "config.toml"

    @property
    def local_config_path(self) -> Path | None:
        """Return the path to the local-scoped ``config.local.toml``."""
        if self._project_root is None:
            return None
        return self._project_root / "config.local.toml"

    # -- TOML file management -------------------------------------------------

    def read_config(self) -> dict[str, Any]:
        """Read the global TOML config file and return its contents."""
        if not self._config_path.exists():
            return {}
        with open(self._config_path, "rb") as fh:
            return tomllib.load(fh)

    def read_project_config(self) -> dict[str, Any]:
        """Read the project-scoped ``config.toml`` from the project root."""
        path = self.project_config_path
        if path is None or not path.exists():
            return {}
        with open(path, "rb") as fh:
            return tomllib.load(fh)

    def read_local_config(self) -> dict[str, Any]:
        """Read the local-scoped ``config.local.toml`` from the project root."""
        path = self.local_config_path
        if path is None or not path.exists():
            return {}
        with open(path, "rb") as fh:
            return tomllib.load(fh)

    def read_merged_config(self) -> dict[str, Any]:
        """Return the three-scope deep-merged configuration.

        Merge order (lowest to highest priority):
        ``global < project < local``
        """
        merged = self.read_config()
        merged = _deep_merge(merged, self.read_project_config())
        merged = _deep_merge(merged, self.read_local_config())
        return merged

    def write_config(self, data: dict[str, Any]) -> None:
        """Write *data* to the global TOML config file, creating dirs if needed."""
        self._config_dir.mkdir(parents=True, exist_ok=True)

        if self._config_path.exists():
            with open(self._config_path) as fh:
                doc = tomlkit.load(fh)
        else:
            doc = tomlkit.document()

        for key, value in data.items():
            doc[key] = value

        with open(self._config_path, "w") as fh:
            tomlkit.dump(doc, fh)

    def write_scoped_config(self, data: dict[str, Any], scope: ConfigScope) -> None:
        """Write *data* to the config file for the given *scope*.

        Parameters
        ----------
        data:
            Key/value pairs to persist.
        scope:
            Which scope file to write to.

        Raises
        ------
        ValueError
            If the project root is not discovered and scope is
            ``PROJECT`` or ``LOCAL``.
        """
        if scope == ConfigScope.GLOBAL:
            self.write_config(data)
            return

        if self._project_root is None:
            msg = (
                f"Cannot write to {scope.value} scope: "
                "no project root discovered.  "
                "Run from within a CleverAgents project directory."
            )
            raise ValueError(msg)

        if scope == ConfigScope.PROJECT:
            target = self._project_root / "config.toml"
        else:
            target = self._project_root / "config.local.toml"

        if target.exists():
            with open(target) as fh:
                doc = tomlkit.load(fh)
        else:
            doc = tomlkit.document()

        for key, value in data.items():
            doc[key] = value

        with open(target, "w") as fh:
            tomlkit.dump(doc, fh)

    def set_value(
        self, key: str, value: Any, *, scope: ConfigScope | None = None
    ) -> None:
        """Persist a single key-value pair into a scoped TOML config.

        Parameters
        ----------
        key:
            Configuration key.
        value:
            Value to persist.
        scope:
            Target scope.  Defaults to ``ConfigScope.GLOBAL``.
        """
        effective_scope = scope or ConfigScope.GLOBAL

        if effective_scope == ConfigScope.GLOBAL:
            data = self.read_config()
        elif effective_scope == ConfigScope.PROJECT:
            data = self.read_project_config()
        else:
            data = self.read_local_config()

        old_value = data.get(key)
        data[key] = value
        self.write_scoped_config(data, effective_scope)

        if is_sensitive_key(key):
            old_value = REDACTED
            value = REDACTED
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.CONFIG_CHANGED,
                        details={
                            "key": key,
                            "old_value": old_value,
                            "new_value": value,
                            "scope": effective_scope.value,
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="CONFIG_CHANGED")

    # -- validation -----------------------------------------------------------

    @staticmethod
    def validate_key(key: str) -> ConfigEntry:
        """Validate that *key* is a known registered key.

        Raises
        ------
        ValueError
            With an actionable message when the key is unknown.
        """
        entry = _REGISTRY.get(key)
        if entry is None:
            available = ", ".join(sorted(_REGISTRY.keys())[:8])
            msg = (
                f"Unknown configuration key: '{key}'. "
                f"Valid keys include: {available} ..."
            )
            raise ValueError(msg)
        return entry

    @staticmethod
    def validate_type(key: str, value: Any) -> Any:
        """Coerce *value* to the registered type for *key*.

        Raises
        ------
        TypeError
            With an actionable message on type mismatch.
        """
        entry = _REGISTRY.get(key)
        if entry is None:
            msg = f"Cannot validate type for unknown key: '{key}'"
            raise ValueError(msg)

        if isinstance(value, entry.python_type):
            return value

        # Coerce from string representations
        try:
            if entry.python_type is bool and isinstance(value, str):
                if value.lower() in ("true", "1", "yes"):
                    return True
                if value.lower() in ("false", "0", "no"):
                    return False
                msg = (
                    f"Cannot convert '{value}' to bool for key '{key}'. "
                    f"Use 'true'/'false', '1'/'0', or 'yes'/'no'."
                )
                raise TypeError(msg)
            if entry.python_type is int:
                return int(value)
            if entry.python_type is float:
                return float(value)
            if entry.python_type is str:
                return str(value)
        except (ValueError, TypeError) as exc:
            if isinstance(exc, TypeError):
                raise
            msg = (
                f"Type mismatch for key '{key}': expected "
                f"{entry.python_type.__name__}, got {type(value).__name__} "
                f"with value '{value}'."
            )
            raise TypeError(msg) from exc

        msg = (
            f"Type mismatch for key '{key}': expected "
            f"{entry.python_type.__name__}, got {type(value).__name__}."
        )
        raise TypeError(msg)

    # -- resolution chain -----------------------------------------------------

    def resolve(
        self,
        key: str,
        *,
        cli_value: Any | None = None,
        project_name: str | None = None,
        verbose: bool = False,
    ) -> ResolvedValue:
        """Resolve *key* through the six-level precedence chain.

        Parameters
        ----------
        key:
            Dotted config key (e.g. ``core.log_level``).
        cli_value:
            Value explicitly passed via CLI flag (highest priority).
        project_name:
            Optional project name for project-scoped lookup.
        verbose:
            When ``True``, the full resolution chain is populated.

        Returns
        -------
        ResolvedValue
            Contains the winning value, its source level, and (if
            *verbose*) the full resolution chain for display.

        Precedence (highest → lowest)::

            CLI flag > env var > local (config.local.toml)
                     > project (config.toml) > global > default
        """
        entry = self.validate_key(key)
        chain: list[dict[str, Any]] = []
        winner_value: Any | None = None
        winner_source: ConfigLevel | None = None

        # Level 1: CLI flag
        if cli_value is not None:
            coerced = self.validate_type(key, cli_value)
            if verbose:
                chain.append({"source": ConfigLevel.CLI_FLAG.value, "value": coerced})
            if winner_source is None:
                winner_value = coerced
                winner_source = ConfigLevel.CLI_FLAG
        elif verbose:
            chain.append({"source": ConfigLevel.CLI_FLAG.value, "value": None})

        # Level 2: Environment variable
        env_val = os.environ.get(entry.env_var)
        if env_val is not None and winner_source is None:
            coerced = self.validate_type(key, env_val)
            if verbose:
                chain.append(
                    {
                        "source": ConfigLevel.ENV_VAR.value,
                        "value": coerced,
                        "env_name": entry.env_var,
                    }
                )
            winner_value = coerced
            winner_source = ConfigLevel.ENV_VAR
        elif verbose:
            chain.append(
                {
                    "source": ConfigLevel.ENV_VAR.value,
                    "value": None,
                    "env_name": entry.env_var,
                }
            )

        # Level 3: Local config (config.local.toml in project root)
        local_val: Any | None = None
        if winner_source is None:
            local_data = self.read_local_config()
            local_val = local_data.get(key)
        if local_val is not None and winner_source is None:
            coerced = self.validate_type(key, local_val)
            if verbose:
                chain.append(
                    {
                        "source": ConfigLevel.LOCAL.value,
                        "value": coerced,
                        "path": str(self.local_config_path or ""),
                    }
                )
            winner_value = coerced
            winner_source = ConfigLevel.LOCAL
        elif verbose:
            chain.append(
                {
                    "source": ConfigLevel.LOCAL.value,
                    "value": None,
                    "path": str(self.local_config_path or ""),
                }
            )

        # Level 4: Project-scoped config (config.toml in project root,
        #           or legacy per-project section in global config)
        project_val: Any | None = None
        if winner_source is None:
            # First try file-based project config
            proj_file_data = self.read_project_config()
            project_val = proj_file_data.get(key)

            # Fall back to legacy per-project section in global config
            if project_val is None and project_name and entry.project_scopable:
                config_data = self.read_config()
                project_section = config_data.get("project", {})
                if isinstance(project_section, dict):
                    project_overrides = project_section.get(project_name, {})
                    if isinstance(project_overrides, dict):
                        project_val = project_overrides.get(key)

        if project_val is not None and winner_source is None:
            coerced = self.validate_type(key, project_val)
            if verbose:
                chain.append(
                    {
                        "source": ConfigLevel.PROJECT.value,
                        "value": coerced,
                        "path": str(self.project_config_path or ""),
                    }
                )
            winner_value = coerced
            winner_source = ConfigLevel.PROJECT
        elif verbose:
            chain.append(
                {
                    "source": ConfigLevel.PROJECT.value,
                    "value": None,
                    "path": str(self.project_config_path or ""),
                }
            )

        # Level 5: Global config file
        global_val: Any | None = None
        if winner_source is None:
            config_data = self.read_config()
            global_val = config_data.get(key)
        if global_val is not None and winner_source is None:
            coerced = self.validate_type(key, global_val)
            if verbose:
                chain.append(
                    {
                        "source": ConfigLevel.GLOBAL.value,
                        "value": coerced,
                        "path": str(self._config_path),
                    }
                )
            winner_value = coerced
            winner_source = ConfigLevel.GLOBAL
        elif verbose:
            chain.append(
                {
                    "source": ConfigLevel.GLOBAL.value,
                    "value": None,
                    "path": str(self._config_path),
                }
            )

        # Level 6: Default
        if verbose:
            chain.append({"source": ConfigLevel.DEFAULT.value, "value": entry.default})
        if winner_source is None:
            winner_value = entry.default
            winner_source = ConfigLevel.DEFAULT

        return ResolvedValue(
            key=key,
            value=winner_value,
            source=winner_source,
            chain=chain,
        )

    # -- env var interpolation ------------------------------------------------

    @staticmethod
    def env_var_for_key(key: str) -> str:
        """Return the env var name for a registered key.

        Raises
        ------
        ValueError
            If the key is not registered.
        """
        entry = _REGISTRY.get(key)
        if entry is None:
            msg = f"Unknown configuration key: '{key}'"
            raise ValueError(msg)
        return entry.env_var

    # -- project-scoped helpers -----------------------------------------------

    def set_project_value(
        self,
        project_name: str,
        key: str,
        value: Any,
    ) -> None:
        """Persist a single key-value pair under a project-scoped TOML table.

        The value is stored at ``[project."<project_name>"].<key>`` in the
        global config file.

        Note: This method does **not** emit a ``CONFIG_CHANGED`` event.
        Per the spec (§Audit Logging), ``config_changed`` is triggered by
        ``agents config set`` which operates on global configuration only.
        Project-scoped config changes are governed by ``agents project
        context set`` and are not classified as security-relevant audit
        events in the current spec revision.

        Parameters
        ----------
        project_name:
            Name of the project.
        key:
            Dotted config key.
        value:
            Value to store (must match the registered type).

        Raises
        ------
        ValueError
            If *key* is not a registered configuration key.
        ValueError
            If *project_name* is empty.
        TypeError
            If *value* does not match the key's registered type.
        """
        if not project_name:
            msg = "project_name must not be empty."
            raise ValueError(msg)
        entry = self.validate_key(key)
        if not entry.project_scopable:
            msg = f"Key '{key}' is not project-scopable."
            raise ValueError(msg)
        coerced: Any = self.validate_type(key, value)

        data = self.read_config()
        project_section: dict[str, Any] = data.get("project", {})
        if not isinstance(project_section, dict):
            project_section = {}
        proj_overrides: dict[str, Any] = project_section.get(project_name, {})
        if not isinstance(proj_overrides, dict):
            proj_overrides = {}
        proj_overrides[key] = coerced
        project_section[project_name] = proj_overrides
        data["project"] = project_section
        self.write_config(data)

    def get_project_overrides(
        self,
        project_name: str,
    ) -> dict[str, Any]:
        """Return all project-scoped overrides for *project_name*.

        Returns a ``{key: value}`` mapping of every key that has been
        explicitly set in the ``[project."<project_name>"]`` TOML table.
        Keys that are not present in the project table are omitted.

        Parameters
        ----------
        project_name:
            Name of the project.

        Raises
        ------
        ValueError
            If *project_name* is empty.
        """
        if not project_name:
            msg = "project_name must not be empty."
            raise ValueError(msg)
        data = self.read_config()
        project_section: dict[str, Any] = data.get("project", {})
        if not isinstance(project_section, dict):
            return {}
        proj_data: dict[str, Any] = project_section.get(project_name, {})
        if not isinstance(proj_data, dict):
            return {}
        return dict(proj_data)

    # -- bulk helpers ---------------------------------------------------------

    def resolve_all(
        self,
        *,
        cli_overrides: dict[str, Any] | None = None,
        project_name: str | None = None,
    ) -> dict[str, ResolvedValue]:
        """Resolve every registered key and return the results."""
        overrides = cli_overrides or {}
        results: dict[str, ResolvedValue] = {}
        for key in _REGISTRY:
            results[key] = self.resolve(
                key,
                cli_value=overrides.get(key),
                project_name=project_name,
            )
        return results
