from __future__ import annotations

import inspect
import json
import os
import tomllib
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any, cast

import yaml
from pydantic import BaseModel, ConfigDict, Field

from oneiric.runtime.health import default_runtime_health_path

from .lifecycle import LifecycleError, LifecycleManager
from .logging import LoggingConfig, get_logger
from .protocols import SecretsCacheProtocol, SecretsProviderProtocol
from .resolution import ResolverSettings
from .secrets_cache import SecretValueCache

logger = get_logger("config")


class AppConfig(BaseModel):
    name: str = "oneiric"
    environment: str = "dev"
    debug: bool = False


class LayerSettings(BaseModel):
    selections: dict[str, str] = Field(
        default_factory=dict, description="category -> provider mapping"
    )
    provider_settings: dict[str, dict[str, Any]] = Field(
        default_factory=dict,
        description="per-provider configuration payloads",
    )
    options: dict[str, Any] = Field(
        default_factory=dict,
        description="Domain-specific configuration knobs (e.g., workflow queue defaults).",
    )


class SecretsConfig(BaseModel):
    domain: str = "adapter"
    key: str = "secrets"
    provider: str | None = None
    inline: dict[str, str] = Field(default_factory=dict)
    cache_ttl_seconds: float = Field(
        default=600.0,
        ge=0.0,
        description="Number of seconds to cache resolved secrets (0 disables cache).",
    )
    refresh_interval: float | None = Field(
        default=None,
        ge=1.0,
        description="Seconds between secrets cache invalidations (None disables).",
    )


class RemoteAuthConfig(BaseModel):
    header_name: str = "Authorization"
    secret_id: str | None = None
    token: str | None = None


class RemoteSourceConfig(BaseModel):
    enabled: bool = False
    manifest_url: str | None = None
    cache_dir: str = ".oneiric_cache"
    verify_tls: bool = True
    auth: RemoteAuthConfig = Field(default_factory=RemoteAuthConfig)
    signature_required: bool = Field(
        default=False,
        description="Reject unsigned manifests when True.",
    )
    signature_threshold: int = Field(
        default=1,
        ge=1,
        description="Number of valid signatures required to accept a manifest.",
    )
    signature_max_age_seconds: float | None = Field(
        default=None,
        ge=0.0,
        description="Optional maximum age (seconds) for signed manifests.",
    )
    signature_require_expiry: bool = Field(
        default=False,
        description="Require expires_at on signed manifests when True.",
    )
    refresh_interval: float | None = Field(
        default=300.0,
        description="Optional interval (seconds) to re-sync remote manifests; disabled when null.",
    )
    max_retries: int = Field(
        default=3, description="Number of retry attempts for remote fetches."
    )
    retry_base_delay: float = Field(
        default=1.0, description="Base delay (seconds) for retry backoff."
    )
    retry_max_delay: float = Field(
        default=30.0, description="Maximum delay (seconds) between retries."
    )
    retry_jitter: float = Field(
        default=0.25, description="Jitter factor added to retry sleep."
    )
    circuit_breaker_threshold: int = Field(
        default=5,
        description="Consecutive failures before opening the remote circuit breaker.",
    )
    circuit_breaker_reset: float = Field(
        default=60.0,
        description="Seconds before allowing attempts after circuit breaker opens.",
    )
    allow_file_uris: bool = Field(
        default=False,
        description="Allow file:// URIs and local manifest paths when True.",
    )
    allowed_file_uri_roots: list[str] = Field(
        default_factory=list,
        description="Optional allowlist of roots for file:// URIs and local paths.",
    )
    latency_budget_ms: float = Field(
        default=5000.0,
        description="Warn threshold (milliseconds) for remote sync latency.",
    )


class LifecycleConfig(BaseModel):
    activation_timeout: float = Field(
        default=30.0,
        description="Seconds before lifecycle activation times out.",
    )
    health_timeout: float = Field(
        default=5.0,
        description="Seconds before lifecycle health checks time out.",
    )
    cleanup_timeout: float = Field(
        default=10.0,
        description="Seconds before cleanup hooks time out.",
    )
    hook_timeout: float = Field(
        default=5.0,
        description="Seconds before pre/post-swap hooks time out.",
    )
    shield_tasks: bool = Field(
        default=True,
        description="Shield lifecycle awaitables from cancellation.",
    )


