"""Configuration manager with YAML persistence and file locking."""

from __future__ import annotations

import contextlib
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from pydantic import ValidationError
from ruamel.yaml import YAML

from ilum.config.models import IlumConfig, ProfileConfig
from ilum.config.paths import IlumPaths
from ilum.errors import ConfigError


class ConfigManager:
    def __init__(self, paths: IlumPaths) -> None:
        self._paths = paths
        self._yaml = YAML()
        self._yaml.default_flow_style = False

    @property
    def config_file(self) -> Path:
        return self._paths.config_dir / "config.yaml"

    def load(self) -> IlumConfig:
        if not self.config_file.exists():
            return IlumConfig()

        try:
            with self.config_file.open("r") as f:
                data = self._yaml.load(f)
        except Exception as exc:
            raise ConfigError(
                f"Failed to read config file: {self.config_file}",
                suggestion="Check file permissions or delete the file to reset.",
                error_code="ILUM-030",
            ) from exc

        if data is None:
            return IlumConfig()

        try:
            return IlumConfig.model_validate(data)
        except ValidationError as exc:
            raise ConfigError(
                f"Invalid config file: {exc}",
                suggestion="Fix the config file or delete it to reset.",
                error_code="ILUM-030",
            ) from exc

    def save(self, config: IlumConfig) -> None:
        self._paths.ensure_dirs()
        config.updated_at = datetime.now(UTC).isoformat()
        data = config.model_dump(mode="json")

        import os
        import tempfile

        # Atomic write: write to temp file, then rename
        parent = self.config_file.parent
        parent.mkdir(parents=True, exist_ok=True)
        fd, tmp_path = tempfile.mkstemp(dir=str(parent), suffix=".tmp")
        try:
            with os.fdopen(fd, "w") as f:
                self._yaml.dump(data, f)
            os.replace(tmp_path, str(self.config_file))
        except BaseException:
            with contextlib.suppress(OSError):
                os.unlink(tmp_path)
            raise

    def ensure_config(self) -> IlumConfig:
        if self.config_file.exists():
            return self.load()

        now = datetime.now(UTC).isoformat()
        config = IlumConfig(
            active_profile="default",
            profiles={
                "default": ProfileConfig(name="default"),
            },
            created_at=now,
            updated_at=now,
        )
        self.save(config)
        return config

    def get(self, key: str) -> Any:
        config = self.load()
        return self._resolve_dotted(config.model_dump(), key)

    def set(self, key: str, value: Any) -> None:
        config = self.load()
        data = config.model_dump()
        self._set_dotted(data, key, value)

        try:
            config = IlumConfig.model_validate(data)
        except ValidationError as exc:
            raise ConfigError(
                f"Invalid value for '{key}': {exc}",
                suggestion="Check the value type and try again.",
                error_code="ILUM-030",
            ) from exc

        self.save(config)

    @staticmethod
    def _resolve_dotted(data: dict[str, Any], key: str) -> Any:
        parts = key.split(".")
        current: Any = data
        for part in parts:
            if isinstance(current, dict) and part in current:
                current = current[part]
            else:
                raise ConfigError(
                    f"Config key not found: {key}",
                    suggestion="Use 'ilum config list' to see available keys.",
                    error_code="ILUM-031",
                )
        return current

    @staticmethod
    def _set_dotted(data: dict[str, Any], key: str, value: Any) -> None:
        parts = key.split(".")
        current: Any = data
        for part in parts[:-1]:
            if isinstance(current, dict) and part in current:
                current = current[part]
            else:
                raise ConfigError(
                    f"Config key not found: {key}",
                    suggestion="Use 'ilum config list' to see available keys.",
                    error_code="ILUM-031",
                )
        if not isinstance(current, dict) or parts[-1] not in current:
            raise ConfigError(
                f"Config key not found: {key}",
                suggestion="Use 'ilum config list' to see available keys.",
                error_code="ILUM-031",
            )
        current[parts[-1]] = value
