"""Compare two LTspice schematics and produce a structured diff."""

from dataclasses import dataclass, field
from pathlib import Path

from .schematic import Component, Schematic, parse_schematic


@dataclass
class ComponentChange:
    """A change to a component."""

    name: str
    change_type: str  # "added", "removed", "modified"
    symbol: str = ""
    old_value: str | None = None
    new_value: str | None = None
    old_attributes: dict[str, str] = field(default_factory=dict)
    new_attributes: dict[str, str] = field(default_factory=dict)
    moved: bool = False  # Position changed


@dataclass
class DirectiveChange:
    """A change to a SPICE directive."""

    change_type: str  # "added", "removed", "modified"
    old_text: str | None = None
    new_text: str | None = None


@dataclass
class SchematicDiff:
    """Complete diff between two schematics."""

    component_changes: list[ComponentChange] = field(default_factory=list)
    directive_changes: list[DirectiveChange] = field(default_factory=list)
    nets_added: list[str] = field(default_factory=list)
    nets_removed: list[str] = field(default_factory=list)
    wires_added: int = 0
    wires_removed: int = 0

    @property
    def has_changes(self) -> bool:
        return bool(
            self.component_changes
            or self.directive_changes
            or self.nets_added
            or self.nets_removed
            or self.wires_added
            or self.wires_removed
        )

    def summary(self) -> str:
        """Human-readable summary of changes."""
        lines: list[str] = []

        if not self.has_changes:
            return "No changes detected."

        # Group component changes by type
        added = [c for c in self.component_changes if c.change_type == "added"]
        removed = [c for c in self.component_changes if c.change_type == "removed"]
        modified = [c for c in self.component_changes if c.change_type == "modified"]

        if modified:
            lines.append(f"{len(modified)} component{'s' if len(modified) != 1 else ''} modified:")
            for c in modified:
                parts: list[str] = []
                if c.old_value != c.new_value:
                    parts.append(f"{c.old_value} -> {c.new_value}")
                if c.moved:
                    parts.append("moved")
                # Check for attribute changes beyond value
                attr_diff = _attr_diff_summary(c.old_attributes, c.new_attributes)
                if attr_diff:
                    parts.append(attr_diff)
                detail = ", ".join(parts) if parts else "attributes changed"
                lines.append(f"  {c.name}: {detail}")

        if added:
            for c in added:
                val = f" = {c.new_value}" if c.new_value else ""
                lines.append(
                    f"1 component added: {c.name} ({c.symbol}){val}"
                    if len(added) == 1
                    else f"  {c.name} ({c.symbol}){val}"
                )
            if len(added) > 1:
                lines.insert(
                    len(lines) - len(added),
                    f"{len(added)} components added:",
                )

        if removed:
            for c in removed:
                val = f" = {c.old_value}" if c.old_value else ""
                lines.append(
                    f"1 component removed: {c.name} ({c.symbol}){val}"
                    if len(removed) == 1
                    else f"  {c.name} ({c.symbol}){val}"
                )
            if len(removed) > 1:
                lines.insert(
                    len(lines) - len(removed),
                    f"{len(removed)} components removed:",
                )

        # Directive changes
        dir_added = [d for d in self.directive_changes if d.change_type == "added"]
        dir_removed = [d for d in self.directive_changes if d.change_type == "removed"]
        dir_modified = [d for d in self.directive_changes if d.change_type == "modified"]

        for d in dir_modified:
            lines.append(f"1 directive changed: {d.old_text} -> {d.new_text}")
        for d in dir_added:
            lines.append(f"1 directive added: {d.new_text}")
        for d in dir_removed:
            lines.append(f"1 directive removed: {d.old_text}")

        # Net changes
        if self.nets_added:
            lines.append(
                f"{len(self.nets_added)} net{'s' if len(self.nets_added) != 1 else ''} "
                f"added: {', '.join(self.nets_added)}"
            )
        if self.nets_removed:
            lines.append(
                f"{len(self.nets_removed)} net{'s' if len(self.nets_removed) != 1 else ''} "
                f"removed: {', '.join(self.nets_removed)}"
            )

        # Wire changes
        wire_parts: list[str] = []
        if self.wires_added:
            wire_parts.append(
                f"{self.wires_added} wire{'s' if self.wires_added != 1 else ''} added"
            )
        if self.wires_removed:
            wire_parts.append(
                f"{self.wires_removed} wire{'s' if self.wires_removed != 1 else ''} removed"
            )
        if wire_parts:
            lines.append(", ".join(wire_parts))

        return "\n".join(lines)

    def to_dict(self) -> dict:
        """Convert to JSON-serializable dict."""
        return {
            "has_changes": self.has_changes,
            "component_changes": [
                {
                    "name": c.name,
                    "change_type": c.change_type,
                    "symbol": c.symbol,
                    "old_value": c.old_value,
                    "new_value": c.new_value,
                    "old_attributes": c.old_attributes,
                    "new_attributes": c.new_attributes,
                    "moved": c.moved,
                }
                for c in self.component_changes
            ],
            "directive_changes": [
                {
                    "change_type": d.change_type,
                    "old_text": d.old_text,
                    "new_text": d.new_text,
                }
                for d in self.directive_changes
            ],
            "nets_added": self.nets_added,
            "nets_removed": self.nets_removed,
            "wires_added": self.wires_added,
            "wires_removed": self.wires_removed,
            "summary": self.summary(),
        }