class PluginsConfig(BaseModel):
    auto_load: bool = Field(
        default=False,
        description="Load built-in Oneiric entry-point groups (oneiric.*).",
    )
    entry_points: list[str] = Field(
        default_factory=list,
        description="Additional entry-point groups to load during bootstrap.",
    )


class RuntimeProfileConfig(BaseModel):
    name: str = "default"
    watchers_enabled: bool = True
    remote_enabled: bool = True
    inline_manifest_only: bool = False
    supervisor_enabled: bool = True


class RuntimePathsConfig(BaseModel):
    workflow_checkpoints_enabled: bool = True
    workflow_checkpoints_path: str | None = Field(
        default=None,
        description="Override path for workflow checkpoint SQLite store.",
    )


class RuntimeSupervisorConfig(BaseModel):
    enabled: bool = True
    poll_interval: float = Field(
        default=2.0,
        ge=0.1,
        description="Seconds between supervisor poll iterations.",
    )


class OneiricMCPConfig(BaseModel):
    http_port: int = 8000
    http_host: str = "127.0.0.1"
    enable_http_transport: bool = True
    debug: bool = False
    environment: str = "development"
    cache_dir: str = ".oneiric_cache"

    model_config = ConfigDict(
        env_prefix="ONEIRIC_MCP_",
        env_file=".env",
        extra="allow",
    )


class OneiricSettings(BaseModel):
    app: AppConfig = Field(default_factory=AppConfig)
    adapters: LayerSettings = Field(default_factory=LayerSettings)
    services: LayerSettings = Field(default_factory=LayerSettings)
    tasks: LayerSettings = Field(default_factory=LayerSettings)
    events: LayerSettings = Field(default_factory=LayerSettings)
    workflows: LayerSettings = Field(default_factory=LayerSettings)
    actions: LayerSettings = Field(default_factory=LayerSettings)
    secrets: SecretsConfig = Field(default_factory=SecretsConfig)
    remote: RemoteSourceConfig = Field(default_factory=RemoteSourceConfig)
    logging: LoggingConfig = Field(default_factory=LoggingConfig)
    lifecycle: LifecycleConfig = Field(default_factory=LifecycleConfig)
    plugins: PluginsConfig = Field(default_factory=PluginsConfig)
    profile: RuntimeProfileConfig = Field(default_factory=RuntimeProfileConfig)
    runtime_paths: RuntimePathsConfig = Field(default_factory=RuntimePathsConfig)
    runtime_supervisor: RuntimeSupervisorConfig = Field(
        default_factory=RuntimeSupervisorConfig
    )


