import json
import shutil
from datetime import datetime
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
from vna_system.core.acquisition.sweep_buffer import SweepData
from vna_system.core.settings.preset_manager import ConfigPreset, VNAMode

logger = get_component_logger(__file__)


class CalibrationStandard(Enum):
    """Supported calibration standards."""
    OPEN = "open"
    SHORT = "short"
    LOAD = "load"
    THROUGH = "through"


class CalibrationSet:
    """
    In-memory container of calibration measurements for a specific VNA preset.

    A set is *complete* when all standards required by the preset mode are present:
        - S11: OPEN, SHORT, LOAD
        - S21: THROUGH
    """

    def __init__(self, preset: ConfigPreset, name: str = "") -> None:
        self.preset = preset
        self.name = name
        self.standards: dict[CalibrationStandard, SweepData] = {}

    # ------------------------------ mutation ------------------------------ #
    def add_standard(self, standard: CalibrationStandard, sweep_data: SweepData) -> None:
        """Attach sweep data for a given standard."""
        self.standards[standard] = sweep_data
        logger.debug("Calibration standard added", standard=standard.value, points=sweep_data.total_points)

    def remove_standard(self, standard: CalibrationStandard) -> None:
        """Remove sweep data for a given standard if present."""
        if standard in self.standards:
            del self.standards[standard]
            logger.debug("Calibration standard removed", standard=standard.value)

    # ------------------------------ queries -------------------------------- #
    def has_standard(self, standard: CalibrationStandard) -> bool:
        """Return True if the standard is present in the set."""
        return standard in self.standards

    def is_complete(self) -> bool:
        """Return True if all required standards for the preset mode are present."""
        required = self._get_required_standards()
        complete = all(s in self.standards for s in required)
        logger.debug("Calibration completeness checked", complete=complete, required=[s.value for s in required])
        return complete

    def get_missing_standards(self) -> list[CalibrationStandard]:
        """List the standards still missing for a complete set."""
        required = self._get_required_standards()
        return [s for s in required if s not in self.standards]

    def get_progress(self) -> tuple[int, int]:
        """Return (completed, total_required) for progress display."""
        required = self._get_required_standards()
        completed = sum(1 for s in required if s in self.standards)
        return completed, len(required)

    # ------------------------------ internals ------------------------------ #
    def _get_required_standards(self) -> list[CalibrationStandard]:
        """Standards required by the current preset mode."""
        if self.preset.mode == VNAMode.S11:
            return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
        if self.preset.mode == VNAMode.S21:
            return [CalibrationStandard.THROUGH]
        return []


