"""Simulation plan schema for PCB axial flux motor automated simulation.

Defines the minimal simulation_plan.json structure for Phase 1,
with validation, point generation, and JSON serialization.

All source is ASCII.
"""

from __future__ import annotations

import json
import math
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import Any

from .scan_engine import values_inclusive, generate_cartesian_points


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class ScanVariable:
    """A single scan variable definition."""
    name: str
    display_name: str = ""
    unit: str = ""
    start: float = 0.0
    stop: float = 0.0
    step: float = 0.0
    values: list[float] = field(default_factory=list)

    def __post_init__(self) -> None:
        if not self.display_name:
            self.display_name = self.name
        # If explicit values not given, generate from start/stop/step
        if not self.values and self.step > 0 and self.stop >= self.start:
            self.values = values_inclusive(self.start, self.stop, self.step)

    def get_values(self) -> list[float]:
        """Return the list of values for this variable."""
        if self.values:
            return list(self.values)
        if self.step > 0 and self.stop >= self.start:
            return values_inclusive(self.start, self.stop, self.step)
        return []

    def to_dict(self) -> dict[str, Any]:
        d = {
            "name": self.name,
            "display_name": self.display_name,
            "unit": self.unit,
        }
        if self.values:
            d["values"] = self.values
        else:
            d["start"] = self.start
            d["stop"] = self.stop
            d["step"] = self.step
        return d

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> "ScanVariable":
        return cls(
            name=d["name"],
            display_name=d.get("display_name", ""),
            unit=d.get("unit", ""),
            start=d.get("start", 0.0),
            stop=d.get("stop", 0.0),
            step=d.get("step", 0.0),
            values=[float(v) for v in d.get("values", [])],
        )


@dataclass
class ScanCase:
    """An operating condition / case definition."""
    id: str
    name: str = ""
    params: dict[str, float] = field(default_factory=dict)

    def __post_init__(self) -> None:
        if not self.name:
            self.name = self.id

    def to_dict(self) -> dict[str, Any]:
        return {"id": self.id, "name": self.name, "params": self.params}

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> "ScanCase":
        return cls(
            id=d["id"],
            name=d.get("name", ""),
            params={k: float(v) for k, v in d.get("params", {}).items()},
        )


@dataclass
class SimulationPlan:
    """A complete simulation plan (Phase 1 minimal schema)."""
    plan_id: str = ""
    plan_version: str = "1.0"
    created_at: str = ""
    topology: str = "SSSR"
    model_path: str = ""
    variables: list[ScanVariable] = field(default_factory=list)
    cases: list[ScanCase] = field(default_factory=list)
    output_metrics: list[str] = field(default_factory=lambda: [
        "tavg_nm", "ripple_pct", "efficiency_pct", "total_losses_w"
    ])
    foreground: bool = True
    notes: str = ""

    def __post_init__(self) -> None:
        if not self.plan_id:
            self.plan_id = f"SP-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
        if not self.created_at:
            self.created_at = datetime.now().isoformat(timespec="seconds")
        if not self.cases:
            self.cases = [ScanCase(id="default", name="Default operating point")]

    # -- Point generation --

    def generate_points(self) -> tuple[list[dict], list[str]]:
        """Generate all scan points (Cartesian product of variables).
        Returns (points, var_names). Each point has 'index' and 'params'."""
        var_list = []
        var_names = []
        for v in self.variables:
            vals = v.get_values()
            if vals:
                var_list.append({"name": v.name, "values": vals})
                var_names.append(v.name)
        points = generate_cartesian_points(var_list)
        return points, var_names

    def estimate_total_points(self) -> int:
        """Estimate total number of scan points."""
        total = 1
        for v in self.variables:
            vals = v.get_values()
            if vals:
                total *= len(vals)
        return total if self.variables else 1

    # -- Serialization --

    def to_dict(self) -> dict[str, Any]:
        return {
            "plan_id": self.plan_id,
            "plan_version": self.plan_version,
            "created_at": self.created_at,
            "topology": self.topology,
            "model_path": self.model_path,
            "variables": [v.to_dict() for v in self.variables],
            "cases": [c.to_dict() for c in self.cases],
            "output_metrics": self.output_metrics,
            "foreground": self.foreground,
            "notes": self.notes,
        }

    def to_json(self, indent: int = 2) -> str:
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)

    def save(self, path: str | Path) -> None:
        Path(path).write_text(self.to_json(), encoding="utf-8")

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> "SimulationPlan":
        return cls(
            plan_id=d.get("plan_id", ""),
            plan_version=d.get("plan_version", "1.0"),
            created_at=d.get("created_at", ""),
            topology=d.get("topology", "SSSR"),
            model_path=d.get("model_path", ""),
            variables=[ScanVariable.from_dict(v) for v in d.get("variables", [])],
            cases=[ScanCase.from_dict(c) for c in d.get("cases", [])],
            output_metrics=d.get("output_metrics", [
                "tavg_nm", "ripple_pct", "efficiency_pct", "total_losses_w"
            ]),
            foreground=d.get("foreground", True),
            notes=d.get("notes", ""),
        )

    @classmethod
    def load(cls, path: str | Path) -> "SimulationPlan":
        text = Path(path).read_text(encoding="utf-8")
        return cls.from_dict(json.loads(text))

    # -- Validation --

    def validate(self) -> tuple[bool, list[str]]:
        """Validate the plan. Returns (ok, list_of_errors)."""
        errors = []
        if not self.model_path:
            errors.append("model_path is required")
        elif not Path(self.model_path).exists():
            errors.append(f"model_path does not exist: {self.model_path}")
        if not self.variables:
            errors.append("At least one scan variable is required")
        for i, v in enumerate(self.variables):
            if not v.name:
                errors.append(f"Variable {i}: name is required")
            vals = v.get_values()
            if not vals:
                errors.append(f"Variable {v.name}: no values generated (check start/stop/step or values)")
        return (len(errors) == 0, errors)