def _attr_diff_summary(old: dict[str, str], new: dict[str, str]) -> str:
    """Summarize attribute differences, excluding Value (handled separately)."""
    changes: list[str] = []
    all_keys = set(old) | set(new)
    # Skip Value since it's reported on its own
    all_keys.discard("Value")
    all_keys.discard("Value2")
    for key in sorted(all_keys):
        old_val = old.get(key)
        new_val = new.get(key)
        if old_val != new_val:
            if old_val is None:
                changes.append(f"+{key}={new_val}")
            elif new_val is None:
                changes.append(f"-{key}={old_val}")
            else:
                changes.append(f"{key}: {old_val} -> {new_val}")
    return "; ".join(changes)


def _normalize_directive(text: str) -> str:
    """Normalize whitespace in a SPICE directive for comparison."""
    return " ".join(text.split())


def _wire_set(schematic: Schematic) -> set[tuple[int, int, int, int]]:
    """Convert wires to a set of coordinate tuples for comparison.

    Each wire is stored in a canonical form (smaller point first) so that
    reversed wires compare equal.
    """
    result: set[tuple[int, int, int, int]] = set()
    for w in schematic.wires:
        # Canonical ordering: sort by (x, y) so direction doesn't matter
        if (w.x1, w.y1) <= (w.x2, w.y2):
            result.add((w.x1, w.y1, w.x2, w.y2))
        else:
            result.add((w.x2, w.y2, w.x1, w.y1))
    return result


def _component_map(schematic: Schematic) -> dict[str, Component]:
    """Build a map of component instance name -> Component."""
    return {comp.name: comp for comp in schematic.components}


def _diff_components(schema_a: Schematic, schema_b: Schematic) -> list[ComponentChange]:
    """Compare components between two schematics."""
    map_a = _component_map(schema_a)
    map_b = _component_map(schema_b)

    names_a = set(map_a)
    names_b = set(map_b)

    changes: list[ComponentChange] = []

    # Removed components (in A but not B)
    for name in sorted(names_a - names_b):
        comp = map_a[name]
        changes.append(
            ComponentChange(
                name=name,
                change_type="removed",
                symbol=comp.symbol,
                old_value=comp.value,
                old_attributes=dict(comp.attributes),
            )
        )

    # Added components (in B but not A)
    for name in sorted(names_b - names_a):
        comp = map_b[name]
        changes.append(
            ComponentChange(
                name=name,
                change_type="added",
                symbol=comp.symbol,
                new_value=comp.value,
                new_attributes=dict(comp.attributes),
            )
        )

    # Potentially modified components (in both)
    for name in sorted(names_a & names_b):
        comp_a = map_a[name]
        comp_b = map_b[name]

        moved = (comp_a.x, comp_a.y) != (comp_b.x, comp_b.y)
        value_changed = comp_a.value != comp_b.value
        attrs_changed = comp_a.attributes != comp_b.attributes
        rotation_changed = comp_a.rotation != comp_b.rotation or comp_a.mirror != comp_b.mirror

        if moved or value_changed or attrs_changed or rotation_changed:
            changes.append(
                ComponentChange(
                    name=name,
                    change_type="modified",
                    symbol=comp_a.symbol,
                    old_value=comp_a.value,
                    new_value=comp_b.value,
                    old_attributes=dict(comp_a.attributes),
                    new_attributes=dict(comp_b.attributes),
                    moved=moved,
                )
            )

    return changes


