"""
EvenAge v3 lightweight config helpers.

This module exposes helpers to construct bus, storage, tracer, and to
resolve agents dynamically from the 'agents' package if present.
"""

from __future__ import annotations

import importlib
from typing import Dict, Any
import os

from .tracing import TraceLogger, NoopTracer
from .factory import BackendFactory


def get_bus():
    # Build bus via BackendFactory honoring runtime config
    try:
        cfg = load_runtime_config()
    except Exception:
        from .config import EvenAgeConfig
        cfg = EvenAgeConfig()
    return BackendFactory(cfg).create_queue_backend()


def get_storage():
    # Return configured storage backend (MinIO or in-memory)
    try:
        cfg = load_runtime_config()
    except Exception:
        from .config import EvenAgeConfig
        cfg = EvenAgeConfig()
    return BackendFactory(cfg).create_storage_backend()


def get_tracer():
    db_url = os.environ.get("DATABASE_URL")
    if db_url:
        return TraceLogger(db_url)
    return NoopTracer()


def load_agents() -> Dict[str, Any]:
    """Load agent classes dynamically from 'agents.<name>.handler'.

    Expects each handler to expose a class named <Name>Agent with a default constructor.
    If absent, skips that agent.
    """
    discovered: Dict[str, Any] = {}
    try:
        agents_pkg = importlib.import_module("agents")
        import pkgutil

        for m in pkgutil.iter_modules(agents_pkg.__path__):
            name = m.name
            try:
                mod = importlib.import_module(f"agents.{name}.handler")
                cls_name = f"{name.capitalize()}Agent"
                AgentCls = getattr(mod, cls_name)
                discovered[name] = AgentCls()  # type: ignore[call-arg]
            except Exception:
                continue
    except Exception:
        pass
    return discovered
"""
Configuration management for EvenAge.

Provides pluggable backend configuration via environment and YAML.
All integrations (queue, database, vector, LLM, cache) are configurable.
"""

from pathlib import Path
from typing import Any, Protocol

from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
import yaml


