"""AgentConfig — system configuration loader for the oedix framework.

Reads ``agent.json`` (or a supplied dict) and exposes typed attributes used
by AgentQueue, ActionSet, Actions, and the LLM client.
"""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from typing import Any, Optional


@dataclass
class LLMModelConfig:
    """Configuration for a single LLM model."""

    model_id: str
    description: str = ""
    context_length: int = 128000
    max_output_tokens: int = 8192
    extra: dict[str, Any] = field(default_factory=dict)


@dataclass
class LLMProviderConfig:
    """Configuration for a single LLM provider."""

    api_key: str = ""
    base_url: str = ""
    models: dict[str, LLMModelConfig] = field(default_factory=dict)


@dataclass
class AgentConfig:
    """Centralised configuration consumed by the oedix pipeline.

    Attributes:
        data_dir: Root path for the data directory.
        max_queue_count: Maximum number of ActionSets the AgentQueue will
            execute (system limit, not changeable by agent).
        max_action_count: Maximum actions per ActionSet (before weight
            scaling).
        max_context_size: Maximum allowed context size (tokens or chars).
        context_compress_threshold: Fraction of *max_context_size* above
            which the organizer auto-compresses on exit (default 0.7).
        default_provider: LLM provider key (e.g. ``"openai"``).
        default_model: LLM model key (e.g. ``"gpt-5.4"``).
        providers: Per-provider configuration.
    """

    # Paths
    data_dir: str = ""

    # Pipeline limits
    max_queue_count: int = 20
    max_action_count: int = 15
    max_context_size: int = 128000
    context_compress_threshold: float = 0.7

    # LLM
    default_provider: str = "openai"
    default_model: str = "gpt-5.4"
    providers: dict[str, LLMProviderConfig] = field(default_factory=dict)

    # Raw data (for forward-compat / custom keys)
    _raw: dict[str, Any] = field(default_factory=dict, repr=False)

    # ------------------------------------------------------------------
    # Loaders
    # ------------------------------------------------------------------

    @classmethod
    def from_file(
        cls,
        path: str,
        *,
        data_dir: Optional[str] = None,
    ) -> AgentConfig:
        """Load configuration from a JSON file.

        Args:
            path: Path to the JSON config file.
            data_dir: Override ``data_dir`` after loading.  When provided,
                this value replaces whatever ``paths.data_root`` is set to
                in the file.
        """
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        config = cls.from_dict(data)
        if data_dir:
            config.data_dir = data_dir
        return config

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> AgentConfig:
        """Build config from a raw dictionary (e.g. parsed agent.json).

        Supports both new top-level keys and legacy nested keys.
        Top-level keys take precedence when both exist.

        Key mappings (new → legacy fallback):
        - ``max_queue_count`` → ``agent.max_iterations``
        - ``max_action_count`` → ``action_set.max_total_actions``
        - ``max_context_size`` (new, no legacy equivalent)
        - ``context_compress_threshold`` (new, no legacy equivalent)
        - ``paths.data_root`` → ``data_dir``
        """
        paths = data.get("paths", {})
        agent = data.get("agent", {})
        action_set = data.get("action_set", {})
        llm = data.get("llm", {})

        # Provider configs
        providers: dict[str, LLMProviderConfig] = {}
        for pname, pdata in llm.get("providers", {}).items():
            models: dict[str, LLMModelConfig] = {}
            for mname, mdata in pdata.get("models", {}).items():
                models[mname] = LLMModelConfig(
                    model_id=mdata.get("model_id", mname),
                    description=mdata.get("description", ""),
                    context_length=mdata.get("context_length", 128000),
                    max_output_tokens=mdata.get("max_output_tokens", 8192),
                    extra=mdata.get("extra", {}),
                )
            providers[pname] = LLMProviderConfig(
                api_key=pdata.get("api_key", ""),
                base_url=pdata.get("base_url", ""),
                models=models,
            )

        return cls(
            data_dir=paths.get("data_root", data.get("data_dir", "")),
            max_queue_count=data.get(
                "max_queue_count",
                agent.get("max_iterations", 20),
            ),
            max_action_count=data.get(
                "max_action_count",
                action_set.get("max_total_actions", 15),
            ),
            max_context_size=data.get("max_context_size", 128000),
            context_compress_threshold=data.get("context_compress_threshold", 0.7),
            default_provider=llm.get("default_provider", "openai"),
            default_model=llm.get("default_model", "gpt-5.4"),
            providers=providers,
            _raw=data,
        )

    # ------------------------------------------------------------------
    # Derived paths
    # ------------------------------------------------------------------

    def actions_dir(self) -> str:
        """Path to the dynamic Action plugins directory."""
        return os.path.join(self.data_dir, "actions")

    def instruction_dir(self) -> str:
        """Path to the Instruction knowledge directory."""
        return os.path.join(self.data_dir, "instruction")

    def tools_dir(self) -> str:
        """Path to the Tool definitions directory."""
        return os.path.join(self.data_dir, "tools")

    # ------------------------------------------------------------------
    # LLM helpers
    # ------------------------------------------------------------------

    def get_provider(self, name: Optional[str] = None) -> LLMProviderConfig:
        """Return a provider config by name, falling back to default."""
        key = name or self.default_provider
        if key not in self.providers:
            raise KeyError(f"LLM provider '{key}' not configured")
        return self.providers[key]

    def get_model(
        self,
        model_name: Optional[str] = None,
        provider_name: Optional[str] = None,
    ) -> LLMModelConfig:
        """Return a model config, falling back to defaults."""
        provider = self.get_provider(provider_name)
        key = model_name or self.default_model
        if key not in provider.models:
            raise KeyError(
                f"Model '{key}' not found in provider '{provider_name or self.default_provider}'"
            )
        return provider.models[key]

    # ------------------------------------------------------------------
    # Serialisation
    # ------------------------------------------------------------------

    def to_dict(self) -> dict[str, Any]:
        return {
            "data_dir": self.data_dir,
            "max_queue_count": self.max_queue_count,
            "max_action_count": self.max_action_count,
            "max_context_size": self.max_context_size,
            "context_compress_threshold": self.context_compress_threshold,
            "default_provider": self.default_provider,
            "default_model": self.default_model,
        }
