"""Cleave settings management.

Settings are stored in ~/.cleave/settings.yaml and control:
- cloven_report_path: Directory for .cloven.md reports (default: workspace)
- cloven_report_naming: How to name reports (default: "descriptor")
- backend: Active backend for TUI (default: "claude")
- backends: Per-backend configuration

Naming options:
- "descriptor": <descriptor>.cloven.md (extracted from directive)
- "timestamp": <ISO8601>.cloven.md
- "workspace": .cloven.md (legacy, in workspace)

Backend options:
- "claude": Claude Agent SDK (requires authentication)
- "ollama": Ollama local inference (http://localhost:11434)
- "openai": OpenAI API (requires OPENAI_API_KEY)
- "vllm": vLLM server (http://localhost:8000)
- "llamacpp": llama.cpp server (http://localhost:8080)
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from cleave.core.yaml_utils import parse_yaml_simple, to_yaml
from cleave.core.file_utils import atomic_write_text


class SettingsValidationError(Exception):
    """Raised when settings validation fails."""

    pass


# Default settings directory
SETTINGS_DIR = Path.home() / ".cleave"
SETTINGS_FILE = SETTINGS_DIR / "settings.yaml"

# Default backend configurations
DEFAULT_BACKEND_CONFIGS: dict[str, dict[str, Any]] = {
    "claude": {
        "default_model": "claude-sonnet-4-20250514",
    },
    "ollama": {
        "base_url": "http://localhost:11434/v1",
        "default_model": "qwen2.5-coder:7b",
        "models": [
            "qwen2.5-coder:7b",
            "qwen3:14b",
            "deepseek-coder-v2:16b",
            "llama3.1:8b",
        ],
    },
    "openai": {
        "base_url": "https://api.openai.com/v1",
        "api_key_env": "OPENAI_API_KEY",
        "default_model": "gpt-4o",
        "models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
    },
    "vllm": {
        "base_url": "http://localhost:8000/v1",
        "default_model": "Qwen/Qwen2.5-Coder-7B-Instruct",
    },
    "llamacpp": {
        "base_url": "http://localhost:8080/v1",
        "default_model": "default",
    },
}

# Default values
DEFAULT_SETTINGS = {
    "cloven_report_path": None,  # None means use workspace directory
    "cloven_report_naming": "descriptor",  # descriptor, timestamp, or workspace
    "backend": "claude",  # Active backend
    "backends": DEFAULT_BACKEND_CONFIGS,
}

# Valid enum values
VALID_REPORT_NAMING = ["descriptor", "timestamp", "workspace"]
VALID_BUILTIN_BACKENDS = ["claude", "ollama", "openai", "vllm", "llamacpp"]
VALID_BACKENDS = VALID_BUILTIN_BACKENDS


@dataclass
class BackendSettings:
    """Settings for a specific backend."""

    base_url: str | None = None
    default_model: str | None = None
    api_key_env: str | None = None
    models: list[str] = field(default_factory=list)
    _extra: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        result: dict[str, Any] = {}
        if self.base_url:
            result["base_url"] = self.base_url
        if self.default_model:
            result["default_model"] = self.default_model
        if self.api_key_env:
            result["api_key_env"] = self.api_key_env
        if self.models:
            result["models"] = self.models
        result.update(self._extra)
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "BackendSettings":
        """Create from dictionary."""
        known_keys = {"base_url", "default_model", "api_key_env", "models"}
        extra = {k: v for k, v in data.items() if k not in known_keys}
        return cls(
            base_url=data.get("base_url"),
            default_model=data.get("default_model"),
            api_key_env=data.get("api_key_env"),
            models=data.get("models", []),
            _extra=extra,
        )


@dataclass
class CleaveSettings:
    """Cleave configuration settings."""

    cloven_report_path: str | None = None
    cloven_report_naming: str = "descriptor"

    # Backend settings
    backend: str = "claude"  # Active backend
    backends: dict[str, BackendSettings] = field(default_factory=dict)

    # TUI settings
    theme: str = "retro-purple"  # Active theme

    # Future settings can be added here
    _extra: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Convert settings to dictionary for serialization."""
        result: dict[str, Any] = {
            "cloven_report_path": self.cloven_report_path,
            "cloven_report_naming": self.cloven_report_naming,
            "backend": self.backend,
            "theme": self.theme,
        }
        if self.backends:
            result["backends"] = {
                name: cfg.to_dict() for name, cfg in self.backends.items()
            }
        # Include any extra settings that were loaded
        result.update(self._extra)
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CleaveSettings":
        """Create settings from dictionary."""
        known_keys = {"cloven_report_path", "cloven_report_naming", "backend", "backends", "theme"}
        extra = {k: v for k, v in data.items() if k not in known_keys}

        # Parse backends
        backends_data = data.get("backends", {})
        backends = {}
        for name, cfg in backends_data.items():
            if isinstance(cfg, dict):
                backends[name] = BackendSettings.from_dict(cfg)

        return cls(
            cloven_report_path=data.get("cloven_report_path"),
            cloven_report_naming=data.get("cloven_report_naming", "descriptor"),
            backend=data.get("backend", "claude"),
            backends=backends,
            theme=data.get("theme", "retro-purple"),
            _extra=extra,
        )

    def get_backend_config(self, backend_name: str | None = None) -> BackendSettings:
        """Get configuration for a backend.

        Args:
            backend_name: Backend name (uses active backend if None).

        Returns:
            BackendSettings for the requested backend.
        """
        name = backend_name or self.backend
        if name in self.backends:
            return self.backends[name]
        # Return defaults
        if name in DEFAULT_BACKEND_CONFIGS:
            return BackendSettings.from_dict(DEFAULT_BACKEND_CONFIGS[name])
        return BackendSettings()

    def set_backend_config(self, backend_name: str, config: BackendSettings) -> None:
        """Set configuration for a backend.

        Args:
            backend_name: Backend name.
            config: Backend configuration.
        """
        self.backends[backend_name] = config


