from pathlib import Path
from typing import Any

from vna_system.core import config as cfg
from vna_system.core.logging.logger import get_component_logger
from vna_system.core.acquisition.data_acquisition import VNADataAcquisition
from vna_system.core.acquisition.sweep_buffer import SweepData
from vna_system.core.settings.preset_manager import PresetManager, ConfigPreset, VNAMode
from vna_system.core.settings.calibration_manager import (
    CalibrationManager,
    CalibrationSet,
    CalibrationStandard,
)
from vna_system.core.settings.reference_manager import ReferenceManager, ReferenceInfo

logger = get_component_logger(__file__)


class VNASettingsManager:
    """
    High-level coordinator for presets and calibrations.

    Responsibilities
    ----------------
    • Discover/select configuration presets (via `PresetManager`).
    • Create/store/select calibration sets (via `CalibrationManager`).
    • Provide combined status used by API/UI.
    • Offer helpers to capture calibration directly from acquisition.

    Notes
    -----
    - All IO paths are derived from `cfg.BASE_DIR`.
    - Logging is performed via the project logger.
    """

    def __init__(self, base_dir: Path | None = None) -> None:
        self.base_dir = Path(base_dir or cfg.BASE_DIR)

        # Sub-managers
        self.preset_manager = PresetManager(self.base_dir / "binary_input")
        self.calibration_manager = CalibrationManager(self.base_dir)
        self.reference_manager = ReferenceManager()

        logger.debug(
            "VNASettingsManager initialized",
            base_dir=str(self.base_dir),
        )

    # ------------------------------------------------------------------ #
    # Preset Management
    # ------------------------------------------------------------------ #
    def get_available_presets(self) -> list[ConfigPreset]:
        """Return all available configuration presets."""
        return self.preset_manager.get_available_presets()
    
    def set_current_preset(self, preset: ConfigPreset) -> ConfigPreset:
        """Set the current configuration preset (updates the symlink)."""
        chosen = self.preset_manager.set_current_preset(preset)
        logger.info("Current preset selected", filename=chosen.filename, mode=chosen.mode.value)
        return chosen

    def get_current_preset(self) -> ConfigPreset | None:
        """Return the currently selected preset, or None if not set."""
        return self.preset_manager.get_current_preset()

    # ------------------------------------------------------------------ #
    # Calibration Management
    # ------------------------------------------------------------------ #
    def start_new_calibration(self, preset: ConfigPreset | None = None) -> CalibrationSet:
        """
        Begin a new in-memory calibration session for a preset.

        If `preset` is omitted, the current preset must be set.
        """
        preset = preset or self.get_current_preset()
        if preset is None:
            raise RuntimeError("No current preset selected")

        calib = self.calibration_manager.start_new_calibration(preset)
        logger.info("Calibration session started", preset=preset.filename, mode=preset.mode.value)
        return calib

    def get_current_working_calibration(self) -> CalibrationSet | None:
        """Return the in-progress (unsaved) calibration set, if any."""
        return self.calibration_manager.get_current_working_set()

    def add_calibration_standard(self, standard: CalibrationStandard, sweep_data: SweepData) -> None:
        """Add a standard measurement to the working calibration set."""
        self.calibration_manager.add_calibration_standard(standard, sweep_data)
        logger.info(
            "Calibration standard added",
            standard=standard.value,
            sweep_number=sweep_data.sweep_number,
            points=sweep_data.total_points,
        )

    def remove_calibration_standard(self, standard: CalibrationStandard) -> None:
        """Remove a standard measurement from the working calibration set."""
        self.calibration_manager.remove_calibration_standard(standard)
        logger.info("Calibration standard removed", standard=standard.value)

    def save_calibration_set(self, calibration_name: str) -> CalibrationSet:
        """Persist the current working calibration set to disk."""
        saved = self.calibration_manager.save_calibration_set(calibration_name)
        logger.info("Calibration set saved", name=calibration_name, preset=saved.preset.filename)
        return saved

    def get_available_calibrations(self, preset: ConfigPreset | None = None) -> list[str]:
        """List available calibration set names for a preset (current if omitted)."""
        preset = preset or self.get_current_preset()
        if preset is None:
            return []
        return self.calibration_manager.get_available_calibrations(preset)

    def set_current_calibration(self, calibration_name: str, preset: ConfigPreset | None = None) -> None:
        """Activate a calibration set by updating the symlink."""
        preset = preset or self.get_current_preset()
        if preset is None:
            raise RuntimeError("No current preset selected")
        self.calibration_manager.set_current_calibration(preset, calibration_name)
        logger.info("Current calibration set", name=calibration_name, preset=preset.filename)

    def get_current_calibration(self) -> CalibrationSet | None:
        """
        Return the active (saved and selected) calibration set for the current preset,
        or None if not set/invalid.
        """
        current = self.get_current_preset()
        if current is None:
            return None
        return self.calibration_manager.get_current_calibration(current)

    def get_calibration_info(self, calibration_name: str, preset: ConfigPreset | None = None) -> dict[str, Any]:
        """Return info/metadata for a specific calibration set."""
        preset = preset or self.get_current_preset()
        if preset is None:
            raise RuntimeError("No current preset selected")
        return self.calibration_manager.get_calibration_info(preset, calibration_name)

    @staticmethod
    def get_required_standards(mode: VNAMode) -> list[CalibrationStandard]:
        """Return the list of required standards for a given VNA mode."""
        if mode == VNAMode.S11:
            return [CalibrationStandard.OPEN, CalibrationStandard.SHORT, CalibrationStandard.LOAD]
        if mode == VNAMode.S21:
            return [CalibrationStandard.THROUGH]
        return []

    # ------------------------------------------------------------------ #
    # Reference Management
    # ------------------------------------------------------------------ #
    def get_available_references(self, preset: ConfigPreset | None = None) -> list[ReferenceInfo]:
        """Return all available references for the given preset (or current preset)."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            return []
        return self.reference_manager.list_references(current_preset)

    def get_current_reference(self, preset: ConfigPreset | None = None) -> ReferenceInfo | None:
        """Return the currently selected reference for the given preset (or current preset)."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            return None
        return self.reference_manager.get_current_reference(current_preset)

    def set_current_reference(self, reference_name: str, preset: ConfigPreset | None = None) -> bool:
        """Set the current reference for the given preset (or current preset)."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            raise ValueError("No current preset available")
        return self.reference_manager.set_current_reference(reference_name, current_preset)

    def clear_current_reference(self) -> bool:
        """Clear the current reference selection."""
        return self.reference_manager.clear_current_reference()

    def get_current_reference_sweep(self, preset: ConfigPreset | None = None) -> SweepData | None:
        """Get sweep data for the currently selected reference."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            return None
        return self.reference_manager.get_current_reference_sweep(current_preset)

    def delete_reference(self, reference_name: str, preset: ConfigPreset | None = None) -> bool:
        """Delete a reference for the given preset (or current preset)."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            raise ValueError("No current preset available")
        return self.reference_manager.delete_reference(reference_name, current_preset)

    def get_reference_info(self, reference_name: str, preset: ConfigPreset | None = None) -> ReferenceInfo | None:
        """Get detailed information about a reference."""
        current_preset = preset or self.get_current_preset()
        if not current_preset:
            raise ValueError("No current preset available")
        return self.reference_manager.get_reference_info(reference_name, current_preset)

    def delete_calibration(self, preset: ConfigPreset, calibration_name: str) -> None:
        """Delete a saved calibration for the given preset."""
        self.calibration_manager.delete_calibration(preset, calibration_name)

    # ------------------------------------------------------------------ #
    # Acquisition integration
    # ------------------------------------------------------------------ #
    def capture_calibration_standard_from_acquisition(
        self,
        standard: CalibrationStandard,
        data_acquisition: VNADataAcquisition,
    ) -> int:
        """
        Capture the latest sweep from acquisition as a calibration standard.

        Returns
        -------
        int
            The sweep number captured.

        Raises
        ------
        RuntimeError
            If no sweep is available in the acquisition buffer.
        """
        # Get current sweep number before any operations
        current_sweep = data_acquisition.sweep_buffer.get_latest_sweep()
        current_sweep_number = current_sweep.sweep_number if current_sweep else -1

        # Check if acquisition is running, if not - trigger single sweep
        if not data_acquisition.is_running:
            logger.info("Acquisition not running, triggering single sweep for calibration")
            data_acquisition.trigger_single_sweep()

        # Wait for new sweep to appear (regardless of acquisition state)
        latest = self._wait_for_new_sweep(data_acquisition.sweep_buffer, current_sweep_number)

        if latest is None:
            raise RuntimeError("No sweep data available in acquisition buffer")

        self.add_calibration_standard(standard, latest)
        logger.info(
            "Captured calibration standard from acquisition",
            standard=standard.value,
            sweep_number=latest.sweep_number,
        )
        return latest.sweep_number

    def capture_reference_from_acquisition(
        self,
        reference_name: str,
        data_acquisition: VNADataAcquisition,
        description: str = "",
        metadata: dict | None = None,
        preset: ConfigPreset | None = None,
    ) -> ReferenceInfo:
        """
        Capture the latest sweep from acquisition as an open air reference.

        Args:
            reference_name: Name for the new reference
            data_acquisition: Data acquisition instance to capture from
            description: Optional description for the reference
            metadata: Optional additional metadata
            preset: Preset to use (defaults to current preset)

        Returns:
            ReferenceInfo: Created reference information

        Raises:
            RuntimeError: If no sweep is available in the acquisition buffer
            ValueError: If no current preset is available
        """
        # Get current sweep number before any operations
        current_sweep = data_acquisition.sweep_buffer.get_latest_sweep()
        current_sweep_number = current_sweep.sweep_number if current_sweep else -1

        # Check if acquisition is running, if not - trigger single sweep
        if not data_acquisition.is_running:
            logger.info("Acquisition not running, triggering single sweep for reference")
            data_acquisition.trigger_single_sweep()

        # Wait for new sweep to appear (regardless of acquisition state)
        latest = self._wait_for_new_sweep(data_acquisition.sweep_buffer, current_sweep_number)

        if latest is None:
            raise RuntimeError("No sweep data available in acquisition buffer")

        current_preset = preset or self.get_current_preset()
        if not current_preset:
            raise ValueError("No current preset available")

        # Get current calibration info to store in metadata
        current_calibration = self.get_current_calibration()
        calibration_info = None
        if current_calibration:
            calibration_info = {
                "calibration_name": current_calibration.name,
                "preset_filename": current_calibration.preset.filename
            }

        # Merge calibration info into metadata
        merged_metadata = metadata or {}
        if calibration_info:
            merged_metadata["calibration"] = calibration_info

        reference_info = self.reference_manager.create_reference(
            name=reference_name,
            preset=current_preset,
            sweep_data=latest,
            description=description,
            metadata=merged_metadata
        )

        logger.info(
            "Captured open air reference from acquisition",
            reference_name=reference_name,
            preset=current_preset.filename,
            sweep_number=latest.sweep_number,
        )
        return reference_info

    def get_status_summary(self) -> dict[str, object]:
        """Get comprehensive status of current configuration and calibration"""
        current_preset = self.get_current_preset()
        current_calibration = self.get_current_calibration()
        working_calibration = self.get_current_working_calibration()
        
        logger.info(f"Settings status requested")
        
        summary = {
            "current_preset": None,
            "current_calibration": None,
            "working_calibration": None,
            "available_presets": len(self.get_available_presets()),
            "available_calibrations": 0,
            "available_references": 0,
        }

        if current_preset:
            summary["current_preset"] = {
                "filename": current_preset.filename,
                "mode": current_preset.mode.value,
                "start_freq": current_preset.start_freq,
                "stop_freq": current_preset.stop_freq,
                "points": current_preset.points,
                "bandwidth": current_preset.bandwidth
            }
            summary["available_calibrations"] = len(self.get_available_calibrations(current_preset))
            summary["available_references"] = len(self.get_available_references(current_preset))

        if current_calibration:
            summary["current_calibration"] = {
                "preset_filename": current_calibration.preset.filename,
                "calibration_name": current_calibration.name
            }

        if working_calibration:
            completed, total = working_calibration.get_progress()
            summary["working_calibration"] = {
                "preset_filename": working_calibration.preset.filename,
                "progress": f"{completed}/{total}",
                "is_complete": working_calibration.is_complete(),
                "missing_standards": [s.value for s in working_calibration.get_missing_standards()]
            }

        return summary

    def _wait_for_new_sweep(self, sweep_buffer, current_sweep_number: int, timeout_seconds: float = 25.0) -> SweepData | None:
        """
        Wait for a new sweep to appear in the buffer with a higher sweep number.

        Args:
            sweep_buffer: The sweep buffer to monitor
            current_sweep_number: Current sweep number to wait beyond
            timeout_seconds: Maximum time to wait

        Returns:
            SweepData | None: New sweep data, or None if timeout occurred
        """
        import time
        start_time = time.time()

        while time.time() - start_time < timeout_seconds:
            latest = sweep_buffer.get_latest_sweep()
            if latest and latest.sweep_number > current_sweep_number:
                logger.debug(
                    "New sweep detected",
                    old_sweep=current_sweep_number,
                    new_sweep=latest.sweep_number
                )
                return latest

            # Short sleep to avoid busy waiting
            time.sleep(0.01)

        logger.warning(
            "Timeout waiting for new sweep",
            current_sweep=current_sweep_number,
            timeout=timeout_seconds
        )
        return None
