from dataclasses import dataclass, asdict, is_dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

import json
import math
import threading

from vna_system.core.logging.logger import get_component_logger
from vna_system.core.settings.preset_manager import ConfigPreset

logger = get_component_logger(__file__)


# =============================================================================
# Data models
# =============================================================================
@dataclass(slots=True)
class UIParameter:
    """
    Descriptor of a single UI control that also serves as a *schema* for config validation.

    Fields
    ------
    name:
        Stable key used in configs and payloads.
    label:
        Human-readable label for the UI.
    type:
        One of: "slider", "toggle", "select", "input", "button".
    value:
        Default value (also used to seed a missing config key).
    options:
        Extra, type-specific metadata used for validation and UI behavior.

    Supported `options` by type
    ---------------------------
    slider:
        {"min": <number>, "max": <number>, "step": <number>, "dtype": "int"|"float"}
        Notes:
            - dtype defaults to "float".
            - step alignment is checked from `min` when provided, otherwise from 0/0.0.
    toggle:
        {}
    select:
        {"choices": [<allowed_value>, ...]}
        Notes:
            - `choices` MUST be present and must be a list.
    input:
        {"type": "int"|"float", "min": <number>, "max": <number>}
        Notes:
            - Strings are NOT allowed; only numeric input is accepted.
            - `type` is required and controls casting/validation.
    button:
        {"action": "<human readable action>"}
        Notes:
            - The button value is ignored by validation (buttons are commands, not state).
    """
    name: str
    label: str
    type: str
    value: Any
    options: dict[str, Any] | None = None


@dataclass(slots=True)
class ProcessedResult:
    """
    Result payload emitted by processors.

    Fields
    ------
    processor_id:
        Logical processor identifier.
    timestamp:
        UNIX timestamp (float) when the result was produced.
    data:
        Arbitrary computed data (domain-specific).
    plotly_config:
        Prebuilt Plotly figure/config to render on the client.
    ui_parameters:
        The UI schema (possibly dynamic) that the client can render.
    metadata:
        Additional context useful to the UI or debugging (e.g., config snapshot).
    """
    processor_id: str
    timestamp: float
    data: dict[str, Any]
    plotly_config: dict[str, Any]
    ui_parameters: list[UIParameter]
    metadata: dict[str, Any]