def _validate_settings(settings: CleaveSettings) -> None:
    """Validate settings values.

    Args:
        settings: Settings to validate.

    Raises:
        SettingsValidationError: If settings contain invalid values.
    """
    # Validate cloven_report_naming
    if settings.cloven_report_naming not in VALID_REPORT_NAMING:
        raise SettingsValidationError(
            f"Invalid cloven_report_naming: '{settings.cloven_report_naming}'. "
            f"Must be one of: {', '.join(VALID_REPORT_NAMING)}"
        )

    # Validate backend (allow built-in and plugin backends)
    from cleave.tui.backends.plugin import list_all_backends
    available_backends = VALID_BUILTIN_BACKENDS + list_all_backends()
    if settings.backend not in available_backends:
        raise SettingsValidationError(
            f"Invalid backend: '{settings.backend}'. "
            f"Built-in backends: {', '.join(VALID_BUILTIN_BACKENDS)}. "
            f"Run 'cleave config list-backends' to see available plugin backends."
        )

    # Validate cloven_report_path if set
    if settings.cloven_report_path is not None:
        path = Path(settings.cloven_report_path).expanduser()
        # Only validate parent exists to avoid creating directories prematurely
        if not path.parent.exists() and str(path.parent) != ".":
            raise SettingsValidationError(
                f"Parent directory of cloven_report_path does not exist: {path.parent}"
            )

    # Validate backend configs are well-formed
    if not isinstance(settings.backends, dict):
        raise SettingsValidationError(
            f"backends must be a dict, got {type(settings.backends).__name__}"
        )

    for backend_name, backend_config in settings.backends.items():
        if not isinstance(backend_config, BackendSettings):
            raise SettingsValidationError(
                f"Backend config for '{backend_name}' must be BackendSettings, "
                f"got {type(backend_config).__name__}"
            )
        # Validate base_url if set
        if backend_config.base_url is not None and not isinstance(backend_config.base_url, str):
            raise SettingsValidationError(
                f"base_url for backend '{backend_name}' must be a string, "
                f"got {type(backend_config.base_url).__name__}"
            )


def get_settings_dir() -> Path:
    """Get the settings directory, creating if needed."""
    SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
    return SETTINGS_DIR


def get_settings_file() -> Path:
    """Get the settings file path."""
    return SETTINGS_FILE


