"""
Configuration Management for Scientific Gallery Generator

This module provides dataclasses for managing configuration, including
defaults for gallery generation settings.
"""

import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast

import yaml
from platformdirs import user_config_dir


def default_config_path() -> Path:
    """Return the path to the package-bundled read-only template config."""
    return Path(__file__).parent / "config.yaml"


def user_config_path() -> Path:
    """Return the user-level config path (~/.config/gallery/config.yaml).

    Follows the XDG Base Directory spec via platformdirs:
      Linux/macOS → ~/.config/gallery/config.yaml
      Windows     → %APPDATA%/gallery/config.yaml
    """
    return Path(user_config_dir("gallery", appauthor=False)) / "config.yaml"


def get_active_config_path() -> Path:
    """Return the config path to use, with this precedence:

    1. User config  (~/.config/gallery/config.yaml)  — if it exists
    2. Package-bundled template                       — fallback
    """
    ucp = user_config_path()
    return ucp if ucp.exists() else default_config_path()


def ensure_user_config() -> Path:
    """Ensure ~/.config/gallery/config.yaml exists, creating it from the
    package template if needed.  Returns the path.
    """
    ucp = user_config_path()
    if not ucp.exists():
        ucp.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy(default_config_path(), ucp)
    return ucp


def _load_raw(path: Path) -> Dict[str, Any]:
    with open(path, "r") as f:
        return yaml.safe_load(f) or {}


def _save_raw(path: Path, data: Dict[str, Any]) -> None:
    with open(path, "w") as f:
        yaml.dump(data, f, default_flow_style=False, allow_unicode=True)


def _get_nested(data: Dict, keys: List[str]) -> Any:
    for k in keys:
        if not isinstance(data, dict) or k not in data:
            raise KeyError(f"Key '{'.'.join(keys)}' not found in config")
        data = data[k]
    return data


def _set_nested(data: Dict, keys: List[str], value: Any) -> None:
    for k in keys[:-1]:
        data = data.setdefault(k, {})
    data[keys[-1]] = value


class ConfigManager:
    """Read/write access to a gallery config YAML file.

    Intended to be reused by both the CLI and a future TUI layer.
    """

    def __init__(self, config_path: Optional[Path] = None):
        self.path = Path(config_path) if config_path else default_config_path()

    # ------------------------------------------------------------------
    # Core operations
    # ------------------------------------------------------------------

    def get(self, key: str) -> Any:
        """Return the value at dot-separated *key* (e.g. 'gallery.png_dpi')."""
        data = _load_raw(self.path)
        return _get_nested(data, key.split("."))

    def set(self, key: str, value: str) -> None:
        """Set *key* to *value*, coercing type via YAML parsing."""
        data = _load_raw(self.path)
        parsed = yaml.safe_load(value)
        _set_nested(data, key.split("."), parsed)
        _save_raw(self.path, data)

    def list_all(self) -> Dict[str, Any]:
        """Return the full config dict."""
        return _load_raw(self.path)

    # ------------------------------------------------------------------
    # Source helpers
    # ------------------------------------------------------------------

    def add_source(self, name: str, path: Union[str, Path]) -> None:
        """Append a new source entry; raises ValueError if name already exists."""
        data = _load_raw(self.path)
        sources: List[Dict] = data.setdefault("sources", [])
        if any(s.get("name") == name for s in sources):
            raise ValueError(f"Source '{name}' already exists")
        sources.append({"name": name, "path": str(path)})
        _save_raw(self.path, data)

    def remove_source(self, name: str) -> None:
        """Remove the source with the given name; raises KeyError if not found."""
        data = _load_raw(self.path)
        sources: List[Dict] = data.get("sources", [])
        filtered = [s for s in sources if s.get("name") != name]
        if len(filtered) == len(sources):
            raise KeyError(f"Source '{name}' not found")
        data["sources"] = filtered
        _save_raw(self.path, data)

    def list_sources(self) -> List[Dict]:
        """Return the list of source dicts."""
        return _load_raw(self.path).get("sources", [])


