from pathlib import Path
from typing import Any, Callable

import threading
from enum import Enum

from vna_system.core.logging.logger import get_component_logger
from vna_system.core.processors.base_processor import BaseProcessor, ProcessedResult
from vna_system.core.processors.calibration_processor import CalibrationProcessor
from vna_system.core.acquisition.sweep_buffer import SweepBuffer, SweepData
from vna_system.core.settings.preset_manager import ConfigPreset
from vna_system.core.settings.settings_manager import VNASettingsManager

logger = get_component_logger(__file__)


class ProcessorManager:
    """
    Orchestrates VNA processors and pushes sweeps through them in the background.

    Responsibilities
    ----------------
    • Keep a registry of `BaseProcessor` instances (add/get/list).
    • Subscribe result callbacks that receive every `ProcessedResult`.
    • Watch the `SweepBuffer` and, when a new sweep arrives:
        1) Optionally apply calibration.
        2) Fetch current VNA preset/config.
        3) Feed the sweep to every registered processor.
        4) Fan-out results to callbacks.
    • Offer on-demand (re)calculation for a specific processor (with config updates).

    Threading model
    ---------------
    A single background thread (`_processing_loop`) polls the `SweepBuffer` for the
    newest sweep. Access to internal state (processors, callbacks) is guarded by
    `_lock` when mutation is possible.
    """

    def __init__(self, sweep_buffer: SweepBuffer, settings_manager: VNASettingsManager, config_dir: Path) -> None:
        # External deps
        self.sweep_buffer = sweep_buffer
        self.settings_manager = settings_manager
        self.config_dir = config_dir

        # Registry & fan-out
        self._processors: dict[str, BaseProcessor] = {}
        self._result_callbacks: list[Callable[[str, ProcessedResult], None]] = []

        # Concurrency
        self._lock = threading.RLock()
        self._running = False
        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()

        # Sweep progress
        self._last_processed_sweep = 0

        # Calibration facility
        self.calibration_processor = CalibrationProcessor()

        # Default processors (safe to skip if missing)
        self._register_default_processors()
        logger.debug(
            "ProcessorManager initialized",
            processors=list(self._processors.keys()),
            config_dir=str(self.config_dir),
        )

    # --------------------------------------------------------------------- #
    # Registry
    # --------------------------------------------------------------------- #
    def register_processor(self, processor: BaseProcessor) -> None:
        """Register (or replace) a processor by its `processor_id`."""
        with self._lock:
            self._processors[processor.processor_id] = processor
            logger.info("Processor registered", processor_id=processor.processor_id)

    def get_processor(self, processor_id: str) -> BaseProcessor | None:
        """Return a processor instance by id, or None if not found."""
        return self._processors.get(processor_id)

    def list_processors(self) -> list[str]:
        """Return a stable snapshot list of registered processor ids."""
        return list(self._processors.keys())

    # --------------------------------------------------------------------- #
    # Results fan-out
    # --------------------------------------------------------------------- #
    def add_result_callback(self, callback: Callable[[str, ProcessedResult], None]) -> None:
        """
        Add a callback invoked for every produced result.

        Callback signature:
            (processor_id: str, result: ProcessedResult) -> None
        """
        with self._lock:
            self._result_callbacks.append(callback)
            logger.debug("Result callback added", callbacks=len(self._result_callbacks))

    # --------------------------------------------------------------------- #
    # Main processing actions
    # --------------------------------------------------------------------- #
    def process_sweep(self, sweep_data: SweepData, calibrated_data: Any, vna_config: ConfigPreset | None, reference_data: Any = None, reference_info: Any = None, raw_reference_data: Any = None, calibration_standards: dict | None = None) -> dict[str, ProcessedResult]:
        """
        Feed a sweep into all processors and dispatch results to callbacks.

        Returns a map {processor_id: ProcessedResult} for successfully computed processors.
        """
        results: dict[str, ProcessedResult] = {}
        with self._lock:
            # Snapshot to avoid holding the lock while user callbacks run
            processors_items = list(self._processors.items())
            callbacks = list(self._result_callbacks)

        for processor_id, processor in processors_items:
            try:
                result = processor.add_sweep_data(sweep_data, calibrated_data, vna_config, reference_data, reference_info, raw_reference_data, calibration_standards)
                if result:
                    results[processor_id] = result
                    for cb in callbacks:
                        try:
                            cb(processor_id, result)
                        except Exception as exc:  # noqa: BLE001
                            logger.error("Result callback failed", processor_id=processor_id, error=repr(exc))
            except Exception as exc:  # noqa: BLE001
                logger.error("Processing error", processor_id=processor_id, error=repr(exc))

        return results

    def recalculate_processor(self, processor_id: str, config_updates: dict[str, Any] | None = None) -> ProcessedResult | None:
        """
        Recalculate a single processor with optional config updates.

        - If `config_updates` is provided, they are applied and validated first.
        - The latest sweep (from the processor's own history) is used for recomputation.
        - Result callbacks are invoked if a result is produced.
        """
        processor = self.get_processor(processor_id)
        if not processor:
            raise ValueError(f"Processor {processor_id} not found")

        try:
            if config_updates:
                processor.update_config(config_updates)

            result = processor.recalculate()
            if result:
                with self._lock:
                    callbacks = list(self._result_callbacks)
                for cb in callbacks:
                    try:
                        cb(processor_id, result)
                    except Exception as exc:  # noqa: BLE001
                        logger.error("Result callback failed", processor_id=processor_id, error=repr(exc))
            return result
        except Exception as exc:  # noqa: BLE001
            logger.error("Recalculation error", processor_id=processor_id, error=repr(exc))
            raise

    def load_processor_history(self, processor_id: str, history_data: list[dict[str, Any]], config: dict[str, Any] | None = None) -> ProcessedResult | None:
        """
        Load sweep history into a processor from JSON data and recalculate.

        Also restores the processor configuration (UI parameters) if provided.

        Parameters
        ----------
        processor_id : str
            The processor to load history into.
        history_data : list[dict]
            History records in the format exported by export_history_data.
        config : dict[str, Any] | None
            Processor configuration (UI parameters) to restore.

        Returns
        -------
        ProcessedResult | None
            The result of recalculation after loading history.
        """
        processor = self.get_processor(processor_id)
        if not processor:
            raise ValueError(f"Processor {processor_id} not found")

        try:
            # Restore processor configuration first if provided
            if config:
                processor.update_config(config)
                logger.info("Processor config restored", processor_id=processor_id, config_keys=list(config.keys()))

            processor.import_history_data(history_data)
            result = processor.recalculate()

            if result:
                with self._lock:
                    callbacks = list(self._result_callbacks)
                for cb in callbacks:
                    try:
                        cb(processor_id, result)
                    except Exception as exc:  # noqa: BLE001
                        logger.error("Result callback failed", processor_id=processor_id, error=repr(exc))

            logger.info("History loaded and recalculated", processor_id=processor_id, records=len(history_data))
            return result

        except Exception as exc:  # noqa: BLE001
            logger.error("History load error", processor_id=processor_id, error=repr(exc))
            raise

    def append_processor_history(self, processor_id: str, history_data: list[dict[str, Any]]) -> ProcessedResult | None:
        """
        Append sweep history to existing processor history from JSON data and recalculate.

        Does NOT update processor configuration - only appends history.

        Parameters
        ----------
        processor_id : str
            The processor to append history to.
        history_data : list[dict]
            History records in the format exported by export_history_data.

        Returns
        -------
        ProcessedResult | None
            The result of recalculation after appending history.
        """
        processor = self.get_processor(processor_id)
        if not processor:
            raise ValueError(f"Processor {processor_id} not found")

        try:
            result = processor.append_history(history_data)

            if result:
                with self._lock:
                    callbacks = list(self._result_callbacks)
                for cb in callbacks:
                    try:
                        cb(processor_id, result)
                    except Exception as exc:  # noqa: BLE001
                        logger.error("Result callback failed", processor_id=processor_id, error=repr(exc))

            logger.info("History appended and recalculated", processor_id=processor_id, records=len(history_data))
            return result

        except Exception as exc:  # noqa: BLE001
            logger.error("History append error", processor_id=processor_id, error=repr(exc))
            raise

    def build_processor_state(self, processor_id: str) -> dict[str, Any]:
        """Return a JSON-ready snapshot of processor state and current result."""
        processor = self.get_processor(processor_id)
        if processor is None:
            raise ValueError(f"Processor {processor_id} not found")

        state_payload = self._json_ready(processor.get_full_state())
        result = processor.recalculate()
        current_data = None
        if result is not None:
            current_data = {
                "data": self._json_ready(result.data),
                "plotly_config": self._json_ready(result.plotly_config),
                "timestamp": float(result.timestamp),
            }

        return {
            "type": "processor_state",
            "processor_id": processor_id,
            "state": state_payload,
            "current_data": current_data,
        }

    # --------------------------------------------------------------------- #
    # Runtime control
    # --------------------------------------------------------------------- #
    def set_sweep_buffer(self, sweep_buffer: SweepBuffer) -> None:
        """Swap the underlying sweep buffer (takes effect immediately)."""
        self.sweep_buffer = sweep_buffer
        logger.info("Sweep buffer updated")

    def start_processing(self) -> None:
        """
        Start the background processing thread.

        Safe to call multiple times (no-op if already running).
        """
        if self._running:
            logger.debug("start_processing ignored; already running")
            return

        self._running = True
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._processing_loop, daemon=True, name="VNA-ProcessorManager")
        self._thread.start()
        logger.info("Processor manager started")

    def stop_processing(self) -> None:
        """
        Stop the background thread and wait briefly for it to join.
        """
        if not self._running:
            logger.debug("stop_processing ignored; not running")
            return

        self._running = False
        self._stop_event.set()
        if self._thread:
            self._thread.join(timeout=1.0)
        logger.info("Processor manager stopped")

    # --------------------------------------------------------------------- #
    # Background loop
    # --------------------------------------------------------------------- #
    def _processing_loop(self) -> None:
        """
        Poll the sweep buffer and process any new sweep.

        The loop:
            • Grabs the latest sweep.
            • Skips if it's already processed.
            • Applies calibration (when available).
            • Retrieves current VNA preset.
            • Sends the sweep to all processors.
            • Sleeps briefly to keep CPU usage low.
        """
        while self._running and not self._stop_event.is_set():
            try:
                latest = self.sweep_buffer.get_latest_sweep()
                if latest and latest.sweep_number > self._last_processed_sweep:
                    calibrated = self._apply_calibration(latest)
                    vna_cfg = self.settings_manager.get_current_preset()

                    # Get raw and calibrated reference data
                    raw_reference_sweep = self.settings_manager.get_current_reference_sweep(vna_cfg)
                    calibrated_reference_data = self._apply_calibration(raw_reference_sweep)
                    reference_info = self.settings_manager.get_current_reference(vna_cfg)

                    # Get calibration standards for export
                    calib_set = self.settings_manager.get_current_calibration()
                    calibration_standards = None
                    if calib_set and calib_set.standards:
                        calibration_standards = {std.value: sweep_data for std, sweep_data in calib_set.standards.items()}

                    self.process_sweep(latest, calibrated, vna_cfg, calibrated_reference_data, reference_info, raw_reference_sweep, calibration_standards)
                    self._last_processed_sweep = latest.sweep_number

                # Light-duty polling to reduce wakeups
                self._stop_event.wait(0.05)
            except Exception as exc:  # noqa: BLE001
                logger.error("Error in processing loop", error=repr(exc))
                self._stop_event.wait(0.1)

    # --------------------------------------------------------------------- #
    # Calibration
    # --------------------------------------------------------------------- #
    def _apply_calibration(self, sweep_data: SweepData | None) -> SweepData | None:
        """
        Apply calibration to the sweep when a complete set is available.

        Returns the original sweep on failure or when no calibration is present.
        """
        if sweep_data is None:
            return None

        try:
            calib_set = self.settings_manager.get_current_calibration()
            if calib_set and calib_set.is_complete():
                points = self.calibration_processor.apply_calibration(sweep_data, calib_set)
                calibrated = SweepData(
                    sweep_number=sweep_data.sweep_number,
                    timestamp=sweep_data.timestamp,
                    points=points,
                    total_points=len(points),
                )
                logger.debug(
                    "Sweep calibrated",
                    sweep_number=calibrated.sweep_number,
                    points=calibrated.total_points,
                )
                return calibrated
        except Exception as exc:  # noqa: BLE001
            logger.error("Calibration failed", error=repr(exc))

        # Fallback: return the original data
        return sweep_data

    # --------------------------------------------------------------------- #
    # Defaults
    # --------------------------------------------------------------------- #
    def _register_default_processors(self) -> None:
        """
        Attempt to import and register default processors.

        This is best-effort: if anything fails (e.g., module not present),
        we log an error and keep going with whatever is available.
        """
        try:
            from .implementations.magnitude_processor import MagnitudeProcessor
            from .implementations.bscan_processor import BScanProcessor

            
            # self.register_processor(PhaseProcessor(self.config_dir))
            self.register_processor(BScanProcessor(self.config_dir))
            self.register_processor(MagnitudeProcessor(self.config_dir))
            # self.register_processor(SmithChartProcessor(self.config_dir))

            logger.info("Default processors registered", count=len(self._processors))
        except Exception as exc:  # noqa: BLE001
            logger.error("Failed to register default processors", error=repr(exc))

    # --------------------------------------------------------------------- #
    # JSON coercion helpers
    # --------------------------------------------------------------------- #
    @staticmethod
    def _json_ready(obj: Any) -> Any:
        """Convert numpy-like objects to JSON primitives recursively."""
        if obj is None or isinstance(obj, (str, bool, int, float)):
            return obj

        if isinstance(obj, Enum):
            return obj.value

        if isinstance(obj, dict):
            return {key: ProcessorManager._json_ready(value) for key, value in obj.items()}

        if isinstance(obj, (list, tuple, set)):
            return [ProcessorManager._json_ready(item) for item in obj]

        item_method = getattr(obj, "item", None)
        if callable(item_method):
            try:
                return item_method()
            except (TypeError, ValueError):
                pass

        tolist_method = getattr(obj, "tolist", None)
        if callable(tolist_method):
            try:
                return ProcessorManager._json_ready(tolist_method())
            except (TypeError, ValueError):
                pass

        return obj
