"""MCP tools for editing role forms via the FormPatchEngine.

These tools provide structured, diff-aware editing of Form.io schemas
embedded in BPA roles.  They wrap the generic ``FormPatchEngine`` with the
``RoleFormAdapter`` so callers work at the *role* level rather than
manipulating raw JSON.

Write operations follow the audit-before-write pattern:
1. Auth check
2. Fetch role via RoleFormAdapter
3. Apply operations via FormPatchEngine
4. Audit (pending -> success/failed)
5. Save modified tree back via adapter

NOTE: patch.* imports are deferred to function bodies to avoid a circular
import.  Chain: tools/__init__ -> role_forms -> patch.engine -> patch.diff
-> tools.formio_helpers -> (triggers tools/__init__ again).
"""

from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Any

from fastmcp.exceptions import ToolError

from mcp_eregistrations_bpa.audit.context import (
    NotAuthenticatedError,
    get_current_user_email,
)
from mcp_eregistrations_bpa.audit.logger import AuditLogger
from mcp_eregistrations_bpa.bpa_client import BPAClient
from mcp_eregistrations_bpa.bpa_client.errors import (
    BPAClientError,
    translate_error,
)
from mcp_eregistrations_bpa.config import resolve_db_path

if TYPE_CHECKING:
    from mcp_eregistrations_bpa.patch.engine import PatchResult
    from mcp_eregistrations_bpa.patch.operations import Operation

__all__ = [
    "role_form_component_update",
    "role_form_component_add",
    "role_form_component_remove",
    "role_form_patch",
    "register_role_form_tools",
]


# ── Internal helpers ──────────────────────────────────────────────


def _diff_to_dict(result: PatchResult) -> dict[str, Any]:
    """Serialize a PatchResult.diff into a JSON-friendly dict."""
    return result.diff.to_dict()