def load_settings(
    path: str | Path | None = None,
    project_name: str = "oneiric",
) -> OneiricSettings:
    """Load Oneiric settings with XDG-compliant layered configuration.

    Configuration priority (highest to lowest):
    1. Explicit path argument
    2. {PROJECT_NAME}_CONFIG environment variable
    3. Environment variable overrides ({PROJECT_NAME}_{SETTING}__)
    4. XDG user config: ~/.config/{project_name}/config.yaml
    5. Project local override: settings/local.yaml (development)
    6. Code defaults

    Note: XDG config overrides project local config because user preferences
    should take precedence over repository defaults.

    Args:
        path: Optional explicit config file path (highest priority)
        project_name: Project identifier for XDG/environment variable lookup
            (e.g., "oneiric", "session_buddy", "crackerjack")

    Returns:
        Validated OneiricSettings instance with all layers applied

    Examples:
        >>> # Load with defaults
        >>> settings = load_settings()

        >>> # Load for specific project (uses XDG config)
        >>> settings = load_settings(project_name="session_buddy")

        >>> # Load with explicit config override
        >>> settings = load_settings(path="/path/to/config.yaml")
    """
    data: dict[str, Any] = {}

    # Track if we have an explicit config (to apply as final override)
    explicit_config_data: dict[str, Any] | None = None
    config_path = path or os.getenv(f"{project_name.upper()}_CONFIG")
    if config_path:
        file = Path(config_path)
        if file.exists():
            explicit_config_data = _read_file(file)
            logger.info(
                "explicit-config-loaded",
                path=str(file),
                layer="explicit",
            )
        else:
            logger.warning(
                "config-file-missing",
                path=str(file),
                layer="explicit",
            )

    # Layer 1: Project local override (settings/local.yaml)
    # Lowest priority (development-specific, user config should override)
    local_yaml = Path("settings") / "local.yaml"
    if local_yaml.exists():
        try:
            local_data = _read_file(local_yaml)
            data = _deep_merge(data, local_data)
            logger.info(
                "local-config-loaded",
                path=str(local_yaml),
                layer="project-local",
            )
        except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e:
            logger.warning(
                "local-config-parse-error",
                path=str(local_yaml),
                error=str(e),
                layer="project-local",
            )

    # Layer 2: XDG user config (~/.config/{project_name}/config.yaml)
    # Higher priority than project local (user config > project defaults)
    xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config")
    xdg_config_path = Path(xdg_config_home).expanduser() / project_name / "config.yaml"
    if xdg_config_path.exists():
        try:
            xdg_data = _read_file(xdg_config_path)
            data = _deep_merge(data, xdg_data)
            logger.info(
                "xdg-config-loaded",
                path=str(xdg_config_path),
                project=project_name,
                layer="xdg",
            )
        except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e:
            logger.warning(
                "xdg-config-parse-error",
                path=str(xdg_config_path),
                error=str(e),
                layer="xdg",
            )

    # Layer 3: Environment variable overrides (highest priority)
    env_data = _env_overrides(project_name)
    if env_data:
        data = _deep_merge(data, env_data)
        logger.info(
            "env-overrides-applied",
            count=len(env_data),
            project=project_name,
            layer="environment",
        )

    # Layer 4: Explicit config (absolute highest priority)
    # Applied LAST to override all other layers
    if explicit_config_data is not None:
        data = _deep_merge(data, explicit_config_data)
        logger.info(
            "explicit-config-applied",
            layer="explicit-final",
        )
        try:
            xdg_data = _read_file(xdg_config_path)
            data = _deep_merge(data, xdg_data)
            logger.info(
                "xdg-config-loaded",
                path=str(xdg_config_path),
                project=project_name,
                layer="xdg",
            )
        except (json.JSONDecodeError, tomllib.TOMLDecodeError, yaml.YAMLError) as e:
            logger.warning(
                "xdg-config-parse-error",
                path=str(xdg_config_path),
                error=str(e),
                layer="xdg",
            )

    # Layer 4: Environment variable overrides (highest priority)
    env_data = _env_overrides(project_name)
    if env_data:
        data = _deep_merge(data, env_data)
        logger.info(
            "env-overrides-applied",
            count=len(env_data),
            project=project_name,
            layer="environment",
        )

    return OneiricSettings.model_validate(data)


def resolver_settings_from_config(settings: OneiricSettings) -> ResolverSettings:
    selections = {}
    domain_map = {
        "adapter": settings.adapters.selections,
        "service": settings.services.selections,
        "task": settings.tasks.selections,
        "event": settings.events.selections,
        "workflow": settings.workflows.selections,
        "action": settings.actions.selections,
    }
    for domain, mapping in domain_map.items():
        if mapping:
            selections[domain] = mapping
    return ResolverSettings(selections=selections)


def lifecycle_snapshot_path(settings: OneiricSettings) -> Path:
    cache = Path(settings.remote.cache_dir)
    return cache / "lifecycle_status.json"


def runtime_health_path(settings: OneiricSettings) -> Path:
    return default_runtime_health_path(settings.remote.cache_dir)


def domain_activity_path(settings: OneiricSettings) -> Path:
    cache = Path(settings.remote.cache_dir)
    return cache / "domain_activity.sqlite"


def runtime_observability_path(settings: OneiricSettings) -> Path:
    cache = Path(settings.remote.cache_dir)
    return cache / "runtime_telemetry.json"


def workflow_checkpoint_path(settings: OneiricSettings) -> Path | None:
    if not settings.runtime_paths.workflow_checkpoints_enabled:
        return None
    override = settings.runtime_paths.workflow_checkpoints_path
    if override:
        return Path(override)
    cache = Path(settings.remote.cache_dir)
    return cache / "workflow_checkpoints.sqlite"


