"""GRI FOSS ecosystem shared configuration.

Provides module-level configuration that reads settings from
~/.config/gri/config.toml. Each package owns its own defaults - this module
provides no hardcoded defaults.

Features:
    - mtime-based cache invalidation (auto-reload on file change)
    - Dot-separated key access (e.g., "gri-terrain.copernicus.cache_dir")
    - Path expansion (~ -> home directory)
    - In-memory overrides via set()

Usage:
    from gri_utils import config

    # Get value with module-provided default
    cache_mb = config.get("gri-terrain.max_memory_cache_mb", 500.0)

    # Get path value
    cache_dir = config.get_path("gri-terrain.copernicus.cache_dir")

    # Override config path for testing
    config.set_config_path("/path/to/test/config.toml")

    # Set in-memory override (does not write to file)
    config.set("gri-terrain.max_memory_cache_mb", 1000.0)

"""

from __future__ import annotations

import tomllib
from pathlib import Path
from typing import Any

# Module-level state
_config_path: Path = Path.home() / ".config" / "gri" / "config.toml"
_config: dict[str, Any] = {}
_overrides: dict[str, Any] = {}
_last_mtime: float | None = None


def _flatten_dict(d: dict[str, Any], parent_key: str = "") -> dict[str, Any]:
    """Flatten nested dict with dot-separated keys.

    Args:
        d: Dictionary to flatten.
        parent_key: Prefix for keys (used in recursion).

    Returns:
        Flattened dictionary with dot-separated keys.

    """
    items: list[tuple[str, Any]] = []
    for k, v in d.items():
        new_key = f"{parent_key}.{k}" if parent_key else k
        if isinstance(v, dict):
            items.extend(_flatten_dict(v, new_key).items())
        else:
            items.append((new_key, v))
    return dict(items)


def _reload_if_changed() -> None:
    """Reload configuration if file has been modified."""
    global _config, _last_mtime  # noqa: PLW0603 - module-level singleton state

    if not _config_path.exists():
        # No config file - use empty config
        if _config:
            _config.clear()
        _last_mtime = None
        return

    try:
        current_mtime = _config_path.stat().st_mtime
    except OSError:
        # File disappeared between exists() and stat()
        _config.clear()
        _last_mtime = None
        return

    if _last_mtime is not None and current_mtime == _last_mtime:
        # File unchanged
        return

    # Reload config
    try:
        with _config_path.open("rb") as f:
            raw_config = tomllib.load(f)
        _config = _flatten_dict(raw_config)
        _last_mtime = current_mtime
    except (OSError, tomllib.TOMLDecodeError):
        # Failed to read/parse - keep existing config
        pass


def get(key: str, default: Any = None) -> Any:  # noqa: ANN401 - config values are dynamic
    """Get configuration value.

    Args:
        key: Dot-separated configuration key (e.g., "gri-terrain.cache_dir").
        default: Default value if key not found.

    Returns:
        Configuration value, with ~ expanded in path strings.

    """
    _reload_if_changed()

    # Check overrides first
    if key in _overrides:
        value = _overrides[key]
    elif key in _config:
        value = _config[key]
    else:
        return default

    # Expand ~ in path strings
    if isinstance(value, str) and value.startswith("~"):
        value = str(Path(value).expanduser())

    return value


def get_path(key: str, default: str | Path | None = None) -> Path | None:
    """Get configuration value as Path.

    Args:
        key: Configuration key.
        default: Default path string or Path.

    Returns:
        Path object or None if not configured.

    """
    default_str = str(default) if default is not None else None
    value = get(key, default_str)
    if value is None:
        return None
    return Path(value).expanduser()


def set(key: str, value: Any) -> None:  # noqa: A001, ANN401 - mirrors stdlib, config values are dynamic
    """Set in-memory configuration override.

    This does NOT write to the config file - it only affects the current
    process. Useful for testing or runtime configuration.

    Args:
        key: Dot-separated configuration key.
        value: Value to set.

    """
    _overrides[key] = value


def set_config_path(path: str | Path) -> None:
    """Override the configuration file path.

    Clears the current config and reloads from the new path.
    Useful for testing or custom config locations.

    Args:
        path: New configuration file path.

    """
    global _config_path, _last_mtime  # noqa: PLW0603 - module-level singleton state

    _config_path = Path(path).expanduser()
    _config.clear()
    _overrides.clear()
    _last_mtime = None
    _reload_if_changed()


def _reset_for_testing() -> None:
    """Reset module state for testing purposes.

    This resets all state to initial values, allowing tests to start fresh.
    Not part of the public API.

    """
    global _config_path, _last_mtime  # noqa: PLW0603 - module-level singleton state

    _config_path = Path.home() / ".config" / "gri" / "config.toml"
    _config.clear()
    _overrides.clear()
    _last_mtime = None