class CalibrationManager:
    """
    Filesystem-backed manager for calibration sets.

    Layout
    ------
    <BASE_DIR>/calibration/
        ├─ current_calibration.json  # Current active calibration metadata
        ├─ <preset_name>/
        │    └─ <calibration_name>/
        │         ├─ open.json / short.json / load.json / through.json
        │         ├─ open_metadata.json ... (per-standard metadata)
        │         └─ calibration_info.json (set-level metadata)
    """

    def __init__(self, base_dir: Path | None = None) -> None:
        self.base_dir = Path(base_dir or cfg.BASE_DIR)
        self.calibration_dir = self.base_dir / "calibration"
        self.current_calibration_file = self.calibration_dir / "current_calibration.json"
        self.calibration_dir.mkdir(parents=True, exist_ok=True)

        self._current_working_set: CalibrationSet | None = None
        logger.debug("CalibrationManager initialized", base_dir=str(self.base_dir))

    # ------------------------------------------------------------------ #
    # Working set lifecycle
    # ------------------------------------------------------------------ #
    def start_new_calibration(self, preset: ConfigPreset) -> CalibrationSet:
        """Start a new, empty working set for a given preset."""
        self._current_working_set = CalibrationSet(preset)
        logger.info("New calibration session started", preset=preset.filename, mode=preset.mode.value)
        return self._current_working_set

    def get_current_working_set(self) -> CalibrationSet | None:
        """Return the current working set (if any)."""
        return self._current_working_set

    def add_calibration_standard(self, standard: CalibrationStandard, sweep_data: SweepData) -> None:
        """Add a standard measurement to the active working set."""
        if self._current_working_set is None:
            raise RuntimeError("No active calibration set. Call start_new_calibration first.")
        self._current_working_set.add_standard(standard, sweep_data)

    def remove_calibration_standard(self, standard: CalibrationStandard) -> None:
        """Remove a standard measurement from the working set."""
        if self._current_working_set is None:
            raise RuntimeError("No active calibration set.")
        self._current_working_set.remove_standard(standard)

    # ------------------------------------------------------------------ #
    # Persistence
    # ------------------------------------------------------------------ #
    def save_calibration_set(self, calibration_name: str) -> CalibrationSet:
        """
        Persist the working set to disk under the preset directory.

        Writes:
            - per-standard sweeps as JSON
            - per-standard metadata
            - set-level metadata
        """
        if self._current_working_set is None:
            raise RuntimeError("No active calibration set to save.")

        if not self._current_working_set.is_complete():
            missing = [s.value for s in self._current_working_set.get_missing_standards()]
            raise ValueError(f"Calibration incomplete. Missing standards: {missing}")

        preset = self._current_working_set.preset
        calib_dir = self._get_preset_calibration_dir(preset) / calibration_name
        calib_dir.mkdir(parents=True, exist_ok=True)

        # Save standards
        for standard, sweep in self._current_working_set.standards.items():
            sweep_json = {
                "sweep_number": sweep.sweep_number,
                "timestamp": sweep.timestamp,
                "points": sweep.points,
                "total_points": sweep.total_points,
            }
            self._atomic_json_write(calib_dir / f"{standard.value}.json", sweep_json)

            metadata = {
                "preset": {
                    "filename": preset.filename,
                    "mode": preset.mode.value,
                    "start_freq": preset.start_freq,
                    "stop_freq": preset.stop_freq,
                    "points": preset.points,
                    "bandwidth": preset.bandwidth,
                },
                "calibration_name": calibration_name,
                "standard": standard.value,
                "sweep_number": sweep.sweep_number,
                "sweep_timestamp": sweep.timestamp,
                "created_timestamp": datetime.now().isoformat(),
                "total_points": sweep.total_points,
            }
            self._atomic_json_write(calib_dir / f"{standard.value}_metadata.json", metadata)

        # Save set-level metadata
        set_metadata = {
            "preset": {
                "filename": preset.filename,
                "mode": preset.mode.value,
                "start_freq": preset.start_freq,
                "stop_freq": preset.stop_freq,
                "points": preset.points,
                "bandwidth": preset.bandwidth,
            },
            "calibration_name": calibration_name,
            "standards": [s.value for s in self._current_working_set.standards],
            "created_timestamp": datetime.now().isoformat(),
            "is_complete": True,
        }
        self._atomic_json_write(calib_dir / "calibration_info.json", set_metadata)

        # Update working set name
        self._current_working_set.name = calibration_name
        logger.info("Calibration set saved", preset=preset.filename, name=calibration_name)
        return self._current_working_set

    def load_calibration_set(self, preset: ConfigPreset, calibration_name: str) -> CalibrationSet:
        """Load a calibration set from disk for the given preset."""
        preset_dir = self._get_preset_calibration_dir(preset)
        calib_dir = preset_dir / calibration_name
        if not calib_dir.exists():
            raise FileNotFoundError(f"Calibration not found: {calibration_name}")

        calib_set = CalibrationSet(preset, calibration_name)

        for standard in CalibrationStandard:
            file_path = calib_dir / f"{standard.value}.json"
            if not file_path.exists():
                continue
            try:
                data = json.loads(file_path.read_text(encoding="utf-8"))
                sweep = SweepData(
                    sweep_number=int(data["sweep_number"]),
                    timestamp=float(data["timestamp"]),
                    points=[(float(r), float(i)) for r, i in data["points"]],
                    total_points=int(data["total_points"]),
                )
                calib_set.add_standard(standard, sweep)
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to load standard file", file=str(file_path), error=repr(exc))

        logger.info("Calibration set loaded", preset=preset.filename, name=calibration_name)
        return calib_set

    # ------------------------------------------------------------------ #
    # Discovery & info
    # ------------------------------------------------------------------ #
    def get_available_calibrations(self, preset: ConfigPreset) -> list[str]:
        """Return sorted list of calibration set names available for a preset."""
        preset_dir = self._get_preset_calibration_dir(preset)
        if not preset_dir.exists():
            return []
        names = sorted([p.name for p in preset_dir.iterdir() if p.is_dir()])
        logger.debug("Available calibrations listed", preset=preset.filename, count=len(names))
        return names

    def get_calibration_info(self, preset: ConfigPreset, calibration_name: str) -> dict:
        """
        Return set-level info for a calibration (from cached metadata if present,
        or by scanning the directory as a fallback).
        """
        preset_dir = self._get_preset_calibration_dir(preset)
        calib_dir = preset_dir / calibration_name
        info_file = calib_dir / "calibration_info.json"

        if info_file.exists():
            try:
                return json.loads(info_file.read_text(encoding="utf-8"))
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to read calibration_info.json; falling back to scan", error=repr(exc))

        required = self._get_required_standards(preset.mode)
        standards: dict[str, bool] = {}
        for s in required:
            standards[s.value] = (calib_dir / f"{s.value}.json").exists()

        return {
            "calibration_name": calibration_name,
            "standards": standards,
            "is_complete": all(standards.values()),
        }

    # ------------------------------------------------------------------ #
    # Current calibration (JSON file)
    # ------------------------------------------------------------------ #
    def set_current_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
        """Save the current calibration selection to JSON file."""
        preset_dir = self._get_preset_calibration_dir(preset)
        calib_dir = preset_dir / calibration_name
        if not calib_dir.exists():
            raise FileNotFoundError(f"Calibration not found: {calibration_name}")

        info = self.get_calibration_info(preset, calibration_name)
        if not info.get("is_complete", False):
            raise ValueError(f"Calibration {calibration_name} is incomplete")

        # Write current calibration info to JSON file
        try:
            current_info = {
                "preset_filename": preset.filename,
                "calibration_name": calibration_name,
                "preset_dir": preset.filename.replace(".bin", ""),
                "updated_at": datetime.now().isoformat()
            }
            self._atomic_json_write(self.current_calibration_file, current_info)
            logger.info("Current calibration set", preset=preset.filename, name=calibration_name)
        except Exception as exc:  # noqa: BLE001
            logger.error("Failed to set current calibration", error=repr(exc))
            raise

    def get_current_calibration(self, current_preset: ConfigPreset) -> CalibrationSet | None:
        """
        Load the calibration currently saved in JSON file.

        Returns None if the file doesn't exist, is invalid,
        or targets a different preset.
        """
        if not self.current_calibration_file.exists():
            return None

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

            calibration_name = current_info.get("calibration_name")
            preset_filename = current_info.get("preset_filename")

            if not calibration_name or not preset_filename:
                logger.warning("Invalid current calibration file format")
                return None

            if preset_filename != current_preset.filename:
                logger.warning(
                    "Current calibration preset mismatch",
                    expected=current_preset.filename,
                    actual=preset_filename,
                )
                return None

            return self.load_calibration_set(current_preset, calibration_name)
        except Exception as exc:  # noqa: BLE001
            logger.warning("Failed to load current calibration", error=repr(exc))
            return None

    def clear_current_calibration(self) -> None:
        """Remove the current calibration JSON file."""
        if self.current_calibration_file.exists():
            try:
                self.current_calibration_file.unlink()
                logger.info("Current calibration cleared")
            except Exception as exc:  # noqa: BLE001
                logger.warning("Failed to clear current calibration", error=repr(exc))

    # ------------------------------------------------------------------ #
    # Deletion
    # ------------------------------------------------------------------ #
    def delete_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
        """Delete a calibration set directory."""
        calib_dir = self._get_preset_calibration_dir(preset) / calibration_name
        if calib_dir.exists():
            shutil.rmtree(calib_dir)
            logger.info("Calibration deleted", preset=preset.filename, name=calibration_name)

    # ------------------------------------------------------------------ #
    # Helpers
    # ------------------------------------------------------------------ #
    def _get_preset_calibration_dir(self, preset: ConfigPreset) -> Path:
        """Return the directory where calibrations for this preset are stored."""
        preset_dir = self.calibration_dir / preset.filename.replace(".bin", "")
        preset_dir.mkdir(parents=True, exist_ok=True)
        return preset_dir

    def _get_required_standards(self, mode: VNAMode) -> list[CalibrationStandard]:
        """Standards required for a given VNA mode."""
        if mode == VNAMode.S11:
            return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
        if mode == VNAMode.S21:
            return [CalibrationStandard.THROUGH]
        return []

    @staticmethod
    def _atomic_json_write(path: Path, payload: dict) -> None:
        """Write JSON atomically via a temporary sidecar file."""
        tmp = path.with_suffix(path.suffix + ".tmp")
        tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
        tmp.replace(path)
