"""
Module: config
Description: Configuration management with hierarchy support

Implements:
    - docs/guides/developer-environment.md#configuration
    - docs/cli/reference.md#huitzo-config

See Also:
    - docs/cli/reference.md (for configuration schema)
"""

from __future__ import annotations

import json
import os
import time
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, ConfigDict


class AuthConfig(BaseModel):
    """Authentication configuration.

    Note: Auth tokens are stored exclusively in TokenStore (~/.huitzo/token),
    NOT in config.yaml. Use ``huitzo login`` or ``HUITZO_AUTH_TOKEN`` env var.
    """

    model_config = ConfigDict(extra="allow")

    token_file: str | None = None


class DevConfig(BaseModel):
    """Development configuration."""

    model_config = ConfigDict(extra="allow")

    port: int = 8080
    verbose: bool = False
    auto_reload: bool = True
    persist_sandbox: bool = False


class CLIConfig(BaseModel):
    """Main CLI configuration."""

    model_config = ConfigDict(extra="allow")

    api_url: str = "https://api.huitzo.ai"
    auth: AuthConfig = AuthConfig()
    dev: DevConfig = DevConfig()


class ConfigManager:
    """Manages CLI configuration with hierarchy support.

    Configuration hierarchy (highest to lowest priority):
    1. Environment variables (HUITZO_*)
    2. Command-line options passed to ConfigManager
    3. User config (~/.config/huitzo/config.yaml)
    4. Project config (./huitzo.yaml)
    5. Defaults (built-in)
    """

    def __init__(self, project_root: Path | None = None) -> None:
        """Initialize configuration manager.

        Args:
            project_root: Root directory for project config. If None, uses current directory.
        """
        self.project_root = project_root or Path.cwd()
        self._config = self._load_config()

    @staticmethod
    def get_config_dir() -> Path:
        """Get platform-specific config directory."""
        if os.name == "nt":  # Windows
            base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
            return base / "huitzo"
        else:  # Unix-like
            xdg_config = os.environ.get("XDG_CONFIG_HOME")
            if xdg_config:
                return Path(xdg_config) / "huitzo"
            return Path.home() / ".config" / "huitzo"

    @staticmethod
    def _get_token_file() -> Path:
        """Get platform-specific token file path."""
        if os.name == "nt":  # Windows
            base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
            return base / "huitzo" / "token"
        else:  # Unix-like
            return Path.home() / ".huitzo" / "token"

    def _load_from_env(self) -> dict[str, Any]:
        """Load configuration from environment variables."""
        config: dict[str, Any] = {}

        if api_url := os.environ.get("HUITZO_API_URL"):
            config["api_url"] = api_url

        if dev_port := os.environ.get("HUITZO_DEV_PORT"):
            config.setdefault("dev", {})
            config["dev"]["port"] = int(dev_port)

        return config

    def _load_from_yaml(self, path: Path) -> dict[str, Any]:
        """Load configuration from YAML file."""
        if not path.exists():
            return {}

        try:
            with open(path) as f:
                data = yaml.safe_load(f)
                return data or {}
        except (OSError, yaml.YAMLError):
            return {}

    @staticmethod
    def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
        """Recursively merge *override* into *base*, returning a new dict.

        Nested dicts are merged rather than replaced, so setting a single
        key inside a nested section doesn't erase sibling keys.
        """
        merged = dict(base)
        for key, value in override.items():
            if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
                merged[key] = ConfigManager._deep_merge(merged[key], value)
            else:
                merged[key] = value
        return merged

    def _load_config(self) -> CLIConfig:
        """Load and merge configuration from all sources."""
        config_dict: dict[str, Any] = {}

        # 1. Project config (lowest priority)
        project_config_path = self.project_root / "huitzo.yaml"
        project_config = self._load_from_yaml(project_config_path)
        config_dict = self._deep_merge(config_dict, project_config)

        # 2. User config
        user_config_path = self.get_config_dir() / "config.yaml"
        user_config = self._load_from_yaml(user_config_path)
        config_dict = self._deep_merge(config_dict, user_config)

        # 3. Environment variables (highest priority)
        env_config = self._load_from_env()
        config_dict = self._deep_merge(config_dict, env_config)

        # Parse into CLIConfig model
        return CLIConfig(**config_dict)

    def get(self, key: str, default: Any = None) -> Any:
        """Get configuration value by dot-notation key.

        Examples:
            - "api_url"
            - "auth.token"
            - "dev.port"
        """
        parts = key.split(".")
        value: Any = self._config

        for part in parts:
            if isinstance(value, dict):
                value = value.get(part)
            elif hasattr(value, part):
                value = getattr(value, part)
            else:
                return default

            if value is None:
                return default

        return value

    def set(self, key: str, value: Any, scope: str = "user") -> None:
        """Set configuration value and save to file.

        Args:
            key: Configuration key (e.g., "auth.token")
            value: Value to set
            scope: Where to save ("user" or "project")
        """
        if scope == "user":
            config_path = self.get_config_dir() / "config.yaml"
            config_path.parent.mkdir(parents=True, exist_ok=True)
            # Set restrictive permissions on parent directory
            config_path.parent.chmod(0o700)
        else:
            config_path = self.project_root / "huitzo.yaml"

        # Load existing config
        existing = self._load_from_yaml(config_path) if config_path.exists() else {}

        # Update nested value
        parts = key.split(".")
        current = existing
        for part in parts[:-1]:
            if part not in current:
                current[part] = {}
            current = current[part]
        current[parts[-1]] = value

        # Save to file with restrictive permissions atomically
        config_path.parent.mkdir(parents=True, exist_ok=True)
        if os.name != "nt":
            fd = os.open(str(config_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
            with os.fdopen(fd, "w") as f:
                yaml.dump(existing, f, default_flow_style=False)
        else:
            with open(config_path, "w") as f:
                yaml.dump(existing, f, default_flow_style=False)

        # Reload configuration
        self._config = self._load_config()

    def to_dict(self) -> dict[str, Any]:
        """Get configuration as dictionary.

        Sensitive fields (auth.token, auth.token_file) are redacted.
        """
        d = self._config.model_dump()
        auth = d.get("auth", {})
        if auth.get("token_file"):
            auth["token_file"] = "***"
        return d

    def get_config_path(self, scope: str = "user") -> Path:
        """Get the path to the config file.

        Args:
            scope: "user" or "project"
        """
        if scope == "user":
            return self.get_config_dir() / "config.yaml"
        else:
            return self.project_root / "huitzo.yaml"

    def check_namespace_quota(self) -> tuple[bool, str | None]:
        """Check if user has exceeded namespace creation quota.

        Limit: 5 namespaces per hour

        Returns:
            Tuple of (allowed: bool, error_message: str | None)
        """
        quota_file = self.get_config_dir() / "namespace_quota.json"
        now = time.time()

        # Load recent attempts
        attempts: list[float] = []
        if quota_file.exists():
            try:
                data = json.loads(quota_file.read_text())
                # Filter attempts within the last hour
                attempts = [ts for ts in data.get("attempts", []) if now - ts < 3600]
            except (OSError, json.JSONDecodeError):
                # Corrupted file - reset quota
                attempts = []

        # Check quota (5 per hour)
        if len(attempts) >= 5:
            return False, "Rate limit exceeded: You can create up to 5 namespaces per hour"

        # Record this attempt
        attempts.append(now)
        try:
            quota_file.parent.mkdir(parents=True, exist_ok=True)
            quota_file.write_text(json.dumps({"attempts": attempts}))
        except OSError:
            # Failed to write quota file - allow creation (graceful degradation)
            pass

        return True, None