async def _execute_patch(
    role_id: str | int,
    operations: list[Operation],
    *,
    instance: str | None = None,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Core patch execution shared by all role_form_* tools.

    1. Auth check
    2. Fetch role via RoleFormAdapter
    3. Apply operations via FormPatchEngine
    4. If dry_run: return diff without PUT
    5. Audit-before-write: record pending, save rollback state
    6. Save modified tree
    7. Mark audit success/failed
    """
    # Deferred imports to break circular dependency
    from mcp_eregistrations_bpa.patch.adapters.role_form import RoleFormAdapter
    from mcp_eregistrations_bpa.patch.engine import FormPatchEngine

    # 1. Auth
    try:
        user_email = get_current_user_email(instance=instance)
    except NotAuthenticatedError as e:
        raise ToolError(str(e))

    try:
        async with BPAClient.for_instance(instance) as client:
            # 2. Fetch
            adapter = RoleFormAdapter(client)
            try:
                components = await adapter.fetch(str(role_id))
            except BPAClientError as e:
                raise translate_error(e, resource_type="role")

            # 3. Apply patch
            engine = FormPatchEngine()
            result = engine.apply(components, operations, dry_run=dry_run)

            if not result.success:
                raise ToolError(f"Patch failed: {'; '.join(result.errors)}")

            diff_dict = _diff_to_dict(result)

            # 4. Dry run — return diff only
            if dry_run:
                return {
                    "success": True,
                    "dry_run": True,
                    "role_id": str(role_id),
                    "diff": diff_dict,
                }

            # 5. Audit-before-write
            audit_logger = AuditLogger(db_path=resolve_db_path(instance))
            audit_id = await audit_logger.record_pending(
                user_email=user_email,
                operation_type="update",
                object_type="role_form",
                object_id=str(role_id),
                params={
                    "operations_count": len(operations),
                    "diff_summary": diff_dict["summary"],
                },
            )

            # Save rollback state (previous form envelope)
            previous_state = deepcopy(adapter.envelope) or {}
            await audit_logger.save_rollback_state(
                audit_id=audit_id,
                object_type="role",
                object_id=str(role_id),
                previous_state=previous_state,
            )

            # 6. Save modified tree
            try:
                assert result.modified_tree is not None
                await adapter.save(result.modified_tree)

                # 7a. Mark success
                await audit_logger.mark_success(
                    audit_id,
                    result={
                        "role_id": str(role_id),
                        "diff_summary": diff_dict["summary"],
                    },
                )

                return {
                    "success": True,
                    "dry_run": False,
                    "role_id": str(role_id),
                    "diff": diff_dict,
                    "audit_id": audit_id,
                }

            except BPAClientError as e:
                # 7b. Mark failed
                await audit_logger.mark_failed(audit_id, str(e))
                raise translate_error(e, resource_type="role")

    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="role")


# ── MCP tools ────────────────────────────────────────────────────


async def role_form_component_update(
    role_id: str | int,
    component_key: str,
    properties: dict[str, Any],
    instance: str | None = None,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Update a component's properties in a role form. Audited write operation.

    Args:
        role_id: Role ID.
        component_key: Key of the component to modify.
        properties: Properties to set (e.g. {"label": "First Name"}).
        instance: Instance profile name (from instance_list).
        dry_run: Preview changes without saving (default: False).

    Returns:
        dict with success, diff, role_id, audit_id.
    """
    from mcp_eregistrations_bpa.patch.operations import (
        Operation,
        OperationType,
    )

    if not properties:
        raise ToolError(
            "No properties provided. Pass a dict of properties to update "
            '(e.g. properties={"label": "First Name"}).'
        )

    op = Operation(
        op=OperationType.SET,
        key=component_key,
        properties=dict(properties),
    )
    return await _execute_patch(role_id, [op], instance=instance, dry_run=dry_run)


async def role_form_component_add(
    role_id: str | int,
    component: dict[str, Any],
    parent_key: str | None = None,
    position: int | None = None,
    instance: str | None = None,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Add a component to a role form. Audited write operation.

    Args:
        role_id: Role ID.
        component: Form.io component dict (must include key and type).
        parent_key: Insert inside this container (default: root).
        position: Zero-based insertion index (default: append).
        instance: Instance profile name (from instance_list).
        dry_run: Preview changes without saving (default: False).

    Returns:
        dict with success, diff, role_id, audit_id.
    """
    from mcp_eregistrations_bpa.patch.operations import (
        Operation,
        OperationType,
    )

    op = Operation(
        op=OperationType.ADD,
        component=component,
        parent_key=parent_key,
        position=position,
    )
    return await _execute_patch(role_id, [op], instance=instance, dry_run=dry_run)


async def role_form_component_remove(
    role_id: str | int,
    component_key: str,
    instance: str | None = None,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Remove a component from a role form. Audited write operation.

    Args:
        role_id: Role ID.
        component_key: Key of the component to remove.
        instance: Instance profile name (from instance_list).
        dry_run: Preview changes without saving (default: False).

    Returns:
        dict with success, diff, role_id, audit_id.
    """
    from mcp_eregistrations_bpa.patch.operations import (
        Operation,
        OperationType,
    )

    op = Operation(op=OperationType.REMOVE, key=component_key)
    return await _execute_patch(role_id, [op], instance=instance, dry_run=dry_run)


async def role_form_patch(
    role_id: str | int,
    operations: list[dict[str, Any]],
    instance: str | None = None,
    dry_run: bool = False,
) -> dict[str, Any]:
    """Apply a batch of patch operations to a role form. Audited write operation.

    Args:
        role_id: Role ID.
        operations: List of operation dicts (each must have 'op' key).
        instance: Instance profile name (from instance_list).
        dry_run: Preview changes without saving (default: False).

    Returns:
        dict with success, diff, role_id, operations_count, audit_id.
    """
    from mcp_eregistrations_bpa.patch.operations import Operation

    if not operations:
        raise ToolError("No operations provided.")

    ops = [Operation.from_dict(d) for d in operations]
    result = await _execute_patch(role_id, ops, instance=instance, dry_run=dry_run)
    result["operations_count"] = len(ops)
    return result


# ── Registration ─────────────────────────────────────────────────


def register_role_form_tools(mcp: Any) -> None:
    """Register role form tools with the MCP server.

    Args:
        mcp: The FastMCP server instance.
    """
    from mcp_eregistrations_bpa.tools.annotations import WRITE

    mcp.tool(annotations=WRITE)(role_form_component_update)
    mcp.tool(annotations=WRITE)(role_form_component_add)
    mcp.tool(annotations=WRITE)(role_form_component_remove)
    mcp.tool(annotations=WRITE)(role_form_patch)