class EvenAgeConfig(BaseSettings):
    """
    Environment-based configuration for EvenAge runtime.
    
    Supports pluggable backends for all infrastructure:
    - queue_backend: "redis" (default) | "memory"
    - db_backend: "postgres" (default) | "sqlite"
    - vector_backend: "local" (default) | "pinecone" | "milvus"
    - llm_backend: "noop" (default) | "openai" | "anthropic" | "gemini"
    - cache_backend: "inmemory" (default) | "redis"
    """

    # Pluggable backend selection
    queue_backend: str = Field(default="redis", description="Message queue backend")
    db_backend: str = Field(default="postgres", description="Database backend")
    vector_backend: str = Field(default="local", description="Vector store backend")
    llm_backend: str = Field(default="noop", description="LLM backend (noop=echo)")
    llm_model: str | None = Field(default=None, description="LLM model name (optional, backend-specific)")
    openai_base_url: str | None = Field(default=None, description="OpenAI-compatible base URL (e.g., vLLM)")
    cache_backend: str = Field(default="inmemory", description="Cache backend")

    # Database connection
    database_url: str = Field(
        default="postgresql://postgres:postgres@localhost:5432/evenage",
        description="Database connection URL (postgres or sqlite)",
    )

    # Redis connection (for queue and optional cache)
    redis_url: str = Field(
        default="redis://localhost:6379", description="Redis connection URL"
    )

    # Kafka (optional)
    kafka_bootstrap: str | None = Field(default=None, description="Kafka bootstrap servers (host:port)")

    # Storage selection and MinIO/S3 settings
    storage_backend: str = Field(default="minio", description="Storage backend: minio | local")
    minio_endpoint: str = Field(default="localhost:9000", description="MinIO endpoint")
    minio_access_key: str = Field(default="minioadmin", description="MinIO access key")
    minio_secret_key: str = Field(
        default="minioadmin123", description="MinIO secret key"
    )
    minio_secure: bool = Field(default=False, description="Use HTTPS for MinIO")
    minio_bucket: str = Field(default="evenage", description="Default bucket name")

    # Tracing and metrics (stored in DB, not external services)
    enable_tracing: bool = Field(default=True, description="Enable internal tracing to DB")
    enable_metrics: bool = Field(default=True, description="Enable internal metrics collection")

    # Large Response Storage
    enable_large_response_storage: bool = Field(
        default=True, description="Enable automatic storage of large responses in MinIO"
    )
    storage_threshold_kb: int = Field(
        default=100, description="Size threshold in KB for storing responses in MinIO"
    )

    # Inline workers: when running locally via AgentRunner, also spin up
    # lightweight in-process workers to consume delegated tasks without Docker
    enable_inline_workers: bool = Field(
        default=True,
        description="Run inline local workers during AgentRunner.run() so delegated tasks execute without external workers",
    )

    # API
    api_host: str = Field(default="0.0.0.0", description="API server host")
    api_port: int = Field(default=8000, description="API server port")
    api_cors_origins: list[str] = Field(
        default_factory=lambda: ["http://localhost:5173"],
        description="Allowed CORS origins (avoid * in production)"
    )

    # Dashboard URL (for containerized setups)
    dashboard_url: str = Field(
        default="http://localhost:5173",
        description="Dashboard URL for links from API"
    )

    # Agent worker settings
    agent_name: str | None = Field(
        default=None, description="Agent name for worker mode"
    )
    worker_concurrency: int = Field(
        default=1, description="Number of concurrent tasks per worker"
    )

    # Coordinator-Specialist Architecture
    has_coordinator: bool = Field(
        default=False,
        description="Enable coordinator-specialist mode where all results route to coordinator"
    )
    coordinator_agent: str = Field(
        default="coordinator",
        description="Name of the coordinator agent (used when has_coordinator=True)"
    )
    
    # MCP (Model Context Protocol) Integration
    mcp_servers: list[dict[str, str]] = Field(
        default_factory=list,
        description="List of MCP server configs with 'url' and optional 'api_key'"
    )

    # LLM API keys (optional, used when respective llm_backend is selected)
    gemini_api_key: str | None = Field(
        default=None, description="Google Gemini API key"
    )
    openai_api_key: str | None = Field(
        default=None, description="OpenAI API key"
    )
    anthropic_api_key: str | None = Field(
        default=None, description="Anthropic API key"
    )
    groq_api_key: str | None = Field(
        default=None, description="Groq API key"
    )
    http_llm_endpoint: str | None = Field(default=None, description="Generic HTTP LLM endpoint URL")

    # Tool API keys (optional)
    serper_api_key: str | None = Field(
        default=None, description="Serper.dev API key for web search"
    )

    # Dashboard auth (optional)
    evenage_dash_users: str | None = Field(
        default=None, description="Dashboard user credentials (user:pass)"
    )

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"
        extra = "ignore"  # Ignore unknown fields from environment


# Backend protocols for pluggable implementations


class QueueBackend(Protocol):
    """Protocol for message queue backends."""

    async def register_agent(self, name: str, metadata: dict) -> bool: ...
    async def publish_task(self, agent_name: str, task: dict) -> str: ...
    async def publish_response(self, task_id: str, response: dict) -> bool: ...
    async def consume_tasks(self, agent_name: str, block_ms: int, count: int) -> list[dict]: ...
    async def wait_for_response(self, task_id: str, timeout_sec: int) -> dict | None: ...
    async def get_registered_agents(self) -> dict[str, dict]: ...
    async def get_queue_depth(self, agent_name: str) -> int: ...
    async def health_check(self) -> bool: ...