def apply_runtime_profile(
    settings: OneiricSettings, profile_name: str | None
) -> OneiricSettings:
    target = (profile_name or settings.profile.name or "default").lower()
    updated = settings.model_copy(deep=True)
    if target in ("", "default"):
        updated.profile = RuntimeProfileConfig()
        if profile_name:
            logger.info("runtime-profile-applied", profile="default")
        return updated
    if target == "serverless":
        updated.profile = RuntimeProfileConfig(
            name="serverless",
            watchers_enabled=False,
            remote_enabled=False,
            inline_manifest_only=True,
            supervisor_enabled=True,
        )
        updated.remote.enabled = False
        updated.remote.refresh_interval = None
        updated.runtime_supervisor.enabled = True
        logger.info("runtime-profile-applied", profile="serverless")
        return updated
    raise ValueError(f"Unknown runtime profile: {profile_name}")


def apply_profile_with_fallback(
    settings: OneiricSettings, profile_name: str | None
) -> OneiricSettings:
    explicit = profile_name or ""
    if explicit:
        return apply_runtime_profile(settings, explicit)

    configured = settings.profile.name or "default"
    if configured.lower() != "default":
        return apply_runtime_profile(settings, configured)
    return settings


class SecretsHook:
    def __init__(self, lifecycle: LifecycleManager, config: SecretsConfig) -> None:
        self.lifecycle = lifecycle
        self.config = config
        self._logger = get_logger("secrets")
        self._cache = SecretValueCache(config.cache_ttl_seconds)
        self._default_cache_key = config.provider or f"{config.domain}:{config.key}"
        self._prefetched = False

    async def get(self, secret_id: str) -> str | None:
        if secret_id in self.config.inline:
            return self.config.inline[secret_id]
        cache_key = self._cache_key()
        hit, cached_value = self._cache.get(cache_key, secret_id)
        if hit:
            self._logger.debug(
                "secret-cache-hit", secret_id=secret_id, provider=cache_key
            )
            return cached_value
        provider = await self._ensure_provider()
        if provider is None:
            return None
        getter = getattr(provider, "get_secret", None)
        if not callable(getter):
            raise LifecycleError(
                "Configured secrets adapter does not implement 'get_secret'"
            )
        value = getter(secret_id)
        resolved = await _maybe_await(value)
        self._cache.set(cache_key, secret_id, resolved)
        return resolved

    async def prefetch(self) -> bool:
        provider = await self._ensure_provider()
        ready = provider is not None
        if ready:
            self._logger.debug(
                "secrets-prefetched",
                provider=self.config.provider or self._default_cache_key,
            )
        return ready

    async def rotate(
        self,
        keys: Sequence[str] | None = None,
        *,
        provider: str | None = None,
        include_provider_cache: bool = True,
    ) -> int:
        removed = self.invalidate(keys=keys, provider=provider)
        if include_provider_cache:
            await self._invalidate_provider_cache()
        return removed

    @property
    def prefetched(self) -> bool:
        return self._prefetched

    async def _ensure_provider(self) -> SecretsProviderProtocol | None:
        instance = self.lifecycle.get_instance(self.config.domain, self.config.key)
        if instance:
            self._prefetched = True
            return instance
        provider_id = self.config.provider
        if not provider_id:
            self._logger.debug("no-secrets-provider-configured")
            return None
        instance = await self.lifecycle.activate(
            self.config.domain,
            self.config.key,
            provider=provider_id,
        )
        if instance is not None:
            self._prefetched = True
        return instance

    def invalidate(
        self,
        keys: Sequence[str] | None = None,
        provider: str | None = None,
    ) -> int:
        provider_key = self._cache_key(provider) if provider else None
        removed = self._cache.invalidate(keys, provider_key)
        self._logger.info(
            "secrets-cache-invalidated",
            removed=removed,
            provider=provider_key or "*",
            keys="*" if keys is None else list(keys),
        )
        return removed

    async def _invalidate_provider_cache(self) -> None:
        provider = await self._ensure_provider()
        if provider is None:
            return
        cache_provider = cast(SecretsCacheProtocol, provider)
        for method_name in ("invalidate_cache", "clear_cache", "refresh"):
            handler = getattr(cache_provider, method_name, None)
            if callable(handler):
                await _maybe_await(handler())
                self._logger.info(
                    "secrets-provider-cache-invalidated",
                    method=method_name,
                )
                return

    def _cache_key(self, override: str | None = None) -> str:
        return (override or self._default_cache_key or "default").lower()


