"""FormPatchEngine — applies operations to Form.io component trees."""

from __future__ import annotations

import copy
from dataclasses import dataclass, field
from typing import Any

from mcp_eregistrations_bpa.patch.diff import DiffResult, generate_diff
from mcp_eregistrations_bpa.patch.operations import (
    Operation,
    OperationType,
    validate_operation,
)
from mcp_eregistrations_bpa.tools.formio_helpers import (
    CONTAINER_TYPES,
    _deep_merge,
    find_component,
    find_component_parent,
    get_all_component_keys,
)


@dataclass
class PatchResult:
    """Result of applying a patch to a component tree."""

    success: bool
    modified_tree: list[dict[str, Any]] | None
    diff: DiffResult
    errors: list[str] = field(default_factory=list)
    validation_passed: bool = True

    def format(self, label: str = "Patch") -> str:
        """Format the result for display."""
        lines = [f"--- {label} ---"]
        if not self.success:
            lines.append(f"FAILED: {'; '.join(self.errors)}")
            return "\n".join(lines)

        lines.append(self.diff.summary())
        lines.append(
            f"Components: {self.diff.components_before} -> {self.diff.components_after}"
        )
        if not self.validation_passed:
            lines.append("WARNING: post-apply validation failed")
        if self.diff.entries:
            lines.append("")
            for entry in self.diff.entries:
                lines.append(f"  {entry}")
        return "\n".join(lines)