# =============================================================================
# Base processor
# =============================================================================
class BaseProcessor:
    """
    Base class for sweep processors.

    Responsibilities
    ----------------
    • Manage a JSON config file (load → validate → save).
    • Keep a bounded, thread-safe history of recent sweeps.
    • Provide a uniform API for (re)calculation and result packaging.
    • Validate config against the UI schema provided by `get_ui_parameters()`.

    Integration contract (to be implemented by subclasses)
    ------------------------------------------------------
    - `process_sweep(sweep_data, calibrated_data, vna_config) -> dict[str, Any]`
        Perform the actual computation and return a pure-data dict.
    - `generate_plotly_config(processed_data, vna_config) -> dict[str, Any]`
        Convert computed data to a Plotly config the client can render.
    - `get_ui_parameters() -> list[UIParameter]`
        Provide the UI schema (and validation rules via `options`) for this processor.
    - `_get_default_config() -> dict[str, Any]`
        Provide defaults for config keys. Keys should match UIParameter names.
    """

    # --------------------------------------------------------------------- #
    # Lifecycle
    # --------------------------------------------------------------------- #
    def __init__(self, processor_id: str, config_dir: Path) -> None:
        self.processor_id = processor_id
        self.config_dir = config_dir
        self.config_file = config_dir / f"{processor_id}_config.json"

        # Concurrency: all shared state guarded by this lock
        self._lock = threading.RLock()

        # Bounded history of recent inputs/results (data-only dicts)
        self._sweep_history: list[dict[str, Any]] = []
        self._max_history = 1

        # Current configuration snapshot
        self._config: dict[str, Any] = {}

        self._load_config()
        logger.debug(
            "Processor initialized",
            processor_id=self.processor_id,
            config_file=str(self.config_file),
        )

    # --------------------------------------------------------------------- #
    # History management
    # --------------------------------------------------------------------- #
    @property
    def max_history(self) -> int:
        """Maximum number of history entries retained in memory."""
        return self._max_history

    @max_history.setter
    def max_history(self, value: int) -> None:
        """Change max history size (min 1) and trim existing history if needed."""
        with self._lock:
            new_size = max(1, int(value))
            if new_size != self._max_history:
                self._max_history = new_size
                self._trim_history()
                logger.debug(
                    "Max history updated",
                    max_history=new_size,
                    current=len(self._sweep_history),
                )

    def clear_history(self) -> None:
        """Drop all stored history entries."""
        with self._lock:
            self._sweep_history.clear()
            logger.debug("History cleared")

    def append_history(self, history_data: list[dict[str, Any]]) -> ProcessedResult | None:
        """
        Append sweep history from external source (e.g., loaded file) to existing history.

        Parameters
        ----------
        history_data:
            List of history entries in the format exported by export_history_data.

        Returns
        -------
        ProcessedResult | None
            Result after appending history, or None if history is empty.

        Notes
        -----
        - Only history is appended, processor config is NOT updated.
        - If total history exceeds max_history limit, oldest entries are dropped.
        """
        if not history_data:
            logger.warning("Append history called with empty data")
            return None

        from vna_system.core.acquisition.sweep_buffer import SweepData
        from vna_system.core.settings.preset_manager import VNAMode

        with self._lock:
            initial_count = len(self._sweep_history)

            # Convert imported data to internal format (same as import_history_data)
            for idx, entry in enumerate(history_data):
                sweep_points = entry.get("sweep_points", [])
                calibrated_points = entry.get("calibrated_points", [])
                reference_points = entry.get("reference_points", [])
                raw_reference_points = entry.get("raw_reference_points", [])

                # Reconstruct SweepData objects
                sweep_data = SweepData(
                    sweep_number=initial_count + idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=sweep_points,
                    total_points=len(sweep_points)
                ) if sweep_points else None

                calibrated_data = SweepData(
                    sweep_number=initial_count + idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=calibrated_points,
                    total_points=len(calibrated_points)
                ) if calibrated_points else None

                reference_data = SweepData(
                    sweep_number=initial_count + idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=reference_points,
                    total_points=len(reference_points)
                ) if reference_points else None

                raw_reference_data = SweepData(
                    sweep_number=initial_count + idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=raw_reference_points,
                    total_points=len(raw_reference_points)
                ) if raw_reference_points else None

                # Reconstruct calibration standards
                calibration_standards = None
                cal_standards_data = entry.get("calibration_standards")
                if cal_standards_data:
                    calibration_standards = {}
                    for std_name, std_data in cal_standards_data.items():
                        std_points = std_data.get("points", [])
                        if std_points:
                            calibration_standards[std_name] = SweepData(
                                sweep_number=std_data.get("sweep_number", initial_count + idx),
                                timestamp=std_data.get("timestamp", entry.get("timestamp", 0.0)),
                                points=std_points,
                                total_points=len(std_points)
                            )

                # Reconstruct reference info
                from vna_system.core.settings.reference_manager import ReferenceInfo
                reference_info = None
                ref_info_data = entry.get("reference_info")
                if ref_info_data and ref_info_data.get("name"):
                    # Only create ReferenceInfo if we have required fields
                    try:
                        reference_info = ReferenceInfo(
                            name=ref_info_data.get("name", ""),
                            timestamp=datetime.fromtimestamp(ref_info_data.get("timestamp", 0.0)) if ref_info_data.get("timestamp") else datetime.now(),
                            preset_filename=ref_info_data.get("preset_filename", ""),
                            description=ref_info_data.get("description", ""),
                            metadata=ref_info_data.get("metadata")
                        )
                    except Exception:
                        # If ReferenceInfo creation fails, skip it
                        reference_info = None

                # Reconstruct VNA config
                vna_config = entry.get("vna_config", {})

                # Append to history
                self._sweep_history.append({
                    "sweep_data": sweep_data,
                    "calibrated_data": calibrated_data,
                    "vna_config": vna_config,
                    "reference_data": reference_data,
                    "reference_info": reference_info,
                    "raw_reference_data": raw_reference_data,
                    "calibration_standards": calibration_standards,
                    "timestamp": entry.get("timestamp", datetime.now().timestamp()),
                })

            self._trim_history()
            final_count = len(self._sweep_history)

            logger.info(
                "History appended",
                processor_id=self.processor_id,
                added=len(history_data),
                initial_count=initial_count,
                final_count=final_count,
                dropped=initial_count + len(history_data) - final_count
            )

        return self.recalculate()

    def _trim_history(self) -> None:
        """Internal: keep only the newest `_max_history` items."""
        if len(self._sweep_history) > self._max_history:
            dropped = len(self._sweep_history) - self._max_history
            self._sweep_history = self._sweep_history[-self._max_history:]
            logger.debug("History trimmed", dropped=dropped, kept=self._max_history)

    # --------------------------------------------------------------------- #
    # Config I/O and updates
    # --------------------------------------------------------------------- #
    def _load_config(self) -> None:
        """
        Load the JSON config from disk; on failure or first run, use defaults.

        Strategy
        --------
        - Read file if present; ensure the root is a dict.
        - Shallow-merge with defaults (unknown keys are preserved).
        - Validate using UI schema (`_validate_config`).
        - On any error, fall back to defaults and save them.
        """
        defaults = self._get_default_config()
        if self.config_file.exists():
            try:
                cfg = json.loads(self.config_file.read_text(encoding="utf-8"))
                if not isinstance(cfg, dict):
                    raise ValueError("Config root must be an object")
                merged = {**defaults, **cfg}
                self._config = merged
                self._validate_config()
                logger.debug("Config loaded", file=str(self.config_file))
                return
            except Exception as exc:  # noqa: BLE001
                logger.warning(
                    "Config load failed; using defaults",
                    error=repr(exc),
                    file=str(self.config_file),
                )

        self._config = defaults
        self.save_config()

    def save_config(self) -> None:
        """
        Save current config to disk atomically.

        Implementation detail
        ---------------------
        Write to a temporary sidecar file and then replace the target to avoid
        partial writes in case of crashes.
        """
        self.config_dir.mkdir(parents=True, exist_ok=True)
        tmp = self.config_file.with_suffix(".json.tmp")
        payload = json.dumps(self._config, indent=2, ensure_ascii=False)
        tmp.write_text(payload, encoding="utf-8")
        tmp.replace(self.config_file)
        logger.debug("Config saved", file=str(self.config_file))

    def update_config(self, updates: dict[str, Any]) -> None:
        """
        Update config with user-provided values.

        - Performs type conversion based on current schema (`_convert_config_types`).
        - Validates against UI schema; on failure rolls back to the previous state.
        - Saves the resulting config when validation passes.
        """
        with self._lock:
            before = self._config.copy()
            converted = self._convert_config_types(updates)
            self._config.update(converted)
            try:
                self._validate_config()
                self.save_config()
                logger.info("Config updated", updates=converted)
            except Exception as exc:  # noqa: BLE001
                self._config = before
                logger.error("Invalid configuration update; rolled back", error=repr(exc))
                raise ValueError(f"Invalid configuration: {exc}") from exc

    def get_config(self) -> dict[str, Any]:
        """Return a shallow copy of the current config snapshot."""
        with self._lock:
            return self._config.copy()

    def _convert_config_types(self, updates: dict[str, Any]) -> dict[str, Any]:
        """
        Convert string inputs into the target types inferred from the current config.

        Rules
        -----
        • Booleans: accept case-insensitive {"true","1","on","yes"}.
        • Int/float: accept numeric strings; for ints, tolerate "50.0" → 50.
        • Unknown keys: kept as-is (subclass validators may use them).
        """
        out: dict[str, Any] = {}
        for key, value in updates.items():
            if key not in self._config:
                out[key] = value
                continue

            current = self._config[key]

            # bool
            if isinstance(current, bool) and isinstance(value, str):
                out[key] = value.strip().lower() in {"true", "1", "on", "yes"}
                continue

            # numbers
            if isinstance(current, int) and isinstance(value, str):
                try:
                    out[key] = int(float(value))
                    continue
                except ValueError:
                    pass
            if isinstance(current, float) and isinstance(value, str):
                try:
                    out[key] = float(value)
                    continue
                except ValueError:
                    pass

            # fallback: unchanged
            out[key] = value
        return out

    # --------------------------------------------------------------------- #
    # Data path: accept new sweep, recompute, produce result
    # --------------------------------------------------------------------- #
    def add_sweep_data(self, sweep_data: Any, 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):
        """
        Add the latest sweep to the in-memory history and trigger recalculation.

        Parameters
        ----------
        sweep_data:
            Raw/parsed sweep data as produced by acquisition.
        calibrated_data:
            Data post-calibration (structure is processor-specific).
        vna_config:
            Snapshot of VNA settings (dataclass or pydantic model supported).
        reference_data:
            Calibrated open air reference sweep data for background subtraction/normalization.
        reference_info:
            ReferenceInfo object with metadata (name, description, etc.) about the reference.
        raw_reference_data:
            Raw (uncalibrated) reference sweep data for export purposes.
        calibration_standards:
            Dictionary of calibration standard sweep data (e.g., {"open": SweepData, "short": SweepData, ...}).

        Returns
        -------
        ProcessedResult | None
            The newly computed result or None when history is empty.
        """
        with self._lock:
            self._sweep_history.append(
                {
                    "sweep_data": sweep_data,
                    "calibrated_data": calibrated_data,
                    "vna_config": self._snapshot_vna_config(vna_config),
                    "reference_data": reference_data,
                    "reference_info": reference_info,
                    "raw_reference_data": raw_reference_data,
                    "calibration_standards": calibration_standards,
                    "timestamp": datetime.now().timestamp(),
                }
            )
            self._trim_history()

        return self.recalculate()

    def recalculate(self) -> ProcessedResult | None:
        """
        Recompute the processor output using the most recent history entry.

        Notes
        -----
        Subclasses must ensure `process_sweep` and `generate_plotly_config`
        are pure (no global side effects) and thread-safe w.r.t. the provided inputs.
        """
        with self._lock:
            if not self._sweep_history:
                logger.debug("Recalculate skipped; history empty")
                return None

            latest = self._sweep_history[-1]
            return self._process_data(
                latest["sweep_data"],
                latest["calibrated_data"],
                latest["vna_config"],
            )

    def _process_data(self, sweep_data: Any, calibrated_data: Any, vna_config: dict[str, Any]) -> ProcessedResult | None:
        """
        Internal: compute processed data, build a Plotly config, and wrap into `ProcessedResult`.

        Returns None if processing resulted in an error (indicated by {"error": ...} dict).
        """
        processed = self.process_sweep(sweep_data, calibrated_data, vna_config)

        # Skip result creation if processing returned an error
        if isinstance(processed, dict) and "error" in processed:
            logger.debug("Processing returned error; skipping result",
                        processor_id=self.processor_id,
                        error=processed.get("error"))
            return None

        plotly_conf = self.generate_plotly_config(processed, vna_config)
        ui_params = self.get_ui_parameters()

        result = ProcessedResult(
            processor_id=self.processor_id,
            timestamp=datetime.now().timestamp(),
            data=processed,
            plotly_config=plotly_conf,
            ui_parameters=ui_params,
            metadata=self._get_metadata(),
        )
        logger.debug("Processed result produced", processor_id=self.processor_id)
        return result

    # --------------------------------------------------------------------- #
    # Abstracts to implement in concrete processors
    # --------------------------------------------------------------------- #
    def process_sweep(self, sweep_data: Any, calibrated_data: Any, vna_config: dict[str, Any]) -> dict[str, Any]:
        """Compute the processor’s domain result from input sweep data."""
        raise NotImplementedError

    def generate_plotly_config(self, processed_data: dict[str, Any], vna_config: dict[str, Any]) -> dict[str, Any]:
        """Create a ready-to-render Plotly configuration from processed data."""
        raise NotImplementedError

    def get_ui_parameters(self) -> list[UIParameter]:
        """Return the UI schema (used both for UI rendering and config validation)."""
        raise NotImplementedError

    def _get_default_config(self) -> dict[str, Any]:
        """Provide default config values; keys should match `UIParameter.name`."""
        raise NotImplementedError

    # --------------------------------------------------------------------- #
    # Validation using UI schema
    # --------------------------------------------------------------------- #
    def _validate_config(self) -> None:
        """
        Validate `self._config` using the schema in `get_ui_parameters()`.

        Validation rules
        ----------------
        slider:
            - dtype: "int"|"float" (default "float")
            - min/max: inclusive numeric bounds
            - step: if present, enforce alignment from `min` (or 0/0.0)
        toggle:
            - must be bool
        select:
            - options must be {"choices": [ ... ]}
            - value must be one of `choices`
        input:
            - options must be {"type": "int"|"float", "min"?, "max"?}
            - value must be numeric (no strings)
        button:
            - options must be {"action": "<text>"}
            - value is ignored by validation

        Unknown control types emit a warning but do not block execution.
        """
        params = {p.name: p for p in self.get_ui_parameters()}
        for name, schema in params.items():
            if name not in self._config:
                # Seed missing keys from the UI default to maintain a consistent shape.
                self._config[name] = schema.value
                logger.debug("Config key missing; seeded from UI default", key=name, value=schema.value)

            value = self._config[name]
            ptype = (schema.type or "").lower()
            opts = schema.options or {}

            try:
                if ptype == "slider":
                    self._validate_slider(name, value, opts)
                elif ptype == "toggle":
                    self._validate_toggle(name, value)
                elif ptype == "select":
                    self._validate_select_strict(name, value, opts)
                elif ptype == "input":
                    self._validate_input_numeric(name, value, opts)
                elif ptype == "button":
                    self._validate_button_opts(name, opts)
                else:
                    logger.warning("Unknown UI control type; skipping validation", key=name, type=ptype)
            except ValueError as exc:
                # Prefix the processor id for easier debugging in multi-processor UIs.
                raise ValueError(f"[{self.processor_id}] Invalid `{name}`: {exc}") from exc

    # ---- Validators (per control type) ----------------------------------- #
    def _validate_slider(self, key: str, value: Any, opts: dict[str, Any]) -> None:
        """
        Validate a slider value; normalize to int/float based on `dtype`.
        """
        dtype = str(opts.get("dtype", "float")).lower()
        min_v = opts.get("min")
        max_v = opts.get("max")
        step = opts.get("step")

        # Type / casting
        if dtype == "int":
            if not isinstance(value, int):
                if isinstance(value, float) and value.is_integer():
                    value = int(value)
                else:
                    raise ValueError(f"expected int, got {type(value).__name__}")
        else:
            if not isinstance(value, (int, float)):
                raise ValueError(f"expected number, got {type(value).__name__}")
            value = float(value)

        # Bounds
        if min_v is not None and value < min_v:
            raise ValueError(f"{value} < min {min_v}")
        if max_v is not None and value > max_v:
            raise ValueError(f"{value} > max {max_v}")

        # Step alignment
        if step is not None:
            base = min_v if min_v is not None else (0 if dtype == "int" else 0.0)
            if dtype == "int":
                if (value - int(base)) % int(step) != 0:
                    raise ValueError(f"value {value} not aligned to step {step} from base {base}")
            else:
                steps = (value - float(base)) / float(step)
                if not math.isclose(steps, round(steps), rel_tol=1e-9, abs_tol=1e-9):
                    raise ValueError(f"value {value} not aligned to step {step} from base {base}")

        # Normalize the stored value
        self._config[key] = int(value) if dtype == "int" else float(value)

    def _validate_toggle(self, key: str, value: Any) -> None:
        """Validate a boolean toggle."""
        if not isinstance(value, bool):
            raise ValueError(f"expected bool, got {type(value).__name__}")

    def _validate_select_strict(self, key: str, value: Any, opts: dict[str, Any]) -> None:
        """
        Validate a 'select' value against a required list of choices.
        """
        if not isinstance(opts, dict) or "choices" not in opts or not isinstance(opts["choices"], list):
            raise ValueError("select.options must be a dict with key 'choices' as a list")
        choices = opts["choices"]
        if value not in choices:
            raise ValueError(f"value {value!r} not in choices {choices!r}")

    def _validate_input_numeric(self, key: str, value: Any, opts: dict[str, Any]) -> None:
        """
        Validate a numeric input.

        options:
            - type: "int" | "float" (required)
            - min/max: optional numeric bounds
        """
        t = str(opts.get("type", "")).lower()
        if t not in {"int", "float"}:
            raise ValueError("input.options.type must be 'int' or 'float'")

        if t == "int":
            # bool is a subclass of int in Python; explicitly reject it
            if isinstance(value, bool) or not isinstance(value, (int, float)):
                raise ValueError(f"expected int, got {type(value).__name__}")
            if isinstance(value, float) and not value.is_integer():
                raise ValueError(f"expected int, got non-integer float {value}")
            iv = int(value)
            self._numeric_bounds_check(key, iv, opts)
            self._config[key] = iv
        else:
            if isinstance(value, bool) or not isinstance(value, (int, float)):
                raise ValueError(f"expected float, got {type(value).__name__}")
            fv = float(value)
            self._numeric_bounds_check(key, fv, opts)
            self._config[key] = fv

    def _validate_button_opts(self, key: str, opts: dict[str, Any]) -> None:
        """
        Validate a button descriptor; buttons are imperative actions, not state.
        """
        if not isinstance(opts, dict) or "action" not in opts or not isinstance(opts["action"], str):
            raise ValueError("button.options must be a dict with key 'action' (str)")

    @staticmethod
    def _numeric_bounds_check(key: str, value: float, opts: dict[str, Any]) -> None:
        """Shared numeric bounds check for input/slider."""
        min_v = opts.get("min")
        max_v = opts.get("max")
        if min_v is not None and value < min_v:
            raise ValueError(f"{key} {value} < min {min_v}")
        if max_v is not None and value > max_v:
            raise ValueError(f"{key} {value} > max {max_v}")

    # --------------------------------------------------------------------- #
    # History Export/Import
    # --------------------------------------------------------------------- #
    @staticmethod
    def _snapshot_vna_config(vna_config: ConfigPreset | dict[str, Any] | None) -> dict[str, Any]:
        """Create a JSON-friendly copy of VNA preset data."""
        if vna_config is None:
            return {}

        from enum import Enum  # Local import to avoid module-level dependency

        if isinstance(vna_config, dict):
            source_items = vna_config.items()
        elif is_dataclass(vna_config):
            source_items = asdict(vna_config).items()
        else:
            try:
                source_items = vars(vna_config).items()
            except TypeError:
                return {}

        sanitized: dict[str, Any] = {}
        for key, value in source_items:
            if isinstance(value, Enum):
                sanitized[key] = value.value
            elif hasattr(value, "item") and callable(value.item):
                try:
                    sanitized[key] = value.item()
                    continue
                except (TypeError, ValueError):
                    pass
            elif hasattr(value, "tolist") and callable(value.tolist):
                sanitized[key] = value.tolist()
                continue

            sanitized[key] = value
        return sanitized

    def export_history_data(self) -> list[dict[str, Any]]:
        """
        Export sweep history in JSON-serializable format.

        Returns
        -------
        list[dict]
            Serializable history records with sweep data converted to points.
        """
        with self._lock:
            exported = []
            for entry in self._sweep_history:
                sweep_data = entry["sweep_data"]
                calibrated_data = entry["calibrated_data"]
                reference_data = entry.get("reference_data")
                raw_reference_data = entry.get("raw_reference_data")
                calibration_standards = entry.get("calibration_standards")
                reference_info = entry.get("reference_info")

                # Export calibration standards if present
                cal_standards_export = None
                if calibration_standards:
                    cal_standards_export = {}
                    for std_name, std_data in calibration_standards.items():
                        cal_standards_export[std_name] = {
                            "points": self._points_to_list(getattr(std_data, "points", [])),
                            "sweep_number": getattr(std_data, "sweep_number", None),
                            "timestamp": getattr(std_data, "timestamp", None),
                        }

                # Export reference info with all fields
                ref_info_export = None
                if reference_info:
                    ref_info_export = {
                        "name": getattr(reference_info, "name", None),
                        "timestamp": getattr(reference_info, "timestamp", datetime.now()).timestamp() if hasattr(reference_info, "timestamp") else None,
                        "preset_filename": getattr(reference_info, "preset_filename", None),
                        "description": getattr(reference_info, "description", None),
                        "metadata": getattr(reference_info, "metadata", None),
                    }

                # Get timestamp safely
                entry_timestamp = entry.get("timestamp")
                timestamp_export = float(entry_timestamp) if entry_timestamp is not None else None

                exported.append({
                    "timestamp": timestamp_export,
                    "sweep_points": self._points_to_list(getattr(sweep_data, "points", [])),
                    "calibrated_points": self._points_to_list(getattr(calibrated_data, "points", [])),
                    "reference_points": self._points_to_list(getattr(reference_data, "points", [])),
                    "raw_reference_points": self._points_to_list(getattr(raw_reference_data, "points", [])),
                    "calibration_standards": cal_standards_export,
                    "reference_info": ref_info_export,
                    "vna_config": self._snapshot_vna_config(entry.get("vna_config")),
                })

            return exported

    def import_history_data(self, history_data: list[dict[str, Any]]) -> None:
        """
        Import sweep history from JSON data.

        Parameters
        ----------
        history_data : list[dict]
            History records in the format exported by export_history_data.
        """
        from vna_system.core.acquisition.sweep_buffer import SweepData
        from vna_system.core.settings.preset_manager import VNAMode

        with self._lock:
            self._sweep_history.clear()

            for idx, entry in enumerate(history_data):
                sweep_points = entry.get("sweep_points", [])
                calibrated_points = entry.get("calibrated_points", [])
                reference_points = entry.get("reference_points", [])
                raw_reference_points = entry.get("raw_reference_points", [])

                # Reconstruct SweepData objects
                # Use sequential index as sweep_number since it's not stored
                sweep_data = SweepData(
                    sweep_number=idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=sweep_points,
                    total_points=len(sweep_points)
                ) if sweep_points else None

                calibrated_data = SweepData(
                    sweep_number=idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=calibrated_points,
                    total_points=len(calibrated_points)
                ) if calibrated_points else None

                reference_data = SweepData(
                    sweep_number=idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=reference_points,
                    total_points=len(reference_points)
                ) if reference_points else None

                raw_reference_data = SweepData(
                    sweep_number=idx,
                    timestamp=entry.get("timestamp", 0.0),
                    points=raw_reference_points,
                    total_points=len(raw_reference_points)
                ) if raw_reference_points else None

                # Reconstruct calibration standards
                calibration_standards = None
                cal_standards_data = entry.get("calibration_standards")
                if cal_standards_data:
                    calibration_standards = {}
                    for std_name, std_data in cal_standards_data.items():
                        std_points = std_data.get("points", [])
                        if std_points:
                            calibration_standards[std_name] = SweepData(
                                sweep_number=std_data.get("sweep_number", idx),
                                timestamp=std_data.get("timestamp", entry.get("timestamp", 0.0)),
                                points=std_points,
                                total_points=len(std_points)
                            )

                # Reconstruct reference info
                from vna_system.core.settings.reference_manager import ReferenceInfo
                reference_info = None
                ref_info_data = entry.get("reference_info")
                if ref_info_data and ref_info_data.get("name"):
                    # Only create ReferenceInfo if we have required fields
                    try:
                        reference_info = ReferenceInfo(
                            name=ref_info_data.get("name", ""),
                            timestamp=datetime.fromtimestamp(ref_info_data.get("timestamp", 0.0)) if ref_info_data.get("timestamp") else datetime.now(),
                            preset_filename=ref_info_data.get("preset_filename", ""),
                            description=ref_info_data.get("description", ""),
                            metadata=ref_info_data.get("metadata")
                        )
                    except Exception:
                        # If ReferenceInfo creation fails, skip it
                        reference_info = None

                # Restore VNAMode enum from string if needed
                vna_config = entry.get("vna_config", {})
                if isinstance(vna_config.get("mode"), str):
                    try:
                        vna_config["mode"] = VNAMode(vna_config["mode"])
                    except (ValueError, KeyError):
                        pass  # Keep as string if conversion fails

                self._sweep_history.append({
                    "sweep_data": sweep_data,
                    "calibrated_data": calibrated_data,
                    "reference_data": reference_data,
                    "raw_reference_data": raw_reference_data,
                    "reference_info": reference_info,
                    "calibration_standards": calibration_standards,
                    "vna_config": vna_config,
                    "timestamp": entry.get("timestamp"),
                })

            logger.info("History imported", processor_id=self.processor_id, records=len(history_data))

    @staticmethod
    def _points_to_list(points: Any) -> list[tuple[float, float]]:
        """Convert sweep points into plain Python floats."""
        result: list[tuple[float, float]] = []
        if not points:
            return result

        for point in points:
            if isinstance(point, dict):
                real = point.get("real")
                imag = point.get("imag")
                if real is None:
                    real = point.get("r")
                if imag is None:
                    imag = point.get("i")
            elif isinstance(point, (list, tuple)) and len(point) >= 2:
                real, imag = point[0], point[1]
            else:
                continue

            try:
                result.append((float(real), float(imag)))
            except (TypeError, ValueError):
                continue

        return result

    # --------------------------------------------------------------------- #
    # Utilities
    # --------------------------------------------------------------------- #
    def _get_metadata(self) -> dict[str, Any]:
        """
        Return diagnostic metadata bundled with each `ProcessedResult`.

        Note: This is sent with every websocket broadcast, so it should be lightweight.
        For full state including sweep_history, use get_full_state().
        """
        with self._lock:
            return {
                "processor_id": self.processor_id,
                "config": self._config.copy(),
                "history_count": len(self._sweep_history),
                "max_history": self._max_history,
            }

    def get_full_state(self) -> dict[str, Any]:
        """
        Return complete processor state including sweep history and active reference.

        This should be called explicitly when needed (e.g., for export/download),
        not on every broadcast.
        """
        with self._lock:
            # Get active reference info from the latest history entry
            active_reference = None
            if self._sweep_history:
                latest_entry = self._sweep_history[-1]
                reference_info = latest_entry.get("reference_info")
                reference_data = latest_entry.get("reference_data")

                if reference_info is not None:
                    # Export full ReferenceInfo metadata using its to_dict() method
                    active_reference = reference_info.to_dict()
                    # Also include the sweep data points
                    if reference_data is not None:
                        active_reference["points"] = self._points_to_list(getattr(reference_data, "points", []))
                        active_reference["total_points"] = getattr(reference_data, "total_points", 0)
                        active_reference["sweep_timestamp"] = getattr(reference_data, "timestamp", None)

            return {
                "processor_id": self.processor_id,
                "config": self._config.copy(),
                "history_count": len(self._sweep_history),
                "max_history": self._max_history,
                "sweep_history": self.export_history_data(),
                "active_reference": active_reference,
            }