def _env_overrides(project_name: str = "oneiric") -> dict[str, Any]:  # noqa: C901
    """Generate environment variable overrides for a project.

    Environment variables should be formatted as:
    {PROJECT_NAME}_{SECTION}__{FIELD}

    Examples:
        ONEIRIC_REMOTE__CACHE_DIR=/tmp/cache
        SESSION_BUDDY_LOGGING__LEVEL=DEBUG
        CRACKERJACK_REMOTE__ENABLED=false

    Args:
        project_name: Project name used for environment variable prefix
            (e.g., "oneiric", "session_buddy", "crackerjack")

    Returns:
        Dict of configuration overrides from environment variables
    """
    # Convert project name to env prefix (e.g., "session_buddy" -> "SESSION_BUDDY_")
    prefix = f"{project_name.upper()}_"

    overrides: dict[str, Any] = {}
    for key, value in os.environ.items():
        if not key.startswith(prefix):
            continue
        # Remove prefix and split on __ to get nested keys
        # Example: "REMOTE__CACHE_DIR" -> ["remote", "cache_dir"]
        path = key[len(prefix) :].lower().split("__")
        coerced = _coerce_env_value(value)

        # Special handling for profile (single-level override)
        if len(path) == 1 and path[0] == "profile":
            profile_section = overrides.setdefault("profile", {})
            if isinstance(profile_section, dict):
                profile_section["name"] = coerced
            else:
                overrides["profile"] = {"name": coerced}
            continue

        # Build nested dict structure
        cursor = overrides
        for part in path[:-1]:
            cursor = cursor.setdefault(part, {})
        cursor[path[-1]] = coerced

    return overrides


def _coerce_env_value(value: str) -> Any:
    lowered = value.lower()
    if lowered in {"true", "false"}:
        return lowered == "true"
    for caster in (int, float):
        try:
            return caster(value)
        except ValueError:
            continue
    if "," in value:
        return [item.strip() for item in value.split(",")]
    return value


def _read_file(path: Path) -> dict[str, Any]:
    """Read configuration file and return parsed dictionary.

    Supports multiple formats:
    - YAML (.yaml, .yml)
    - TOML (.toml, .tml)
    - JSON (.json or starts with '{')

    Args:
        path: Path to configuration file

    Returns:
        Parsed configuration dictionary

    Raises:
        tomllib.TOMLDecodeError: If TOML parsing fails
        json.JSONDecodeError: If JSON parsing fails
        yaml.YAMLError: If YAML parsing fails
    """
    content = path.read_text()

    # Try YAML first (most common for config files)
    if path.suffix in {".yaml", ".yml"}:
        try:
            return yaml.safe_load(content) or {}
        except yaml.YAMLError as e:
            logger.error("yaml-parse-error", path=str(path), error=str(e))
            raise

    # Try TOML
    if path.suffix in {".toml", ".tml"}:
        return tomllib.loads(content)

    # Try JSON
    if path.suffix == ".json":
        return json.loads(content)

    # Detect JSON by content
    if content.strip().startswith("{"):
        return json.loads(content)

    # Default to TOML for backwards compatibility
    return tomllib.loads(content)


def _deep_merge(base: Mapping[str, Any], override: Mapping[str, Any]) -> dict[str, Any]:
    result: dict[str, Any] = dict(base)
    for key, value in override.items():
        if (
            key in result
            and isinstance(result[key], Mapping)
            and isinstance(value, Mapping)
        ):
            result[key] = _deep_merge(result[key], value)
        else:
            result[key] = value
    return result


async def _maybe_await(value: Any) -> Any:
    if inspect.isawaitable(value):
        return await value
    return value