class DatabaseBackend(Protocol):
    """Protocol for database backends."""

    def create_job(self, job_id: str, pipeline: str, inputs: dict) -> None: ...
    def save_result(self, job_id: str, outputs: dict) -> None: ...
    def get_result(self, job_id: str) -> dict | None: ...
    def list_jobs(self, limit: int) -> list[dict]: ...
    def append_trace(self, job_id: str, agent_name: str, event_type: str, payload: dict) -> None: ...
    def memory_put(self, agent_name: str, key: str, value: Any, job_id: str | None = None) -> None: ...
    def memory_get(self, agent_name: str, key: str, job_id: str | None = None) -> Any: ...
    def register_agent(self, agent_name: str, metadata: dict) -> None: ...
    def list_agents(self) -> list[dict]: ...


class VectorBackend(Protocol):
    """Protocol for vector store backends."""

    def store(self, key: str, vector: list[float], metadata: dict) -> None: ...
    def search(self, vector: list[float], k: int) -> list[dict]: ...
    def delete(self, key: str) -> None: ...


class LLMBackend(Protocol):
    """Protocol for LLM backends."""

    async def generate(self, prompt: str, system: str | None = None, **kwargs) -> str: ...


class CacheBackend(Protocol):
    """Protocol for cache backends."""

    def get(self, key: str) -> Any: ...
    def set(self, key: str, value: Any, ttl: int | None = None) -> None: ...
    def delete(self, key: str) -> None: ...
    def clear(self) -> None: ...


class BackendFactory:
    """
    Factory for creating backend instances based on configuration.
    
    Provides pluggable implementations for queue, database, vector, LLM, and cache.
    """

    def __init__(self, config: EvenAgeConfig):
        self.config = config

    # Singleton memory bus shared across agents within a process
    _memory_bus_singleton = None

    def create_queue_backend(self) -> QueueBackend:
        """Create message queue backend based on config."""
        if self.config.queue_backend == "redis":
            from .backends.queue import RedisBus
            bus = RedisBus(self.config.redis_url)
        elif self.config.queue_backend == "memory":
            from .backends.queue import MemoryBus
            if BackendFactory._memory_bus_singleton is None:
                BackendFactory._memory_bus_singleton = MemoryBus()
            bus = BackendFactory._memory_bus_singleton
        elif self.config.queue_backend == "kafka":
            from .backends.queue import KafkaBus
            if not self.config.kafka_bootstrap:
                raise ValueError("kafka_bootstrap is required for kafka queue_backend")
            bus = KafkaBus(self.config.kafka_bootstrap)
        else:
            raise ValueError(f"Unknown queue_backend: {self.config.queue_backend}")

        # Optionally enable large-response offload to storage
        try:
            if getattr(self.config, "enable_large_response_storage", False):
                storage = self.create_storage_backend()
                bus._storage = storage
                bus._storage_bucket = self.config.minio_bucket
                bus._storage_threshold_kb = self.config.storage_threshold_kb
        except Exception:
            # Non-fatal: continue without offload
            pass

        return bus

    def create_database_backend(self) -> DatabaseBackend:
        """Create database backend based on config."""
        from .backends.database import DatabaseService
        return DatabaseService(self.config.database_url)

    def create_vector_backend(self) -> VectorBackend:
        """Create vector store backend based on config."""
        if self.config.vector_backend == "local":
            from .backends.vector import LocalVectorStore
            return LocalVectorStore()
        if self.config.vector_backend == "pgvector":
            from .backends.vector import PostgresVectorStore
            return PostgresVectorStore(self.config.database_url)
        raise ValueError(f"Unknown vector_backend: {self.config.vector_backend}")

    def create_llm_backend(self) -> LLMBackend:
        """Create LLM backend based on config."""
        if self.config.llm_backend == "noop":
            from .llm import NoOpLLM
            return NoOpLLM()
        if self.config.llm_backend == "openai":
            from .llm import OpenAILLM
            return OpenAILLM(self.config.openai_api_key, self.config.llm_model, base_url=self.config.openai_base_url)
        if self.config.llm_backend == "anthropic":
            from .llm import AnthropicLLM
            return AnthropicLLM(self.config.anthropic_api_key, self.config.llm_model)
        if self.config.llm_backend == "gemini":
            from .llm import GeminiLLM
            return GeminiLLM(self.config.gemini_api_key, self.config.llm_model)
        if self.config.llm_backend == "http":
            from .llm import HTTPLLM
            if not self.config.http_llm_endpoint:
                raise ValueError("http_llm_endpoint is required for http llm_backend")
            return HTTPLLM(self.config.http_llm_endpoint)
        if self.config.llm_backend == "vllm":
            from .llm import OpenAILLM
            if not self.config.openai_base_url:
                raise ValueError("openai_base_url is required for vllm llm_backend")
            return OpenAILLM(self.config.openai_api_key, self.config.llm_model, base_url=self.config.openai_base_url)
        raise ValueError(f"Unknown llm_backend: {self.config.llm_backend}")

    def create_cache_backend(self) -> CacheBackend:
        """Create cache backend based on config."""
        if self.config.cache_backend == "inmemory":
            from .cache import InMemoryCache
            return InMemoryCache()
        if self.config.cache_backend == "redis":
            from .cache import RedisCache
            return RedisCache(self.config.redis_url)
        raise ValueError(f"Unknown cache_backend: {self.config.cache_backend}")

    def create_storage_backend(self):
        """Create storage backend for artifacts (MinIO/S3)."""
        if self.config.storage_backend == "minio" and self.config.minio_endpoint:
            from .backends.storage import MinIOStorage
            return MinIOStorage(
                endpoint=self.config.minio_endpoint,
                access_key=self.config.minio_access_key,
                secret_key=self.config.minio_secret_key,
                secure=self.config.minio_secure
            )
        # Fall back to in-memory storage if no MinIO configured
        from .backends.storage import InMemoryStorage
        return InMemoryStorage()


