import json
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

from vna_system.core import config as cfg
from vna_system.core.logging.logger import get_component_logger

logger = get_component_logger(__file__)


class VNAMode(Enum):
    """Supported VNA measurement modes."""
    S11 = "s11"
    S21 = "s21"


@dataclass(slots=True)
class ConfigPreset:
    """
    Parsed configuration preset derived from a filename.

    Fields
    ------
    filename:
        Original filename (e.g., 's11_start100_stop8800_points1000_bw1khz.bin').
    mode:
        VNA mode (S11 or S21).
    start_freq:
        Start frequency in Hz (None if not provided in the filename).
    stop_freq:
        Stop frequency in Hz (None if not provided in the filename).
    points:
        Number of sweep points (None if not provided).
    bandwidth:
        IF bandwidth in Hz (None if not provided).
    """
    filename: str
    mode: VNAMode
    start_freq: float | None = None
    stop_freq: float | None = None
    points: int | None = None
    bandwidth: float | None = None


class PresetManager:
    """
    Discover, parse, and manage configuration presets stored on disk.

    Directory layout
    ----------------
    <BASE_DIR>/vna_system/binary_input/
        ├─ config_inputs/
        │    └─ *.bin (preset files; configuration encoded in filename)
        └─ current_input.json  # Current active preset metadata

    Filenames encode parameters, e.g.:
        s11_start100_stop8800_points1000_bw1khz.bin
        s21_start0.1ghz_stop3.0ghz_points1001_bw10kHz.bin

    Parsing rules
    -------------
    - Mode must be the prefix: 's11' or 's21'
    - start/stop: numbers with optional unit suffix (hz|khz|mhz|ghz). If no suffix, defaults to MHz.
    - points: integer (points or point also accepted)
    - bw: number with optional unit suffix (hz|khz|mhz). Defaults to Hz if absent.
    """

    def __init__(self, binary_input_dir: Path | None = None) -> None:
        self.binary_input_dir = Path(binary_input_dir or (cfg.BASE_DIR / "vna_system" / "binary_input"))
        self.config_inputs_dir = self.binary_input_dir / "config_inputs"
        self.current_input_file = self.binary_input_dir / "current_input.json"

        self.config_inputs_dir.mkdir(parents=True, exist_ok=True)
        logger.debug(
            "PresetManager initialized",
            binary_input=str(self.binary_input_dir),
            config_inputs=str(self.config_inputs_dir),
        )

    # ------------------------------------------------------------------ #
    # Parsing
    # ------------------------------------------------------------------ #
    def _parse_filename(self, filename: str) -> ConfigPreset | None:  # type: ignore[name-defined]
        """
        Parse configuration parameters from a preset filename.

        Accepted fragments (case-insensitive):
            - ^s11 or ^s21
            - start<value><unit?>
            - stop<value><unit?>
            - points?<int>
            - bw<value><unit?>

        Units
        -----
        - For start/stop: hz|khz|mhz|ghz (default: MHz when absent)
        - For bw:        hz|khz|mhz     (default: Hz when absent)
        """
        base = Path(filename).stem.lower()

        # Mode at the beginning
        if base.startswith("s11"):
            mode = VNAMode.S11
        elif base.startswith("s21"):
            mode = VNAMode.S21
        else:
            logger.debug("Filename does not start with mode token", filename=filename)
            return None

        preset = ConfigPreset(filename=filename, mode=mode)

        # Patterns with optional unit suffixes
        pat_start = r"start(?P<val>\d+(?:\.\d+)?)(?P<unit>hz|khz|mhz|ghz)?"
        pat_stop = r"stop(?P<val>\d+(?:\.\d+)?)(?P<unit>hz|khz|mhz|ghz)?"
        pat_points = r"points?(?P<val>\d+)"
        pat_bw = r"bw(?P<val>\d+(?:\.\d+)?)(?P<unit>hz|khz|mhz)?"

        def _match(pattern: str) -> re.Match[str] | None:
            return re.search(pattern, base, flags=re.IGNORECASE)

        m = _match(pat_start)
        if m:
            preset.start_freq = self._to_hz(float(m.group("val")), (m.group("unit") or "mhz"))

        m = _match(pat_stop)
        if m:
            preset.stop_freq = self._to_hz(float(m.group("val")), (m.group("unit") or "mhz"))

        m = _match(pat_points)
        if m:
            preset.points = int(m.group("val"))

        m = _match(pat_bw)
        if m:
            preset.bandwidth = self._to_hz(float(m.group("val")), (m.group("unit") or "hz"))

        logger.debug(
            "Preset filename parsed",
            filename=filename,
            start=preset.start_freq,
            stop=preset.stop_freq,
            points=preset.points,
            bw=preset.bandwidth,
        )
        return preset

    @staticmethod
    def _to_hz(value: float, unit: str) -> float:
        """Convert a numeric value with textual unit into Hz."""
        u = unit.lower()
        if u == "hz":
            return value
        if u == "khz":
            return value * 1e3
        if u == "mhz":
            return value * 1e6
        if u == "ghz":
            return value * 1e9
        # Fallback: treat as Hz if unknown
        return value

    # ------------------------------------------------------------------ #
    # Discovery & selection
    # ------------------------------------------------------------------ #
    def get_available_presets(self) -> list[ConfigPreset]:
        """
        Return a list of available presets discovered in `config_inputs_dir`.

        Only files that parse successfully are returned.
        """
        presets: list[ConfigPreset] = []
        if not self.config_inputs_dir.exists():
            return presets

        for path in self.config_inputs_dir.glob("*.bin"):
            p = self._parse_filename(path.name)
            if p is not None:
                presets.append(p)

        presets.sort(key=lambda x: x.filename.lower())
        logger.debug("Available presets enumerated", count=len(presets))
        return presets

    def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset:
        """
        Select a preset by saving it to JSON file.
        """
        src = self.config_inputs_dir / preset.filename
        if not src.exists():
            raise FileNotFoundError(f"Preset file not found: {preset.filename}")

        # Write current preset info to JSON file
        current_info = {
            "filename": preset.filename,
            "mode": preset.mode.value,
            "start_freq": preset.start_freq,
            "stop_freq": preset.stop_freq,
            "points": preset.points,
            "bandwidth": preset.bandwidth
        }

        with self.current_input_file.open("w", encoding="utf-8") as f:
            json.dump(current_info, f, indent=2, ensure_ascii=False)

        logger.info("Current preset set", filename=preset.filename)
        return preset

    def get_current_preset(self) -> ConfigPreset | None:
        """
        Load current preset from JSON file.

        Returns None if the file is missing or invalid.
        """
        if not self.current_input_file.exists():
            return None

        try:
            with self.current_input_file.open("r", encoding="utf-8") as f:
                current_info = json.load(f)

            filename = current_info.get("filename")
            if not filename:
                logger.warning("Invalid current preset file format")
                return None

            # Verify the file still exists
            if not (self.config_inputs_dir / filename).exists():
                logger.warning("Current preset file no longer exists", filename=filename)
                return None

            return self._parse_filename(filename)
        except Exception as exc:  # noqa: BLE001
            logger.warning("Failed to load current preset", error=repr(exc))
            return None

    def preset_exists(self, preset: ConfigPreset) -> bool:
        """Return True if the underlying file for `preset` exists."""
        exists = (self.config_inputs_dir / preset.filename).exists()
        logger.debug("Preset existence checked", filename=preset.filename, exists=exists)
        return exists
