"""Application configuration helpers and RecorderConfig dataclass.

Provides default paths, environment override logic, and persistence for
recorder runtime configuration.
"""

import json
import logging
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, ClassVar

CONFIG_DIR_NAME = ".adcp-recorder"
CONFIG_FILE_NAME = "config.json"
LOGGER = logging.getLogger(__name__)

# ── Platform-specific path defaults ──────────────────────────────────
WIN_BASE_DIR = r"C:\s1000"
WIN_DATA_DIR = WIN_BASE_DIR + r"\data"
WIN_CONF_DIR = WIN_BASE_DIR + r"\conf"
LINUX_DATA_DIR_NAME = "adcp_data"


def get_default_output_dir() -> str:
    """Returns the default data output directory."""
    if sys.platform == "win32":
        fallback = WIN_DATA_DIR
    else:
        fallback = str(Path.home() / LINUX_DATA_DIR_NAME)
    return os.environ.get("ADCP_RECORDER_OUTPUT_DIR", fallback)


def get_default_serial_port() -> str:
    """Returns the default serial port based on platform."""
    if sys.platform == "win32":
        return "COM6"
    return "/dev/ttyUSB0"


@dataclass
class RecorderConfig:
    """Configuration container for the ADCP Recorder application.

    Holds runtime settings, persisted fields, and environment override logic.
    """

    serial_port: str = field(default_factory=get_default_serial_port)
    baudrate: int = 9600
    timeout: float = 1.0
    output_dir: str = field(default_factory=get_default_output_dir)
    log_level: str = "INFO"

    # Database settings
    db_path: str | None = None  # If None, will default to output_dir/adcp.duckdb

    # Fields persisted to disk
    PERSISTED_FIELDS: ClassVar[tuple[str, ...]] = (
        "serial_port",
        "baudrate",
        "timeout",
        "output_dir",
        "log_level",
        "db_path",
    )

    ENV_PREFIX: ClassVar[str] = "ADCP_RECORDER_"
    ENV_OVERRIDES: ClassVar[dict[str, Any]] = {
        "serial_port": str,
        "baudrate": int,
        "timeout": float,
        "output_dir": str,
        "log_level": str,
        "db_path": str,
    }

    @classmethod
    def get_default_config_dir(cls) -> Path:
        """Returns the default configuration directory path.

        On Windows, uses ``WIN_CONF_DIR`` for system-wide config.
        On other platforms, uses the user's home directory.
        """
        if sys.platform == "win32":
            return Path(WIN_CONF_DIR)
        return Path.home() / CONFIG_DIR_NAME

    @classmethod
    def get_config_path(cls) -> Path:
        """Returns the full path to the configuration file."""
        return cls.get_default_config_dir() / CONFIG_FILE_NAME

    @classmethod
    def load(cls) -> "RecorderConfig":
        """Loads configuration from file or returns defaults if not found."""
        config_path = cls.get_config_path()

        if not config_path.exists():
            config = cls()
            config.save()
            return cls._apply_env_overrides(config)

        try:
            with open(config_path) as f:
                data = json.load(f)
            # Filter out keys that might not be in the dataclass anymore
            # This is a basic way to handle schema evolution/extra keys
            valid_keys = cls.__annotations__.keys()
            filtered_data = {k: v for k, v in data.items() if k in valid_keys}
            config = cls(**filtered_data)

            # If serial_port was not in the loaded data, it will be the default.
            # We want to ensure it's written to the config file if it wasn't there.
            if "serial_port" not in data:
                config.save()

        except (json.JSONDecodeError, OSError) as e:
            # Fallback to default if corrupted, maybe log warning in future
            print(f"Warning: Could not load config, using defaults. Error: {e}")
            config = cls()
            # Since we are using defaults, save them to disk
            config.save()

        return cls._apply_env_overrides(config)

    @classmethod
    def _apply_env_overrides(cls, config: "RecorderConfig") -> "RecorderConfig":
        """Apply environment variable overrides to a RecorderConfig instance.

        Reads environment variables prefixed by `ENV_PREFIX` and casts them
        using the types defined in `ENV_OVERRIDES`.
        """

        overrides = cls.ENV_OVERRIDES or {}
        prefix = cls.ENV_PREFIX

        for attr, caster in overrides.items():
            env_var = f"{prefix}{attr.upper()}"
            raw_value = os.environ.get(env_var)
            if raw_value is None:
                continue

            try:
                value = caster(raw_value)
            except (ValueError, TypeError) as exc:
                LOGGER.warning("Ignoring %s=%r; cannot parse (%s)", env_var, raw_value, exc)
                continue

            setattr(config, attr, value)

        return config

    def _to_persisted_dict(self) -> dict[str, Any]:
        """Return a dict containing only fields that should be persisted to disk."""
        return {field: getattr(self, field) for field in self.PERSISTED_FIELDS}

    def save(self) -> None:
        """Saves current configuration to file."""
        config_path = self.get_config_path()
        config_dir = config_path.parent

        if not config_dir.exists():
            config_dir.mkdir(parents=True, exist_ok=True)

        with open(config_path, "w") as f:
            json.dump(self._to_persisted_dict(), f, indent=4)

    def update(self, **kwargs: Any) -> None:
        """Updates configuration with provided values."""
        for key, value in kwargs.items():
            if hasattr(self, key) and value is not None:
                setattr(self, key, value)
        self.save()