# --- Runtime config loader (JSON-driven) ---

def _find_project_root(start: Path | None = None) -> Path | None:
    """Locate project root by looking for evenage.yml or .evenage/ directory."""
    current = start or Path.cwd()
    for directory in [current, *current.parents]:
        if (directory / "evenage.yml").exists() or (directory / ".evenage").exists():
            return directory
    return None


def load_runtime_config() -> EvenAgeConfig:
    """Load EvenAgeConfig, honoring .evenage/config.json when present.

    Rules:
    - If .evenage/config.json exists, map its fields to EvenAgeConfig overrides
    - If enable_scaling is false (or absent), default to local lightweight mode:
        queue_backend=memory, db_backend=sqlite, database_url=sqlite:///./evenage.db, storage_backend=local
    - If enable_scaling is true, use selected backends with sensible defaults if URLs missing
    - Environment variables (via BaseSettings) still apply as base; JSON overrides take precedence
    """
    base = EvenAgeConfig()  # load from env/.env

    root = _find_project_root()
    if not root:
        return base

    json_path = root / ".evenage" / "config.json"
    if not json_path.exists():
        return base

    try:
        import json
        data = json.loads(json_path.read_text(encoding="utf-8"))
    except Exception:
        return base

    enable_scaling = bool(data.get("enable_scaling", False))
    queue_backend = data.get("queue_backend") or ("redis" if enable_scaling else "memory")
    db_backend = data.get("db_backend") or ("postgres" if enable_scaling else "sqlite")
    storage_backend = data.get("storage_backend") or ("minio" if enable_scaling else "local")

    overrides: dict[str, Any] = {
        "queue_backend": queue_backend,
        "db_backend": db_backend,
        "storage_backend": storage_backend,
    }

    # URLs / endpoints
    if db_backend == "sqlite":
        overrides["database_url"] = "sqlite:///./evenage.db"
    elif db_backend == "postgres" and not getattr(base, "database_url", None):
        overrides["database_url"] = "postgresql://evenage:evenage@localhost:5432/evenage"

    if queue_backend == "redis" and not getattr(base, "redis_url", None):
        overrides["redis_url"] = "redis://localhost:6379"

    if storage_backend == "minio":
        # Use defaults unless already set via env
        if not getattr(base, "minio_endpoint", None):
            overrides["minio_endpoint"] = "localhost:9000"
        overrides.setdefault("minio_access_key", base.minio_access_key)
        overrides.setdefault("minio_secret_key", base.minio_secret_key)
        overrides.setdefault("minio_secure", base.minio_secure)
    else:
        # Local storage: disable minio usage
        overrides["minio_endpoint"] = ""

    # Vector/LLM/cache remain as in env unless we choose to derive them later

    return EvenAgeConfig(**{**base.model_dump(), **overrides})



