"""Diff generation for Form.io component trees."""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any

from mcp_eregistrations_bpa.tools.formio_helpers import (
    find_component,
    get_all_component_keys,
)

# Properties that are structural / internal and should not be diffed.
_INTERNAL_PROPS = frozenset({"components", "columns", "rows", "tabs", "id"})


class DiffType(StrEnum):
    """Types of differences between component trees."""

    MODIFIED = "modified"
    ADDED = "added"
    REMOVED = "removed"
    MOVED = "moved"


@dataclass(frozen=True)
class DiffEntry:
    """A single difference between two component trees."""

    diff_type: DiffType
    path: str
    property: str | None = None
    old_value: Any = None
    new_value: Any = None
    component_key: str | None = None
    component_type: str | None = None

    def __str__(self) -> str:
        if self.diff_type == DiffType.MODIFIED:
            old_display = _truncate(repr(self.old_value), 60)
            new_display = _truncate(repr(self.new_value), 60)
            return (
                f"MODIFIED {self.path} .{self.property}: {old_display} -> {new_display}"
            )
        if self.diff_type == DiffType.ADDED:
            return f"ADDED {self.component_key} ({self.component_type}) at {self.path}"
        if self.diff_type == DiffType.REMOVED:
            return (
                f"REMOVED {self.component_key} ({self.component_type}) from {self.path}"
            )
        if self.diff_type == DiffType.MOVED:
            return (
                f"MOVED {self.component_key} from {self.old_value} to {self.new_value}"
            )
        return f"{self.diff_type.value}: {self.path}"  # pragma: no cover


@dataclass
class DiffResult:
    """Aggregated result of comparing two component trees."""

    entries: list[DiffEntry]
    components_before: int
    components_after: int
    keys_before: set[str] = field(default_factory=set)
    keys_after: set[str] = field(default_factory=set)

    @property
    def has_changes(self) -> bool:
        return len(self.entries) > 0

    @property
    def added_count(self) -> int:
        return sum(1 for e in self.entries if e.diff_type == DiffType.ADDED)

    @property
    def removed_count(self) -> int:
        return sum(1 for e in self.entries if e.diff_type == DiffType.REMOVED)

    @property
    def modified_count(self) -> int:
        return sum(1 for e in self.entries if e.diff_type == DiffType.MODIFIED)

    def to_dict(self) -> dict[str, Any]:
        """Serialize to a JSON-friendly dict."""
        return {
            "summary": self.summary(),
            "components_before": self.components_before,
            "components_after": self.components_after,
            "has_changes": self.has_changes,
            "entries": [str(e) for e in self.entries],
        }

    def summary(self) -> str:
        """One-line summary of changes."""
        if not self.has_changes:
            return "No changes detected."

        parts: list[str] = []
        if self.added_count:
            parts.append(f"{self.added_count} added")
        if self.removed_count:
            parts.append(f"{self.removed_count} removed")
        if self.modified_count:
            parts.append(f"{self.modified_count} modified")
        return f"Changes: {', '.join(parts)}."

    def format(self, label: str = "Diff") -> str:
        """Multi-line formatted output with a label header."""
        lines = [f"--- {label} ---"]
        lines.append(self.summary())
        lines.append(f"Components: {self.components_before} -> {self.components_after}")
        if self.entries:
            lines.append("")
            for entry in self.entries:
                lines.append(f"  {entry}")
        return "\n".join(lines)


# ── Public API ────────────────────────────────────────────────────


def generate_diff(
    before: list[dict[str, Any]],
    after: list[dict[str, Any]],
) -> DiffResult:
    """Compare two Form.io component trees and produce a diff.

    Args:
        before: Component list before changes.
        after: Component list after changes.

    Returns:
        DiffResult with entries for all detected differences.
    """
    keys_before = get_all_component_keys(before)
    keys_after = get_all_component_keys(after)

    # Ensure we have sets (get_all_component_keys returns set by default)
    if not isinstance(keys_before, set):
        keys_before = set(keys_before)
    if not isinstance(keys_after, set):
        keys_after = set(keys_after)

    entries: list[DiffEntry] = []

    # Added components
    for key in sorted(keys_after - keys_before):
        result = find_component(after, key)
        if result:
            comp, path = result
            entries.append(
                DiffEntry(
                    diff_type=DiffType.ADDED,
                    path=".".join(path) if path else "",
                    component_key=key,
                    component_type=comp.get("type"),
                )
            )

    # Removed components
    for key in sorted(keys_before - keys_after):
        result = find_component(before, key)
        if result:
            comp, path = result
            entries.append(
                DiffEntry(
                    diff_type=DiffType.REMOVED,
                    path=".".join(path) if path else "",
                    component_key=key,
                    component_type=comp.get("type"),
                )
            )

    # Modified components (present in both)
    for key in sorted(keys_before & keys_after):
        result_before = find_component(before, key)
        result_after = find_component(after, key)
        if not result_before or not result_after:
            continue

        comp_before, path_before = result_before
        comp_after, path_after = result_after

        # Determine the display path
        display_path = ".".join(path_after + [key]) if path_after else key

        # Compare all non-internal properties
        all_props = set(comp_before.keys()) | set(comp_after.keys())
        for prop in sorted(all_props - _INTERNAL_PROPS):
            old_val = comp_before.get(prop)
            new_val = comp_after.get(prop)
            if old_val != new_val:
                entries.append(
                    DiffEntry(
                        diff_type=DiffType.MODIFIED,
                        path=display_path,
                        property=prop,
                        old_value=old_val,
                        new_value=new_val,
                        component_key=key,
                        component_type=comp_after.get("type"),
                    )
                )

    return DiffResult(
        entries=entries,
        components_before=_count_components(before),
        components_after=_count_components(after),
        keys_before=keys_before,
        keys_after=keys_after,
    )


# ── Helpers ───────────────────────────────────────────────────────


def _count_components(components: list[dict[str, Any]], depth: int = 0) -> int:
    """Count all components in a tree, including keyless ones.

    Unlike ``get_all_component_keys``, this counts every component dict
    regardless of whether it has a ``key`` property.
    """
    if not isinstance(components, list) or depth > 50:
        return 0

    count = 0
    for comp in components:
        if not isinstance(comp, dict):
            continue
        count += 1

        # Nested components
        nested = comp.get("components", [])
        if isinstance(nested, list) and nested:
            count += _count_components(nested, depth + 1)

        # Columns
        columns = comp.get("columns", [])
        if isinstance(columns, list):
            for col in columns:
                if isinstance(col, dict):
                    col_comps = col.get("components", [])
                    if isinstance(col_comps, list) and col_comps:
                        count += _count_components(col_comps, depth + 1)

        # Tabs
        tabs = comp.get("tabs", [])
        if isinstance(tabs, list):
            for tab in tabs:
                if isinstance(tab, dict):
                    tab_comps = tab.get("components", [])
                    if isinstance(tab_comps, list) and tab_comps:
                        count += _count_components(tab_comps, depth + 1)

        # Table rows (rows[][] structure)
        rows = comp.get("rows", [])
        if isinstance(rows, list):
            for row in rows:
                if isinstance(row, list):
                    for cell in row:
                        if isinstance(cell, dict):
                            cell_comps = cell.get("components", [])
                            if isinstance(cell_comps, list) and cell_comps:
                                count += _count_components(cell_comps, depth + 1)

    return count


def _truncate(s: str, max_len: int = 80) -> str:
    """Truncate a string for display, adding ellipsis if needed."""
    if len(s) <= max_len:
        return s
    return s[: max_len - 3] + "..."