def load_settings() -> CleaveSettings:
    """Load settings from ~/.cleave/settings.yaml.

    Returns default settings if file doesn't exist.

    Raises:
        SettingsValidationError: If settings contain invalid values.
    """
    if not SETTINGS_FILE.exists():
        return CleaveSettings()

    try:
        content = SETTINGS_FILE.read_text()
        data = parse_yaml_simple(content)
        if not isinstance(data, dict):
            return CleaveSettings()
        settings = CleaveSettings.from_dict(data)
        _validate_settings(settings)
        return settings
    except SettingsValidationError:
        raise
    except Exception:
        # Return defaults on any parsing error
        return CleaveSettings()


def save_settings(settings: CleaveSettings) -> Path:
    """Save settings to ~/.cleave/settings.yaml.

    Args:
        settings: Settings to save.

    Returns:
        Path to the settings file.

    Raises:
        SettingsValidationError: If settings contain invalid values.
    """
    _validate_settings(settings)
    get_settings_dir()  # Ensure directory exists

    content = to_yaml(settings.to_dict())
    atomic_write_text(SETTINGS_FILE, content)
    return SETTINGS_FILE


def get_setting(key: str) -> Any:
    """Get a single setting value."""
    settings = load_settings()
    if hasattr(settings, key):
        return getattr(settings, key)
    return settings._extra.get(key)


def set_setting(key: str, value: Any) -> None:
    """Set a single setting value and save.

    Raises:
        SettingsValidationError: If value is invalid for the key.
    """
    settings = load_settings()

    # Validate specific keys
    if key == "cloven_report_naming":
        if not isinstance(value, str):
            raise SettingsValidationError(
                f"cloven_report_naming must be a string, got {type(value).__name__}"
            )
        if value not in VALID_REPORT_NAMING:
            raise SettingsValidationError(
                f"Invalid cloven_report_naming: '{value}'. "
                f"Must be one of: {', '.join(VALID_REPORT_NAMING)}"
            )
    elif key == "backend":
        if not isinstance(value, str):
            raise SettingsValidationError(
                f"backend must be a string, got {type(value).__name__}"
            )
        # Allow built-in and plugin backends
        from cleave.tui.backends.plugin import list_all_backends
        available_backends = VALID_BUILTIN_BACKENDS + list_all_backends()
        if value not in available_backends:
            raise SettingsValidationError(
                f"Invalid backend: '{value}'. "
                f"Built-in: {', '.join(VALID_BUILTIN_BACKENDS)}. "
                f"Use 'cleave config list-backends' to see plugins."
            )
    elif key == "cloven_report_path":
        if value is not None and not isinstance(value, str):
            raise SettingsValidationError(
                f"cloven_report_path must be a string or None, got {type(value).__name__}"
            )
        if value is not None:
            path = Path(value).expanduser()
            if not path.parent.exists():
                raise SettingsValidationError(
                    f"Parent directory of cloven_report_path does not exist: {path.parent}"
                )
    elif key == "backends":
        if not isinstance(value, dict):
            raise SettingsValidationError(
                f"backends must be a dict, got {type(value).__name__}"
            )

    if hasattr(settings, key) and key != "_extra":
        setattr(settings, key, value)
    else:
        settings._extra[key] = value
    save_settings(settings)


def generate_cloven_filename(
    directive: str,
    workspace_path: Path | str | None = None,
    settings: CleaveSettings | None = None,
) -> Path:
    """Generate the cloven report filename based on settings.

    Args:
        directive: The root directive text
        workspace_path: Path to the .cleave workspace (for workspace naming)
        settings: Settings to use (loads from file if not provided)

    Returns:
        Full path to the cloven report file
    """
    if settings is None:
        settings = load_settings()

    # Determine base directory
    if settings.cloven_report_path:
        base_dir = Path(settings.cloven_report_path).expanduser()
        base_dir.mkdir(parents=True, exist_ok=True)
    elif workspace_path:
        base_dir = Path(workspace_path)
    else:
        base_dir = Path.cwd()

    # Generate filename based on naming strategy
    naming = settings.cloven_report_naming

    if naming == "workspace":
        # Legacy: .cloven.md in workspace
        return base_dir / ".cloven.md"

    elif naming == "timestamp":
        # ISO8601 timestamp
        from datetime import datetime
        timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
        return base_dir / f"{timestamp}.cloven.md"

    else:  # "descriptor" (default)
        # Extract meaningful descriptor from directive
        descriptor = _extract_descriptor(directive)
        return base_dir / f"{descriptor}.cloven.md"