class ProjectConfig(BaseModel):
    """
    Project configuration from evenage.yml.
    
    Defines which agents exist and backend preferences.
    """

    name: str = Field(description="Project name")

    # Backend selections (can override env defaults)
    queue_backend: str = Field(default="redis", description="Message queue backend")
    db_backend: str = Field(default="postgres", description="Database backend")
    vector_backend: str = Field(default="local", description="Vector store backend")
    llm_backend: str = Field(default="noop", description="LLM backend")
    cache_backend: str = Field(default="inmemory", description="Cache backend")

    # Agent registry
    agents: list[str] = Field(default_factory=list, description="List of agent names")


class AgentConfig(BaseModel):
    """
    Agent configuration (minimal for @actor pattern).
    
    Most runtime config is auto-discovered or comes from EvenAgeConfig.
    """

    name: str
    role: str
    goal: str
    backstory: str | None = None
    max_iterations: int = Field(default=15)
    allow_delegation: bool = Field(default=True)
    verbose: bool = Field(default=False)


class PipelineStage(BaseModel):
    """Stage definition in a pipeline (simplified)."""

    agent: str = Field(description="Agent name to execute")
    inputs: dict[str, Any] = Field(default_factory=dict, description="Stage inputs")


class PipelineConfig(BaseModel):
    """Pipeline configuration for sequential agent execution."""

    name: str
    description: str | None = None
    stages: list[PipelineStage]


def load_project_config(path: Path | str = "evenage.yml") -> ProjectConfig:
    """Load project configuration from YAML file."""
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"Configuration file not found: {path}")

    with open(path) as f:
        data = yaml.safe_load(f)

    return ProjectConfig(**data.get("project", {}))


def save_project_config(config: ProjectConfig, path: Path | str = "evenage.yml") -> None:
    """Save project configuration to YAML file."""
    path = Path(path)
    data = {"project": config.model_dump(exclude_none=True)}

    with open(path, "w") as f:
        yaml.dump(data, f, default_flow_style=False, sort_keys=False)


def load_pipeline_config(path: Path | str) -> PipelineConfig:
    """Load pipeline configuration from YAML file."""
    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"Pipeline configuration not found: {path}")

    with open(path) as f:
        data = yaml.safe_load(f)

    return PipelineConfig(**data)


# --- Code-first infra convenience ---


def connect_infrastructure(config: EvenAgeConfig):
    """
    Explicitly connect all core infrastructure from a config instance.

    Returns a lightweight object with attributes:
    bus, database, vector, llm, storage, cache
    """
    from evenage.infra import Infra

    factory = BackendFactory(config)

    # Core backends
    bus = factory.create_queue_backend()
    database = factory.create_database_backend()
    storage = factory.create_storage_backend()

    # Optional backends
    try:
        vector = factory.create_vector_backend()
    except Exception:
        vector = None

    try:
        llm = factory.create_llm_backend()
    except Exception:
        llm = None

    cache = factory.create_cache_backend()

    return Infra(
        bus=bus,
        database=database,
        vector=vector,
        llm=llm,
        storage=storage,
        cache=cache,
    )