def _diff_directives(schema_a: Schematic, schema_b: Schematic) -> list[DirectiveChange]:
    """Compare SPICE directives between two schematics."""
    directives_a = [t.content for t in schema_a.texts if t.type == "spice"]
    directives_b = [t.content for t in schema_b.texts if t.type == "spice"]

    # Normalize for comparison but keep originals for display
    norm_a = {_normalize_directive(d): d for d in directives_a}
    norm_b = {_normalize_directive(d): d for d in directives_b}

    keys_a = set(norm_a)
    keys_b = set(norm_b)

    changes: list[DirectiveChange] = []

    # Removed directives
    for key in sorted(keys_a - keys_b):
        changes.append(DirectiveChange(change_type="removed", old_text=norm_a[key]))

    # Added directives
    for key in sorted(keys_b - keys_a):
        changes.append(DirectiveChange(change_type="added", new_text=norm_b[key]))

    # For modified detection: directives that share a command keyword but differ.
    # We match by the first token (e.g., ".tran", ".ac") to detect modifications
    # vs pure add/remove pairs.
    removed = [c for c in changes if c.change_type == "removed"]
    added = [c for c in changes if c.change_type == "added"]

    matched_removed: set[int] = set()
    matched_added: set[int] = set()

    for ri, rc in enumerate(removed):
        old_cmd = (rc.old_text or "").split()[0].lower() if rc.old_text else ""
        for ai, ac in enumerate(added):
            if ai in matched_added:
                continue
            new_cmd = (ac.new_text or "").split()[0].lower() if ac.new_text else ""
            if old_cmd and old_cmd == new_cmd:
                matched_removed.add(ri)
                matched_added.add(ai)
                break

    # Rebuild the list: unmatched stay as-is, matched pairs become "modified"
    final_changes: list[DirectiveChange] = []
    for ri, rc in enumerate(removed):
        if ri not in matched_removed:
            final_changes.append(rc)

    for ai, ac in enumerate(added):
        if ai not in matched_added:
            final_changes.append(ac)

    for ri in sorted(matched_removed):
        rc = removed[ri]
        # Find its matched added entry
        old_cmd = (rc.old_text or "").split()[0].lower()
        for ai, ac in enumerate(added):
            new_cmd = (ac.new_text or "").split()[0].lower() if ac.new_text else ""
            if ai in matched_added and old_cmd == new_cmd:
                final_changes.append(
                    DirectiveChange(
                        change_type="modified",
                        old_text=rc.old_text,
                        new_text=ac.new_text,
                    )
                )
                break

    return final_changes


def _diff_nets(schema_a: Schematic, schema_b: Schematic) -> tuple[list[str], list[str]]:
    """Compare net flags between two schematics.

    Returns:
        (nets_added, nets_removed)
    """
    names_a = {f.name for f in schema_a.flags}
    names_b = {f.name for f in schema_b.flags}

    added = sorted(names_b - names_a)
    removed = sorted(names_a - names_b)
    return added, removed


def _diff_wires(schema_a: Schematic, schema_b: Schematic) -> tuple[int, int]:
    """Compare wires between two schematics using set operations.

    Returns:
        (wires_added, wires_removed)
    """
    set_a = _wire_set(schema_a)
    set_b = _wire_set(schema_b)

    added = len(set_b - set_a)
    removed = len(set_a - set_b)
    return added, removed


def diff_schematics(
    path_a: Path | str,
    path_b: Path | str,
) -> SchematicDiff:
    """Compare two schematics and return differences.

    Args:
        path_a: Path to the "before" schematic
        path_b: Path to the "after" schematic

    Returns:
        SchematicDiff with all changes
    """
    schema_a = parse_schematic(path_a)
    schema_b = parse_schematic(path_b)

    component_changes = _diff_components(schema_a, schema_b)
    directive_changes = _diff_directives(schema_a, schema_b)
    nets_added, nets_removed = _diff_nets(schema_a, schema_b)
    wires_added, wires_removed = _diff_wires(schema_a, schema_b)

    return SchematicDiff(
        component_changes=component_changes,
        directive_changes=directive_changes,
        nets_added=nets_added,
        nets_removed=nets_removed,
        wires_added=wires_added,
        wires_removed=wires_removed,
    )