@dataclass
class GalleryDefaults:
    """Default values for gallery generation."""

    png_dpi: int = 400
    plot_root: str = "gallery"
    cache_enabled: bool = True
    inherit_from_parent: bool = True


@dataclass
class GallerySource:
    """Represents a single data source for the gallery."""

    name: str
    path: Union[str, Path]

    def __post_init__(self):
        if isinstance(self.path, str):
            self.path = Path(self.path)


@dataclass
class GalleryConfig:
    """
    Main configuration for gallery generation.

    Can be created programmatically or loaded from YAML.
    """

    web_folder: Union[str, Path]
    sources: List[Union[GallerySource, Dict[str, Any]]] = field(default_factory=list)
    png_dpi: int = GalleryDefaults.png_dpi
    plot_root: str = GalleryDefaults.plot_root
    cache_enabled: bool = GalleryDefaults.cache_enabled
    inherit_from_parent: bool = GalleryDefaults.inherit_from_parent

    def __post_init__(self):
        if isinstance(self.web_folder, str):
            self.web_folder = Path(self.web_folder)

        normalized_sources: List[Union[GallerySource, Dict[str, Any]]] = []
        for source in self.sources:
            if isinstance(source, dict):
                source_dict = cast(Dict[str, Any], source)
                source = GallerySource(name=source_dict["name"], path=source_dict["path"])
            elif not isinstance(source, GallerySource):
                raise TypeError(f"Source must be dict or GallerySource, got {type(source)}")
            normalized_sources.append(source)
        self.sources = normalized_sources

    @classmethod
    def from_yaml(cls, yaml_file: Union[str, Path]) -> "GalleryConfig":
        """Load configuration from a YAML file."""
        yaml_path = Path(yaml_file)
        if not yaml_path.exists():
            raise FileNotFoundError(f"Config file not found: {yaml_file}")

        with open(yaml_path, "r") as f:
            data = yaml.safe_load(f)

        if data is None:
            data = {}

        web_folder = data.get("paths", {}).get("web_folder")
        if not web_folder:
            raise ValueError("web_folder must be specified in config under paths")

        gallery_cfg = data.get("gallery", {})
        sources_data = data.get("sources", [])
        sources: List[Union[GallerySource, Dict[str, Any]]] = [
            {"name": s["name"], "path": s["path"]} for s in sources_data
        ]

        metadata_cfg = data.get("metadata", {})

        return cls(
            web_folder=web_folder,
            sources=sources,
            png_dpi=gallery_cfg.get("png_dpi", GalleryDefaults.png_dpi),
            plot_root=gallery_cfg.get("plot_root", GalleryDefaults.plot_root),
            cache_enabled=metadata_cfg.get("cache_enabled", GalleryDefaults.cache_enabled),
            inherit_from_parent=metadata_cfg.get("inherit_from_parent", GalleryDefaults.inherit_from_parent),
        )

    def to_yaml(self, yaml_file: Union[str, Path]) -> None:
        """Save the current configuration to a YAML file."""
        yaml_path = Path(yaml_file)

        data = {
            "paths": {
                "web_folder": str(self.web_folder),
            },
            "gallery": {
                "plot_root": self.plot_root,
                "png_dpi": self.png_dpi,
            },
            "ui": {
                "max_recent_plots": 20,
                "search_debounce_ms": 300,
            },
            "metadata": {
                "cache_enabled": self.cache_enabled,
                "inherit_from_parent": self.inherit_from_parent,
                "supported_formats": [".yaml", ".yml", ".json"],
            },
            # self.sources is always List[GallerySource] after __post_init__ normalizes it.
            "sources": [{"name": s.name, "path": str(s.path)} for s in cast(List[GallerySource], self.sources)],
        }

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