"""Project model for managing multiple schematics and settings."""

import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from uuid import UUID

from pulsimgui.models.circuit import Circuit
from pulsimgui.models.component import (
    get_control_sample_time,
    set_control_sample_time,
    supports_control_sample_time,
)
from pulsimgui.models.subcircuit import SubcircuitDefinition


@dataclass
class SimulationSettings:
    """Settings for transient simulation."""

    tstop: float = 1e-3
    dt: float = 1e-6
    max_step: float = 1e-6
    tstart: float = 0.0
    abstol: float = 1e-12
    reltol: float = 1e-3
    solver: str = "auto"
    step_mode: str = "fixed"
    output_points: int = 10000
    enable_events: bool = True
    max_step_retries: int = 8
    enable_losses: bool = True
    max_iterations: int = 50
    enable_voltage_limiting: bool = False
    max_voltage_step: float = 5.0
    dc_strategy: str = "auto"
    gmin_initial: float = 1e-3
    gmin_final: float = 1e-12
    dc_source_steps: int = 10
    transient_robust_mode: bool = True
    transient_auto_regularize: bool = True
    thermal_ambient: float = 25.0
    thermal_include_switching_losses: bool = True
    thermal_include_conduction_losses: bool = True
    thermal_network: str = "foster"
    thermal_policy: str = "loss_with_temperature_scaling"
    thermal_default_rth: float = 1.0
    thermal_default_cth: float = 0.1
    formulation_mode: str = "projected_wrapper"
    direct_formulation_fallback: bool = True
    control_mode: str = "auto"
    control_sample_time: float = 0.0
    ac_f_start: float = 1.0
    ac_f_stop: float = 1e6
    ac_points_per_decade: int = 10
    ac_anchor_mode: str = "auto"
    ac_sweep_scale: str = "decade"
    ac_injection_node: str = ""
    ac_measurement_node: str = ""
    averaged_options: dict | None = None

    def to_dict(self) -> dict:
        """Serialize to dictionary."""
        return {
            "tstop": self.tstop,
            "dt": self.dt,
            "max_step": self.max_step,
            "tstart": self.tstart,
            "abstol": self.abstol,
            "reltol": self.reltol,
            "solver": self.solver,
            "step_mode": self.step_mode,
            "output_points": self.output_points,
            "enable_events": self.enable_events,
            "max_step_retries": self.max_step_retries,
            "enable_losses": self.enable_losses,
            "max_iterations": self.max_iterations,
            "enable_voltage_limiting": self.enable_voltage_limiting,
            "max_voltage_step": self.max_voltage_step,
            "dc_strategy": self.dc_strategy,
            "gmin_initial": self.gmin_initial,
            "gmin_final": self.gmin_final,
            "dc_source_steps": self.dc_source_steps,
            "transient_robust_mode": self.transient_robust_mode,
            "transient_auto_regularize": self.transient_auto_regularize,
            "thermal_ambient": self.thermal_ambient,
            "thermal_include_switching_losses": self.thermal_include_switching_losses,
            "thermal_include_conduction_losses": self.thermal_include_conduction_losses,
            "thermal_network": self.thermal_network,
            "thermal_policy": self.thermal_policy,
            "thermal_default_rth": self.thermal_default_rth,
            "thermal_default_cth": self.thermal_default_cth,
            "formulation_mode": self.formulation_mode,
            "direct_formulation_fallback": self.direct_formulation_fallback,
            "control_mode": self.control_mode,
            "control_sample_time": self.control_sample_time,
            "ac_f_start": self.ac_f_start,
            "ac_f_stop": self.ac_f_stop,
            "ac_points_per_decade": self.ac_points_per_decade,
            "ac_anchor_mode": self.ac_anchor_mode,
            "ac_sweep_scale": self.ac_sweep_scale,
            "ac_injection_node": self.ac_injection_node,
            "ac_measurement_node": self.ac_measurement_node,
            "averaged_options": dict(self.averaged_options)
            if isinstance(self.averaged_options, dict)
            else None,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "SimulationSettings":
        """Deserialize from dictionary."""
        thermal_network = str(data.get("thermal_network", "foster") or "foster").strip().lower()
        if thermal_network not in {"foster", "cauer"}:
            thermal_network = "foster"
        thermal_policy = str(
            data.get("thermal_policy", "loss_with_temperature_scaling")
            or "loss_with_temperature_scaling"
        ).strip().lower()
        thermal_policy_aliases = {
            "losswithtemperaturescaling": "loss_with_temperature_scaling",
            "temperature_scaling": "loss_with_temperature_scaling",
            "lossonly": "loss_only",
        }
        thermal_policy = thermal_policy_aliases.get(thermal_policy, thermal_policy)
        if thermal_policy not in {"loss_only", "loss_with_temperature_scaling"}:
            thermal_policy = "loss_with_temperature_scaling"
        formulation_mode = str(
            data.get("formulation_mode", "projected_wrapper") or "projected_wrapper"
        ).strip().lower()
        if formulation_mode in {"projected", "projectedwrapper", "native"}:
            formulation_mode = "projected_wrapper"
        elif formulation_mode in {"directdae", "dae"}:
            formulation_mode = "direct"
        if formulation_mode not in {"projected_wrapper", "direct"}:
            formulation_mode = "projected_wrapper"
        control_mode = str(data.get("control_mode", "auto") or "auto").strip().lower()
        control_mode_aliases = {
            "sampled": "discrete",
            "sample": "discrete",
            "continuous_time": "continuous",
        }
        control_mode = control_mode_aliases.get(control_mode, control_mode)
        if control_mode not in {"auto", "continuous", "discrete"}:
            control_mode = "auto"
        ac_anchor_mode = str(data.get("ac_anchor_mode", "auto") or "auto").strip().lower()
        if ac_anchor_mode not in {"auto", "dc", "periodic", "averaged"}:
            ac_anchor_mode = "auto"
        ac_sweep_scale = str(data.get("ac_sweep_scale", "decade") or "decade").strip().lower()
        if ac_sweep_scale in {"logarithmic"}:
            ac_sweep_scale = "log"
        if ac_sweep_scale not in {"decade", "log", "linear"}:
            ac_sweep_scale = "decade"
        ac_f_start = max(1e-12, float(data.get("ac_f_start", 1.0)))
        ac_f_stop = max(
            ac_f_start * (1.0 + 1e-12),
            float(data.get("ac_f_stop", 1e6)),
        )
        raw_averaged_options = data.get("averaged_options")
        averaged_options = (
            dict(raw_averaged_options) if isinstance(raw_averaged_options, dict) else None
        )
        return cls(
            tstop=data.get("tstop", 1e-3),
            dt=data.get("dt", 1e-6),
            max_step=float(data.get("max_step", data.get("dt", 1e-6))),
            tstart=data.get("tstart", 0.0),
            abstol=data.get("abstol", 1e-12),
            reltol=data.get("reltol", 1e-3),
            solver=str(data.get("solver", "auto")),
            step_mode=str(data.get("step_mode", "fixed")),
            output_points=int(data.get("output_points", 10000)),
            enable_events=bool(data.get("enable_events", True)),
            max_step_retries=int(data.get("max_step_retries", 8)),
            enable_losses=bool(data.get("enable_losses", True)),
            max_iterations=data.get("max_iterations", 50),
            enable_voltage_limiting=bool(data.get("enable_voltage_limiting", False)),
            max_voltage_step=float(data.get("max_voltage_step", 5.0)),
            dc_strategy=str(data.get("dc_strategy", "auto")),
            gmin_initial=float(data.get("gmin_initial", 1e-3)),
            gmin_final=float(data.get("gmin_final", 1e-12)),
            dc_source_steps=int(data.get("dc_source_steps", 10)),
            transient_robust_mode=bool(data.get("transient_robust_mode", True)),
            transient_auto_regularize=bool(data.get("transient_auto_regularize", True)),
            thermal_ambient=float(data.get("thermal_ambient", 25.0)),
            thermal_include_switching_losses=bool(
                data.get("thermal_include_switching_losses", True)
            ),
            thermal_include_conduction_losses=bool(
                data.get("thermal_include_conduction_losses", True)
            ),
            thermal_network=thermal_network,
            thermal_policy=thermal_policy,
            thermal_default_rth=max(0.0, float(data.get("thermal_default_rth", 1.0))),
            thermal_default_cth=max(0.0, float(data.get("thermal_default_cth", 0.1))),
            formulation_mode=formulation_mode,
            direct_formulation_fallback=bool(
                data.get("direct_formulation_fallback", True)
            ),
            control_mode=control_mode,
            control_sample_time=max(0.0, float(data.get("control_sample_time", 0.0))),
            ac_f_start=ac_f_start,
            ac_f_stop=ac_f_stop,
            ac_points_per_decade=max(1, int(data.get("ac_points_per_decade", 10))),
            ac_anchor_mode=ac_anchor_mode,
            ac_sweep_scale=ac_sweep_scale,
            ac_injection_node=str(data.get("ac_injection_node", "") or ""),
            ac_measurement_node=str(data.get("ac_measurement_node", "") or ""),
            averaged_options=averaged_options,
        )


def _legacy_project_control_sample_time(settings: SimulationSettings) -> float | None:
    """Extract legacy global control sample time when available."""
    mode = str(settings.control_mode or "auto").strip().lower()
    sample_time = max(0.0, float(settings.control_sample_time))
    if mode == "discrete":
        return sample_time if sample_time > 0.0 else max(1e-12, float(settings.dt))
    if sample_time > 0.0:
        return sample_time
    return None


def _extract_explicit_component_sample_time_ids(data: dict) -> set[str]:
    """Collect component IDs that already define sample-time parameters in payload."""
    explicit_ids: set[str] = set()
    circuits_data = data.get("circuits", {}) if isinstance(data, dict) else {}
    if not isinstance(circuits_data, dict):
        return explicit_ids

    for circuit_payload in circuits_data.values():
        if not isinstance(circuit_payload, dict):
            continue
        components = circuit_payload.get("components", [])
        if not isinstance(components, list):
            continue
        for component_payload in components:
            if not isinstance(component_payload, dict):
                continue
            params = component_payload.get("parameters")
            if not isinstance(params, dict):
                continue
            if "sample_time" not in params and "sample_period" not in params:
                continue
            component_id = str(component_payload.get("id") or "").strip()
            if component_id:
                explicit_ids.add(component_id)

    return explicit_ids


def _migrate_legacy_global_control_schedule(
    *,
    circuits: dict[str, Circuit],
    settings: SimulationSettings,
    explicit_sample_time_ids: set[str],
) -> None:
    """Backfill per-block Ts from legacy global control settings when needed."""
    legacy_sample_time = _legacy_project_control_sample_time(settings)
    if legacy_sample_time is None:
        return

    for circuit in circuits.values():
        for component in circuit.components.values():
            if not supports_control_sample_time(component.type):
                continue
            component_id = str(component.id)
            if component_id in explicit_sample_time_ids:
                continue
            if get_control_sample_time(component.parameters, default=0.0) > 0.0:
                continue
            set_control_sample_time(component.parameters, legacy_sample_time)


@dataclass
class ScopeWindowState:
    """Persisted UI state for a per-scope window."""

    component_id: str
    is_open: bool = False
    geometry: list[int] | None = None  # [x, y, width, height]

    def to_dict(self) -> dict:
        """Serialize the window state."""
        return {
            "component_id": self.component_id,
            "is_open": self.is_open,
            "geometry": self.geometry,
        }

    @classmethod
    def from_dict(cls, data: dict, fallback_id: str | None = None) -> "ScopeWindowState":
        """Deserialize window state."""
        return cls(
            component_id=data.get("component_id") or fallback_id or "",
            is_open=data.get("is_open", False),
            geometry=data.get("geometry"),
        )


@dataclass
class Project:
    """A project containing one or more circuit schematics."""

    name: str = "Untitled Project"
    path: Path | None = None
    circuits: dict[str, Circuit] = field(default_factory=dict)
    active_circuit: str = "main"
    subcircuits: dict[UUID, SubcircuitDefinition] = field(default_factory=dict)
    simulation_settings: SimulationSettings = field(default_factory=SimulationSettings)
    created: datetime = field(default_factory=datetime.now)
    modified: datetime = field(default_factory=datetime.now)
    scope_windows: dict[str, ScopeWindowState] = field(default_factory=dict)
    _dirty: bool = field(default=False, repr=False)

    def __post_init__(self):
        """Ensure there's at least one circuit."""
        if not self.circuits:
            self.circuits["main"] = Circuit(name="main")

    @property
    def is_dirty(self) -> bool:
        """Check if project has unsaved changes."""
        return self._dirty

    def mark_dirty(self) -> None:
        """Mark project as having unsaved changes."""
        self._dirty = True
        self.modified = datetime.now()

    def mark_clean(self) -> None:
        """Mark project as saved."""
        self._dirty = False

    def get_active_circuit(self) -> Circuit:
        """Get the currently active circuit."""
        if self.active_circuit not in self.circuits:
            self.active_circuit = list(self.circuits.keys())[0]
        return self.circuits[self.active_circuit]

    def add_circuit(self, name: str) -> Circuit:
        """Add a new circuit to the project."""
        if name in self.circuits:
            raise ValueError(f"Circuit '{name}' already exists")
        circuit = Circuit(name=name)
        self.circuits[name] = circuit
        self.mark_dirty()
        return circuit

    def remove_circuit(self, name: str) -> None:
        """Remove a circuit from the project."""
        if name not in self.circuits:
            raise ValueError(f"Circuit '{name}' not found")
        if len(self.circuits) <= 1:
            raise ValueError("Cannot remove the last circuit")
        del self.circuits[name]
        if self.active_circuit == name:
            self.active_circuit = list(self.circuits.keys())[0]
        self.mark_dirty()

    def to_dict(self) -> dict:
        """Serialize project to dictionary."""
        return {
            "version": "1.0",
            "name": self.name,
            "created": self.created.isoformat(),
            "modified": self.modified.isoformat(),
            "active_circuit": self.active_circuit,
            "simulation_settings": self.simulation_settings.to_dict(),
            "circuits": {name: c.to_dict() for name, c in self.circuits.items()},
            "subcircuits": [definition.to_dict() for definition in self.subcircuits.values()],
            "scope_windows": {
                component_id: state.to_dict()
                for component_id, state in self.scope_windows.items()
            },
        }

    @classmethod
    def from_dict(cls, data: dict, path: Path | None = None) -> "Project":
        """Deserialize project from dictionary."""
        explicit_sample_time_ids = _extract_explicit_component_sample_time_ids(data)
        circuits = {}
        for name, circuit_data in data.get("circuits", {}).items():
            circuits[name] = Circuit.from_dict(circuit_data)
        simulation_settings_payload = data.get("simulation_settings", {})
        simulation_settings = SimulationSettings.from_dict(simulation_settings_payload)
        _migrate_legacy_global_control_schedule(
            circuits=circuits,
            settings=simulation_settings,
            explicit_sample_time_ids=explicit_sample_time_ids,
        )

        subcircuits: dict[UUID, SubcircuitDefinition] = {}
        for definition_data in data.get("subcircuits", []):
            definition = SubcircuitDefinition.from_dict(definition_data)
            subcircuits[definition.id] = definition

        scope_windows: dict[str, ScopeWindowState] = {}
        for component_id, state_data in data.get("scope_windows", {}).items():
            state = ScopeWindowState.from_dict(state_data, component_id)
            if state.component_id:
                scope_windows[state.component_id] = state

        return cls(
            name=data.get("name", "Untitled Project"),
            path=path,
            circuits=circuits,
            active_circuit=data.get("active_circuit", "main"),
            simulation_settings=simulation_settings,
            created=datetime.fromisoformat(data["created"]) if "created" in data else datetime.now(),
            modified=datetime.fromisoformat(data["modified"]) if "modified" in data else datetime.now(),
            subcircuits=subcircuits,
            scope_windows=scope_windows,
        )

    def save(self, path: Path | None = None) -> None:
        """Save project to file."""
        save_path = path or self.path
        if save_path is None:
            raise ValueError("No path specified for saving")

        save_path = Path(save_path)
        self.save_copy(save_path)

        self.path = save_path
        self.mark_clean()

    def save_copy(self, path: Path) -> None:
        """Write a project copy to disk without changing active project state."""
        copy_path = Path(path)
        copy_path.parent.mkdir(parents=True, exist_ok=True)

        with open(copy_path, "w", encoding="utf-8") as f:
            json.dump(self.to_dict(), f, indent=2)

    @classmethod
    def load(cls, path: Path) -> "Project":
        """Load project from file."""
        path = Path(path)
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
        project = cls.from_dict(data, path)
        project.mark_clean()
        return project

    # Subcircuit helpers

    def add_subcircuit(self, definition: SubcircuitDefinition) -> None:
        """Add a subcircuit definition to the project."""
        self.subcircuits[definition.id] = definition
        self.mark_dirty()

    def get_subcircuit(self, definition_id: UUID) -> SubcircuitDefinition | None:
        """Retrieve a subcircuit definition by ID."""
        return self.subcircuits.get(definition_id)

    def remove_subcircuit(self, definition_id: UUID) -> None:
        """Remove a subcircuit definition if present."""
        if definition_id in self.subcircuits:
            del self.subcircuits[definition_id]
            self.mark_dirty()

    def scope_state_for(self, component_id: str) -> ScopeWindowState:
        """Return (and create if needed) the window state for a scope component."""
        state = self.scope_windows.get(component_id)
        if state is None:
            state = ScopeWindowState(component_id=component_id)
            self.scope_windows[component_id] = state
        return state