class FormPatchEngine:
    """Applies a sequence of operations to a Form.io component tree.

    All-or-nothing semantics: if any operation fails, no changes are made.
    A single deepcopy is made at the start; internal methods mutate in-place.
    """

    def apply(
        self,
        components: list[dict[str, Any]],
        operations: list[Operation],
        *,
        dry_run: bool = False,
    ) -> PatchResult:
        """Apply operations to a component tree.

        Args:
            components: Original Form.io component list.
            operations: Ordered list of operations to apply.
            dry_run: If True, compute diff but return modified_tree=None.

        Returns:
            PatchResult with success status, modified tree, diff, and errors.
        """
        # 1. Validate all operations first
        all_errors: list[str] = []
        for i, op in enumerate(operations):
            op_errors = validate_operation(op)
            for err in op_errors:
                all_errors.append(f"Operation {i} ({op.op.value}): {err}")

        if all_errors:
            return PatchResult(
                success=False,
                modified_tree=None,
                diff=_empty_diff(components),
                errors=all_errors,
            )

        # 2. Deep copy components ONCE
        working = copy.deepcopy(components)

        # 3. Apply operations sequentially on the working copy
        for i, op in enumerate(operations):
            try:
                working = self._apply_op(working, op)
            except Exception as e:
                return PatchResult(
                    success=False,
                    modified_tree=None,
                    diff=_empty_diff(components),
                    errors=[f"Operation {i} ({op.op.value}): {e}"],
                )

        # 4. Post-apply validation
        validation_errors = self._validate(components, working)
        validation_passed = len(validation_errors) == 0

        # 5. Generate diff
        diff = generate_diff(components, working)

        # 6. Return result — validation failure = patch failure
        if not validation_passed:
            return PatchResult(
                success=False,
                modified_tree=None,
                diff=diff,
                errors=validation_errors,
                validation_passed=False,
            )

        return PatchResult(
            success=True,
            modified_tree=None if dry_run else working,
            diff=diff,
            errors=[],
            validation_passed=True,
        )

    # ── Dispatch ─────────────────────────────────────────────────

    def _apply_op(
        self, working: list[dict[str, Any]], op: Operation
    ) -> list[dict[str, Any]]:
        """Dispatch an operation to its handler. Returns the (possibly new) tree."""
        if op.op == OperationType.SET:
            self._apply_set(working, op)
            return working
        elif op.op == OperationType.ADD:
            self._apply_add(working, op)
            return working
        elif op.op == OperationType.REMOVE:
            return self._apply_remove(working, op)
        elif op.op == OperationType.MOVE:
            return self._apply_move(working, op)
        elif op.op == OperationType.REPLACE_ALL:
            self._apply_replace_all(working, op)
            return working
        else:
            raise ValueError(f"Unknown operation type: {op.op}")  # pragma: no cover

    # ── SET ──────────────────────────────────────────────────────

    def _apply_set(self, working: list[dict[str, Any]], op: Operation) -> None:
        """Set properties on an existing component (in-place)."""
        assert op.key is not None  # guaranteed by validate_operation
        assert op.properties is not None
        result = find_component(working, op.key)
        if result is None:
            raise ValueError(f"Component '{op.key}' not found")
        comp, _path = result
        _deep_merge(comp, op.properties)

    # ── ADD ──────────────────────────────────────────────────────

    def _apply_add(self, working: list[dict[str, Any]], op: Operation) -> None:
        """Add a new component to the tree (in-place)."""
        assert op.component is not None  # guaranteed by validate_operation
        new_comp = copy.deepcopy(op.component)
        new_key = new_comp.get("key")

        # Check for duplicate key
        if new_key:
            existing_keys = get_all_component_keys(working)
            if new_key in existing_keys:
                raise ValueError(
                    f"Duplicate key '{new_key}': "
                    f"a component with this key already exists"
                )

        if op.parent_key is not None:
            # Insert into a parent container
            parent_result = find_component(working, op.parent_key)
            if parent_result is None:
                raise ValueError(f"Parent component '{op.parent_key}' not found")

            parent_comp, _path = parent_result
            parent_type = parent_comp.get("type", "")

            # Verify parent is a container type
            has_components_array = isinstance(parent_comp.get("components"), list)
            if parent_type not in CONTAINER_TYPES and not has_components_array:
                valid = ", ".join(sorted(CONTAINER_TYPES))
                raise ValueError(
                    f"Component '{op.parent_key}' "
                    f"(type: {parent_type}) is not a container. "
                    f"Valid container types: {valid}"
                )

            children = parent_comp.get("components")
            if children is None:
                children = []
                parent_comp["components"] = children

            if op.position is not None:
                children.insert(op.position, new_comp)
            else:
                children.append(new_comp)
        else:
            # Insert at root level
            if op.position is not None:
                working.insert(op.position, new_comp)
            else:
                working.append(new_comp)

    # ── REMOVE ───────────────────────────────────────────────────

    def _apply_remove(
        self, working: list[dict[str, Any]], op: Operation
    ) -> list[dict[str, Any]]:
        """Remove a component from the tree.

        Uses find_component_parent directly on the working copy (already
        deep-copied by the engine) to avoid a second deepcopy.
        """
        assert op.key is not None  # guaranteed by validate_operation
        try:
            _parent, siblings, index = find_component_parent(working, op.key)
        except ValueError:
            raise ValueError(f"Component '{op.key}' not found — cannot remove")

        siblings.pop(index)
        return working

    # ── MOVE ─────────────────────────────────────────────────────

    def _apply_move(
        self, working: list[dict[str, Any]], op: Operation
    ) -> list[dict[str, Any]]:
        """Move a component to a new parent (in-place on working copy).

        Removes from current location then inserts at new location.
        """
        assert op.key is not None  # guaranteed by validate_operation
        # 1. Find and remove from current location
        try:
            _parent, siblings, index = find_component_parent(working, op.key)
        except ValueError:
            raise ValueError(f"Component '{op.key}' not found — cannot move")

        removed = siblings.pop(index)

        # 2. Insert at new location
        if op.parent_key is not None:
            parent_result = find_component(working, op.parent_key)
            if parent_result is None:
                raise ValueError(f"Target parent '{op.parent_key}' not found")

            parent_comp, _path = parent_result
            children = parent_comp.get("components")
            if children is None:
                children = []
                parent_comp["components"] = children

            if op.position is not None:
                children.insert(op.position, removed)
            else:
                children.append(removed)
        else:
            # Move to root
            if op.position is not None:
                working.insert(op.position, removed)
            else:
                working.append(removed)

        return working

    # ── REPLACE_ALL ──────────────────────────────────────────────

    def _apply_replace_all(self, working: list[dict[str, Any]], op: Operation) -> None:
        """Walk the tree and replace matching property values in-place."""
        assert op.prop is not None  # guaranteed by validate_operation
        assert op.match is not None
        assert op.value is not None
        self._walk_replace(working, op.prop, op.match, op.value)

    def _walk_replace(
        self,
        components: list[dict[str, Any]],
        prop: str,
        match: str,
        value: str,
    ) -> None:
        """Recursively walk tree, replacing substring matches in a string property."""
        for comp in components:
            if not isinstance(comp, dict):
                continue

            current = comp.get(prop)
            if isinstance(current, str) and match in current:
                comp[prop] = current.replace(match, value)

            # Recurse into nested structures
            nested = comp.get("components")
            if isinstance(nested, list):
                self._walk_replace(nested, prop, match, value)

            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):
                            self._walk_replace(col_comps, prop, match, value)

            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):
                            self._walk_replace(tab_comps, prop, match, value)

            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):
                                    self._walk_replace(cell_comps, prop, match, value)

    # ── Validation ───────────────────────────────────────────────

    def _validate(
        self,
        before: list[dict[str, Any]],
        after: list[dict[str, Any]],
    ) -> list[str]:
        """Post-apply validation of the modified tree."""
        errors: list[str] = []

        # Check for duplicate keys
        all_keys = get_all_component_keys(after, include_duplicates=True)
        if isinstance(all_keys, list):
            seen: set[str] = set()
            for k in all_keys:
                if k in seen:
                    errors.append(f"Duplicate key '{k}' in modified tree")
                seen.add(k)

        # Check tree integrity: every component must have a 'type' property
        self._check_types(after, errors)

        return errors

    def _check_types(
        self,
        components: list[dict[str, Any]],
        errors: list[str],
        depth: int = 0,
        *,
        parent_type: str | None = None,
    ) -> None:
        """Recursively check that every component has a 'type' property."""
        if depth > 50:
            return

        for comp in components:
            if not isinstance(comp, dict):
                continue

            key = comp.get("key", "<no key>")
            comp_type = comp.get("type")
            # Tab items stored in components[] of a 'tabs' parent don't have
            # their own 'type' — this is normal Form.io structure.
            if comp_type is None and parent_type != "tabs":
                errors.append(f"Component '{key}' is missing 'type' property")

            nested = comp.get("components")
            if isinstance(nested, list):
                self._check_types(
                    nested,
                    errors,
                    depth + 1,
                    parent_type=comp_type,
                )

            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):
                            self._check_types(col_comps, errors, depth + 1)

            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):
                            self._check_types(tab_comps, errors, depth + 1)

            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):
                                    self._check_types(cell_comps, errors, depth + 1)


# ── Module-level helpers ─────────────────────────────────────────


def _empty_diff(components: list[dict[str, Any]]) -> DiffResult:
    """Create an empty diff result (no changes)."""
    return generate_diff(components, components)