def _extract_descriptor(directive: str) -> str:
    """Extract a useful filename descriptor from a directive.

    Takes the first ~50 chars, cleans them for filesystem use.
    """
    # Take first line or first 80 chars
    first_line = directive.split("\n")[0][:80]

    # Remove common prefixes
    prefixes_to_strip = [
        "integrate", "add", "implement", "create", "build", "migrate",
        "update", "fix", "refactor", "the", "a", "an",
    ]
    words = first_line.lower().split()
    while words and words[0] in prefixes_to_strip:
        words.pop(0)

    cleaned = " ".join(words[:6])  # Take up to 6 meaningful words

    # Clean for filesystem: lowercase, replace spaces/special chars with hyphens
    cleaned = re.sub(r"[^a-z0-9\s-]", "", cleaned.lower())
    cleaned = re.sub(r"\s+", "-", cleaned.strip())
    cleaned = re.sub(r"-+", "-", cleaned)  # Collapse multiple hyphens
    cleaned = cleaned[:50].rstrip("-")  # Limit length

    if not cleaned:
        cleaned = "cleave-report"

    return cleaned


def get_active_backend() -> str:
    """Get the currently active backend name.

    Returns:
        Backend name (e.g., 'claude', 'ollama').
    """
    return load_settings().backend


def set_active_backend(backend: str) -> None:
    """Set the active backend.

    Args:
        backend: Backend name to activate.
    """
    settings = load_settings()
    settings.backend = backend
    save_settings(settings)


def get_backend_setting(backend: str, key: str) -> Any:
    """Get a setting for a specific backend.

    Args:
        backend: Backend name.
        key: Setting key (e.g., 'base_url', 'default_model').

    Returns:
        Setting value or None.
    """
    settings = load_settings()
    config = settings.get_backend_config(backend)
    if hasattr(config, key):
        return getattr(config, key)
    return config._extra.get(key)


def set_backend_setting(backend: str, key: str, value: Any) -> None:
    """Set a setting for a specific backend.

    Args:
        backend: Backend name.
        key: Setting key.
        value: Setting value.
    """
    settings = load_settings()
    config = settings.get_backend_config(backend)

    if hasattr(config, key) and key != "_extra":
        setattr(config, key, value)
    else:
        config._extra[key] = value

    settings.backends[backend] = config
    save_settings(settings)


def detect_available_backends() -> list[tuple[str, bool, str]]:
    """Detect which backends are available (built-in and plugins).

    Returns:
        List of (backend_name, is_available, status_message) tuples.
    """
    results: list[tuple[str, bool, str]] = []

    # Check built-in backends
    # Claude: check for SDK and auth
    try:
        from cleave.tui.backends.claude import ClaudeBackend
        is_auth, msg = ClaudeBackend.check_auth()
        results.append(("claude", is_auth, msg))
    except ImportError:
        results.append(("claude", False, "Claude SDK not installed"))

    # Ollama: check for local server
    try:
        import socket
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1)
        result = sock.connect_ex(("localhost", 11434))
        sock.close()
        if result == 0:
            results.append(("ollama", True, "Ollama running on localhost:11434"))
        else:
            results.append(("ollama", False, "Ollama not running"))
    except Exception:
        results.append(("ollama", False, "Could not check Ollama"))

    # OpenAI: check for API key
    import os
    if os.environ.get("OPENAI_API_KEY"):
        results.append(("openai", True, "OPENAI_API_KEY set"))
    else:
        results.append(("openai", False, "OPENAI_API_KEY not set"))

    # Check plugin backends
    try:
        from cleave.tui.backends.plugin import get_global_registry
        registry = get_global_registry()
        for plugin_name in registry.list_plugins():
            status = registry.get_backend_status(plugin_name)
            results.append((
                plugin_name,
                status["is_authenticated"],
                f"Plugin: {status['auth_message']}"
            ))
    except Exception:
        pass  # Plugins are optional

    return results
