"""MCP tools for BPA service export operations.

This module provides tools for exporting complete BPA service definitions.

The export endpoint returns large JSON payloads (5-15MB) containing:
- Service metadata
- Registrations, determinants, roles, bots
- Form definitions (Form.io schemas)
- Translations, tutorials, catalogs

For large exports (>5MB), the data is saved to a file and a summary is returned.

API Endpoint used:
- POST /download_service/{service_id} - Export complete service definition
"""

from __future__ import annotations

import copy
import json
import logging
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from fastmcp import Context
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,
    BPANotFoundError,
    translate_error,
)
from mcp_eregistrations_bpa.config import resolve_db_path
from mcp_eregistrations_bpa.tools.yaml_transformer import (
    TransformationError,
    YAMLTransformer,
    normalize_export_data,
)

__all__ = [
    "service_export_raw",
    "service_to_yaml",
    "service_copy",
    "service_copy_repair_roles",
    "register_export_tools",
    "_safe_list_count",
    "_validate_copy_roles",
    "_remap_registration_ids",
]

# Threshold for saving to file instead of returning inline (5MB for JSON)
LARGE_EXPORT_THRESHOLD_BYTES = 5 * 1024 * 1024

# Threshold for YAML output (500KB)
LARGE_YAML_THRESHOLD_BYTES = 500 * 1024


def _safe_list_count(value: Any) -> int:
    """Safely count items in a list, handling None and non-list values.

    Args:
        value: The value to count (expected to be a list, but handles edge cases).

    Returns:
        int: Count of items if value is a non-empty list, otherwise 0.
    """
    if value is None:
        return 0
    if not isinstance(value, list):
        return 0
    return len(value)


def _generate_export_summary(
    data: dict[str, Any],
    size_bytes: int,
    *,
    include_forms: bool = True,
    include_translations: bool = True,
    include_catalogs: bool = True,
) -> dict[str, Any]:
    """Generate a summary of the exported service.

    Args:
        data: The complete export data.
        size_bytes: Size of the export in bytes.
        include_forms: Whether forms were included in export.
        include_translations: Whether translations were included in export.
        include_catalogs: Whether catalogs were included in export.

    Returns:
        dict: Summary statistics about the export.
    """
    # Normalize export data to handle both live API and test mock structures
    data = normalize_export_data(data)

    # Count entities using safe counting that handles None, missing keys, non-lists
    registration_count = _safe_list_count(data.get("registrations"))
    determinant_count = _safe_list_count(data.get("determinants"))
    role_count = _safe_list_count(data.get("roles"))
    bot_count = _safe_list_count(data.get("bots"))

    # Count fields from forms
    field_count = 0
    for form_key in ["applicantForm", "guideForm", "sendFileForm", "paymentForm"]:
        form = data.get(form_key)
        if form and isinstance(form, dict):
            form_schema = form.get("formSchema")
            if form_schema and isinstance(form_schema, dict):
                components = form_schema.get("components", [])
                field_count += _count_form_components(components)

    # Build excluded sections list
    excluded_sections: list[str] = []
    if not include_forms:
        excluded_sections.append("forms")
    if not include_translations:
        excluded_sections.append("translations")
    if not include_catalogs:
        excluded_sections.append("catalogs")

    summary: dict[str, Any] = {
        "service_name": data.get("name", "Unknown"),
        "service_id": data.get("id"),
        "registration_count": registration_count,
        "determinant_count": determinant_count,
        "role_count": role_count,
        "bot_count": bot_count,
        "field_count": field_count,
        "size_bytes": size_bytes,
        "size_mb": round(size_bytes / (1024 * 1024), 2),
    }

    # Add selection info if any sections were excluded
    if excluded_sections:
        summary["excluded_sections"] = excluded_sections

    return summary


def _count_form_components(components: list[Any]) -> int:
    """Recursively count form components (fields).

    Args:
        components: List of Form.io components.

    Returns:
        int: Total count of form components.
    """
    count = 0
    for comp in components:
        if not isinstance(comp, dict):
            continue
        # Count this component if it has a key (it's a field)
        if comp.get("key"):
            count += 1
        # Recursively count nested components
        nested = comp.get("components", [])
        if isinstance(nested, list):
            count += _count_form_components(nested)
        # Handle columns layout
        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):
                        count += _count_form_components(col_comps)
    return count


def _save_export_to_file(
    data: dict[str, Any],
    service_id: str,
) -> str:
    """Save export data to a temporary file.

    Args:
        data: The export data to save.
        service_id: The service ID for the filename.

    Returns:
        str: Path to the saved file.
    """
    timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S")
    filename = f"bpa_export_{service_id}_{timestamp}.json"

    # Use system temp directory
    temp_dir = Path(tempfile.gettempdir())
    file_path = temp_dir / filename

    with open(file_path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    return str(file_path)


async def service_export_raw(
    service_id: str | int,
    include_all: bool = True,
    include_forms: bool = True,
    include_translations: bool = True,
    include_catalogs: bool = True,
    catalog_ids: list[str] | None = None,
    ctx: Context | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Export complete service definition as raw JSON.

    Large exports (>5MB) are saved to file; path returned in response.

    Args:
        service_id: BPA service UUID.
        include_all: Include all components (default True).
        include_forms: Include form schemas (default True).
        include_translations: Include translations (default True).
        include_catalogs: Include catalogs (default True).
        catalog_ids: Filter to specific catalog IDs (optional).
        ctx: MCP context for progress reporting (optional).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with export_data or file_path, summary, metadata.
    """
    # Build export options
    options: dict[str, Any] | None = None

    if not include_all:
        # Minimal export: only core metadata (granular options ignored)
        options = {
            "serviceSelected": True,
            "registrationsSelected": True,
            "costsSelected": False,
            "requirementsSelected": False,
            "resultsSelected": False,
            "activityConditionsSelected": False,
            "registrationLawsSelected": False,
            "serviceLocationsSelected": False,
            "serviceTutorialsSelected": False,
            "serviceTranslationsSelected": False,
            "guideFormSelected": False,
            "applicantFormSelected": False,
            "sendFileFormSelected": False,
            "paymentFormSelected": False,
            "catalogsSelected": False,
            "rolesSelected": False,
            "determinantsSelected": False,
            "printDocumentsSelected": False,
            "botsSelected": False,
            "copyService": False,
        }
        # Force granular options to match include_all=False behavior for summary
        include_forms = False
        include_translations = False
        include_catalogs = False
    else:
        # Check if any granular options differ from defaults
        has_granular_options = (
            not include_forms
            or not include_translations
            or not include_catalogs
            or catalog_ids is not None
        )

        if has_granular_options:
            # Build selective options with granular control
            options = {
                "serviceSelected": True,
                "registrationsSelected": True,
                "costsSelected": True,
                "requirementsSelected": True,
                "resultsSelected": True,
                "activityConditionsSelected": True,
                "registrationLawsSelected": True,
                "serviceLocationsSelected": True,
                "serviceTutorialsSelected": True,
                "serviceTranslationsSelected": include_translations,
                "guideFormSelected": include_forms,
                "applicantFormSelected": include_forms,
                "sendFileFormSelected": include_forms,
                "paymentFormSelected": include_forms,
                "catalogsSelected": include_catalogs,
                "rolesSelected": True,
                "determinantsSelected": True,
                "printDocumentsSelected": True,
                "botsSelected": True,
                "copyService": False,
            }
            # Add catalog filter if specified
            if include_catalogs and catalog_ids is not None:
                options["catalogsToCopy"] = catalog_ids

    # Report progress: starting export
    if ctx:
        await ctx.report_progress(
            progress=0.1,
            total=1.0,
            message=f"Starting export for service {service_id}...",
        )

    try:
        async with BPAClient.for_instance(instance) as client:
            # Report progress: connected, requesting export
            if ctx:
                await ctx.report_progress(
                    progress=0.2,
                    total=1.0,
                    message="Connected to BPA. Requesting service export...",
                )

            try:
                export_data, size_bytes = await client.download_service(
                    str(service_id),
                    options=options,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"[Service not found]: Service ID '{service_id}' does not exist. "
                    "[Verify the service ID using service_list]"
                )

            # Report progress: export received
            if ctx:
                size_mb = size_bytes / (1024 * 1024)
                await ctx.report_progress(
                    progress=0.8,
                    total=1.0,
                    message=f"Export received ({size_mb:.2f} MB). Processing...",
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # Generate summary with selection options
    summary = _generate_export_summary(
        export_data,
        size_bytes,
        include_forms=include_forms,
        include_translations=include_translations,
        include_catalogs=include_catalogs,
    )

    # Build metadata
    metadata = {
        "exported_at": datetime.now(UTC).isoformat(),
        "service_id": str(service_id),
        "include_all": include_all,
    }

    # Handle large exports
    if size_bytes >= LARGE_EXPORT_THRESHOLD_BYTES:
        file_path = _save_export_to_file(export_data, str(service_id))

        # Report progress: complete
        if ctx:
            await ctx.report_progress(
                progress=1.0,
                total=1.0,
                message=f"Export complete. Saved to {file_path}",
            )

        return {
            "file_path": file_path,
            "summary": summary,
            "metadata": metadata,
            "message": (
                f"Export saved to file ({summary['size_mb']} MB). "
                "Use the file_path to access the full data. "
                "Note: Temporary files should be cleaned up after use."
            ),
        }

    # Report progress: complete
    if ctx:
        await ctx.report_progress(
            progress=1.0,
            total=1.0,
            message="Export complete.",
        )

    # Return inline for smaller exports
    return {
        "export_data": export_data,
        "summary": summary,
        "metadata": metadata,
    }


def _save_yaml_to_file(
    content: str,
    service_id: str,
    extension: str = "yaml",
) -> str:
    """Save YAML/JSON content to a temporary file.

    Args:
        content: The YAML or JSON content to save.
        service_id: The service ID for the filename.
        extension: File extension to use (default "yaml").

    Returns:
        str: Path to the saved file.
    """
    timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S")
    filename = f"bpa_service_{service_id}_{timestamp}.{extension}"

    # Use system temp directory
    temp_dir = Path(tempfile.gettempdir())
    file_path = temp_dir / filename

    with open(file_path, "w", encoding="utf-8") as f:
        f.write(content)

    return str(file_path)


async def service_to_yaml(
    service_id: str | int,
    export_data: dict[str, Any] | None = None,
    include_metadata: bool = True,
    format: str = "yaml",
    ctx: Context | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Transform BPA service export to clean YAML/JSON format.

    Converts camelCase to snake_case, extracts Form.io essentials,
    parses determinant conditions. Large outputs (>500KB) saved to file.

    Args:
        service_id: BPA service UUID.
        export_data: Pre-fetched export (optional, auto-fetches if None).
        include_metadata: Include IDs for re-import (default True).
        format: "yaml" (default) or "json".
        ctx: MCP context for progress (optional).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with yaml_content/json_content or file_path, summary, metadata.
    """
    # Validate format parameter
    valid_formats = ("yaml", "json")
    if format.lower() not in valid_formats:
        raise ToolError(
            f"[Invalid format]: '{format}' is not valid. "
            f"[Use one of: {', '.join(valid_formats)}]"
        )
    # Report progress: starting
    if ctx:
        await ctx.report_progress(
            progress=0.1,
            total=1.0,
            message="Starting YAML transformation...",
        )

    # Get export data if not provided
    if export_data is None:
        if ctx:
            await ctx.report_progress(
                progress=0.2,
                total=1.0,
                message=f"Fetching export for service {service_id}...",
            )

        # Fetch raw export
        result = await service_export_raw(
            service_id=service_id,
            include_all=True,
            ctx=None,  # Don't double-report progress
            instance=instance,
        )

        # Handle file-based exports (large files)
        if "file_path" in result:
            # Read the export from file
            with open(result["file_path"], encoding="utf-8") as f:
                export_data = json.load(f)
        else:
            export_data = result.get("export_data", {})

    # Normalize format
    output_format = format.lower()

    if ctx:
        await ctx.report_progress(
            progress=0.5,
            total=1.0,
            message=f"Transforming to {output_format.upper()}...",
        )

    # Transform using the transformer
    transformer = YAMLTransformer(include_metadata=include_metadata)

    try:
        # Get the optimized data structure
        data_dict, transform_summary = transformer.transform_to_dict(export_data)

        # Serialize to requested format
        if output_format == "yaml":
            import yaml as yaml_module

            content = yaml_module.dump(
                data_dict,
                default_flow_style=False,
                allow_unicode=True,
                sort_keys=False,
                width=120,
            )
            content_key = "yaml_content"
            file_ext = "yaml"
        else:  # json
            content = json.dumps(data_dict, indent=2, ensure_ascii=False)
            content_key = "json_content"
            file_ext = "json"

    except TransformationError as e:
        # Save raw JSON for debugging
        raw_file_path = _save_export_to_file(export_data, str(service_id))
        raise ToolError(
            f"[Transformation failed]: {e}. [Raw JSON preserved at {raw_file_path}]"
        )

    # Calculate size
    content_size = len(content.encode("utf-8"))

    # Build result metadata
    result_metadata = {
        "transformed_at": datetime.now(UTC).isoformat(),
        "service_id": str(service_id),
        "schema_version": "1.0",
        "include_metadata": include_metadata,
        "format": output_format,
    }

    # Merge transformer summary with size info
    summary = {
        **transform_summary,
        "size_bytes": content_size,
        "size_kb": round(content_size / 1024, 2),
        "format": output_format,
    }

    # Handle large output
    if content_size >= LARGE_YAML_THRESHOLD_BYTES:
        file_path = _save_yaml_to_file(content, str(service_id), extension=file_ext)

        if ctx:
            await ctx.report_progress(
                progress=1.0,
                total=1.0,
                message=f"{output_format.upper()} saved to {file_path}",
            )

        return {
            "file_path": file_path,
            "summary": summary,
            "metadata": result_metadata,
            "message": (
                f"{output_format.upper()} saved to file ({summary['size_kb']} KB). "
                "Use the file_path to access the full content. "
                "Note: Temporary files should be cleaned up after use."
            ),
        }

    # Report completion
    if ctx:
        await ctx.report_progress(
            progress=1.0,
            total=1.0,
            message=f"{output_format.upper()} transformation complete.",
        )

    return {
        content_key: content,
        "summary": summary,
        "metadata": result_metadata,
    }


async def service_copy(
    source_service_id: str | int,
    new_name: str | None = None,
    include_forms: bool = True,
    include_roles: bool = True,
    include_determinants: bool = True,
    include_bots: bool = True,
    include_catalogs: bool = True,
    include_translations: bool = False,
    include_tutorials: bool = False,
    catalog_ids: list[str] | None = None,
    validate_roles: bool = False,
    ctx: Context | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Copy a service with a new name. Audited write operation.

    Uses two-step export+import pattern internally (BPA has no direct copy API).

    Args:
        source_service_id: Service UUID to copy.
        new_name: New service name (default: "{original} - copy").
        include_forms: Copy forms (default True).
        include_roles: Copy roles (default True).
        include_determinants: Copy determinants (default True).
        include_bots: Copy bots (default True).
        include_catalogs: Copy catalogs (default True).
        include_translations: Copy translations (default False).
        include_tutorials: Copy tutorials (default False).
        catalog_ids: Specific catalogs to copy (optional).
        validate_roles: Validate role formSchemas after copy (default False).
        ctx: MCP context for progress (optional).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with new_service_id, new_service_name, source_service_id,
        source_service_name, components_copied, components_excluded, warnings, audit_id.
    """
    # Pre-flight validation
    if not source_service_id or str(source_service_id).strip() == "":
        raise ToolError(
            "[Validation error]: source_service_id is required. "
            "[Use service_list to find available services]"
        )

    if new_name is not None and new_name.strip() == "":
        raise ToolError(
            "[Validation error]: new_name cannot be empty whitespace. "
            "[Provide a valid name or omit to use default naming]"
        )

    # Get authenticated user for audit
    try:
        user_email = get_current_user_email(instance=instance)
    except NotAuthenticatedError as e:
        raise ToolError(str(e))

    # Fetch source service to verify it exists and get original name
    if ctx:
        await ctx.report_progress(
            progress=0.1,
            total=1.0,
            message=f"Verifying source service {source_service_id}...",
        )

    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                source_service = await client.get(
                    f"/service/{source_service_id}",
                    resource_type="service",
                )
            except BPANotFoundError:
                raise ToolError(
                    f"[Service not found]: Service ID '{source_service_id}' "
                    "does not exist. [Use service_list to see available services]"
                )

            source_name = source_service.get("name", "Unknown")

            # Determine new service name
            target_name = new_name.strip() if new_name else f"{source_name} - copy"

            # Build copy payload
            copy_options = _build_copy_payload(
                new_service_name=target_name,
                include_forms=include_forms,
                include_roles=include_roles,
                include_determinants=include_determinants,
                include_bots=include_bots,
                include_catalogs=include_catalogs,
                include_translations=include_translations,
                include_tutorials=include_tutorials,
                catalog_ids=catalog_ids,
            )

            # Track what will be copied/excluded
            components_copied = ["registrations"]  # Always copied
            components_excluded = []

            if include_forms:
                components_copied.append("forms")
            else:
                components_excluded.append("forms")

            if include_roles:
                components_copied.append("roles")
            else:
                components_excluded.append("roles")

            if include_determinants:
                components_copied.append("determinants")
            else:
                components_excluded.append("determinants")

            if include_bots:
                components_copied.append("bots")
            else:
                components_excluded.append("bots")

            if include_catalogs:
                components_copied.append("catalogs")
            else:
                components_excluded.append("catalogs")

            if include_translations:
                components_copied.append("translations")
            else:
                components_excluded.append("translations")

            if include_tutorials:
                components_copied.append("tutorials")
            else:
                components_excluded.append("tutorials")

            # Create audit record BEFORE API call (audit-before-write pattern)
            audit_logger = AuditLogger(db_path=resolve_db_path(instance))
            audit_params = {
                "source_service_id": str(source_service_id),
                "source_service_name": source_name,
                "new_service_name": target_name,
                "include_forms": include_forms,
                "include_roles": include_roles,
                "include_determinants": include_determinants,
                "include_bots": include_bots,
                "include_catalogs": include_catalogs,
                "include_translations": include_translations,
                "include_tutorials": include_tutorials,
                "catalog_ids": catalog_ids,
            }
            audit_id = await audit_logger.record_pending(
                user_email=user_email,
                operation_type="copy",
                object_type="service",
                object_id=str(source_service_id),
                params=audit_params,
            )

            if ctx:
                await ctx.report_progress(
                    progress=0.2,
                    total=1.0,
                    message=f"Exporting service for copy to '{target_name}'...",
                )

            try:
                # Step 1: Export service with copy flag (prepares JSON with new name)
                export_data, _size = await client.download_service(
                    str(source_service_id),
                    options=copy_options,
                )

                if ctx:
                    await ctx.report_progress(
                        progress=0.5,
                        total=1.0,
                        message=f"Importing as new service '{target_name}'...",
                    )

                # Step 2: Import via validate_catalogs (matches frontend flow)
                import_result = await client.validate_and_import_service(export_data)
                new_service_id = import_result.get("service_id")
                new_service_name = target_name

                # Build audit result
                audit_result: dict[str, Any] = {
                    "new_service_id": new_service_id,
                    "new_service_name": new_service_name,
                    "source_service_id": str(source_service_id),
                }
                if import_result.get("had_catalogs"):
                    audit_result["catalog_count"] = import_result.get(
                        "catalog_count", 0
                    )

                # Mark audit as success
                await audit_logger.mark_success(audit_id, result=audit_result)

            except BPAClientError as e:
                # Mark audit as failed
                await audit_logger.mark_failed(audit_id, str(e))
                raise

            # Validate roles after copy (opt-in, makes N+M extra API calls)
            role_validation: dict[str, Any] | None = None
            if validate_roles:
                try:
                    role_validation = await _validate_copy_roles(
                        client, str(source_service_id), str(new_service_id)
                    )
                except Exception:
                    role_validation = None

    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=source_service_id)

    if ctx:
        await ctx.report_progress(
            progress=1.0,
            total=1.0,
            message="Service copy complete.",
        )

    # Generate warnings for potential broken references
    warnings = _generate_copy_warnings(
        include_forms=include_forms,
        include_catalogs=include_catalogs,
        include_determinants=include_determinants,
    )

    # Add role validation warnings if available
    if role_validation is not None:
        if role_validation["missing_roles"]:
            warnings.append(
                f"{len(role_validation['missing_roles'])} role(s) missing from copy"
            )
        if role_validation["empty_form_schema"]:
            n = len(role_validation["empty_form_schema"])
            warnings.append(f"{n} role(s) have empty formSchema")
    elif validate_roles:
        warnings.append("Could not validate roles after copy")

    result: dict[str, Any] = {
        "new_service_id": new_service_id,
        "new_service_name": new_service_name,
        "source_service_id": str(source_service_id),
        "source_service_name": source_name,
        "components_copied": components_copied,
        "components_excluded": components_excluded,
        "warnings": warnings,
        "audit_id": audit_id,
    }

    if role_validation is not None:
        result["role_validation"] = role_validation

    return result


def _build_copy_payload(
    new_service_name: str,
    include_forms: bool,
    include_roles: bool,
    include_determinants: bool,
    include_bots: bool,
    include_catalogs: bool,
    include_translations: bool,
    include_tutorials: bool,
    catalog_ids: list[str] | None,
) -> dict[str, Any]:
    """Build the payload for the copy service API call.

    Args:
        new_service_name: Name for the new service.
        include_forms: Whether to copy forms.
        include_roles: Whether to copy roles.
        include_determinants: Whether to copy determinants.
        include_bots: Whether to copy bots.
        include_catalogs: Whether to copy catalogs.
        include_translations: Whether to copy translations.
        include_tutorials: Whether to copy tutorials.
        catalog_ids: Specific catalog IDs to copy, or None for all.

    Returns:
        dict: The payload for the download_service API call.
    """
    payload: dict[str, Any] = {
        "serviceSelected": True,
        "copyService": True,
        "newServiceName": new_service_name,
        # Core components always copied
        "costsSelected": True,
        "requirementsSelected": True,
        "resultsSelected": True,
        "activityConditionsSelected": True,
        "registrationLawsSelected": True,
        "serviceLocationsSelected": True,
        "registrationsSelected": True,
        "printDocumentsSelected": True,
        # Configurable components
        "guideFormSelected": include_forms,
        "applicantFormSelected": include_forms,
        "sendFileFormSelected": include_forms,
        "paymentFormSelected": include_forms,
        "rolesSelected": include_roles,
        "determinantsSelected": include_determinants,
        "botsSelected": include_bots,
        "catalogsSelected": include_catalogs,
        "serviceTranslationsSelected": include_translations,
        "serviceTutorialsSelected": include_tutorials,
    }

    # Add specific catalog IDs if provided
    if include_catalogs and catalog_ids is not None:
        payload["catalogsToCopy"] = catalog_ids

    return payload


def _generate_copy_warnings(
    include_forms: bool,
    include_catalogs: bool,
    include_determinants: bool,
) -> list[str]:
    """Generate warnings about potential broken references in the copy.

    Args:
        include_forms: Whether forms were copied.
        include_catalogs: Whether catalogs were copied.
        include_determinants: Whether determinants were copied.

    Returns:
        list: Warning messages about potential issues.
    """
    warnings: list[str] = []

    # Warn about catalog references in forms
    if include_forms and not include_catalogs:
        warnings.append(
            "Forms were copied but catalogs were excluded. "
            "Catalog references in forms may be broken."
        )

    # Warn about determinant references
    if include_forms and not include_determinants:
        warnings.append(
            "Forms were copied but determinants were excluded. "
            "Field visibility rules may not work correctly."
        )

    return warnings


logger = logging.getLogger(__name__)


async def _validate_copy_roles(
    client: BPAClient,
    source_service_id: str,
    new_service_id: str,
) -> dict[str, Any]:
    """Compare roles between source and copy, identify discrepancies."""
    # Fetch role lists from both services
    source_roles = await client.get_list(
        "/service/{service_id}/role",
        path_params={"service_id": source_service_id},
        resource_type="role",
    )
    copy_roles = await client.get_list(
        "/service/{service_id}/role",
        path_params={"service_id": new_service_id},
        resource_type="role",
    )

    # Build name-to-role mapping for the copy
    copy_by_name: dict[str, dict[str, Any]] = {
        r["name"]: r for r in copy_roles if r.get("name")
    }

    missing_roles: list[dict[str, Any]] = []
    empty_form_schema: list[dict[str, Any]] = []
    roles_ok = 0

    for source_role_summary in source_roles:
        name = source_role_summary.get("name")
        if not name:
            continue

        copy_role_summary = copy_by_name.get(name)
        if copy_role_summary is None:
            missing_roles.append(
                {
                    "name": name,
                    "source_role_id": source_role_summary["id"],
                }
            )
            continue

        # Fetch full role data to see formSchema
        source_full = await client.get(
            "/role/{role_id}",
            path_params={"role_id": source_role_summary["id"]},
            resource_type="role",
            resource_id=source_role_summary["id"],
        )
        copy_full = await client.get(
            "/role/{role_id}",
            path_params={"role_id": copy_role_summary["id"]},
            resource_type="role",
            resource_id=copy_role_summary["id"],
        )

        source_schema = source_full.get("formSchema") or {}
        copy_schema = copy_full.get("formSchema") or {}
        source_components = source_schema.get("components", [])
        copy_components = copy_schema.get("components", [])

        source_count = (
            len(source_components) if isinstance(source_components, list) else 0
        )
        copy_count = len(copy_components) if isinstance(copy_components, list) else 0

        if source_count > 0 and copy_count == 0:
            empty_form_schema.append(
                {
                    "role_id": copy_role_summary["id"],
                    "name": name,
                    "source_component_count": source_count,
                    "source_role_id": source_role_summary["id"],
                }
            )
        else:
            roles_ok += 1

    return {
        "source_role_count": len(source_roles),
        "copy_role_count": len(copy_roles),
        "missing_roles": missing_roles,
        "empty_form_schema": empty_form_schema,
        "roles_ok": roles_ok,
    }


def _remap_registration_ids(
    form_schema: dict[str, Any],
    reg_id_map: dict[str, str],
) -> dict[str, Any]:
    """Recursively remap registration IDs in formSchema.

    Walks all nested components arrays. For any dict with a ``registrations``
    key that maps to a dict, replaces the keys using ``reg_id_map``.
    """
    schema = copy.deepcopy(form_schema)
    _walk_and_remap(schema, reg_id_map)
    return schema


def _walk_and_remap(node: Any, reg_id_map: dict[str, str]) -> None:
    """Walk a formSchema tree, remapping registration IDs in-place."""
    if not isinstance(node, dict):
        return

    # Remap registrations property if present
    regs = node.get("registrations")
    if isinstance(regs, dict):
        new_regs: dict[str, Any] = {}
        for key, value in regs.items():
            str_key = str(key)
            new_regs[reg_id_map.get(str_key, str_key)] = value
        node["registrations"] = new_regs

    # Recurse into components
    components = node.get("components")
    if isinstance(components, list):
        for child in components:
            _walk_and_remap(child, reg_id_map)

    # Recurse into columns
    columns = node.get("columns")
    if isinstance(columns, list):
        for col in columns:
            if isinstance(col, dict):
                col_comps = col.get("components")
                if isinstance(col_comps, list):
                    for child in col_comps:
                        _walk_and_remap(child, reg_id_map)

    # Recurse into table rows (rows[][] structure)
    rows = node.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):
                            for child in cell_comps:
                                _walk_and_remap(child, reg_id_map)


async def service_copy_repair_roles(
    source_service_id: str | int,
    target_service_id: str | int,
    role_ids: list[str] | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Repair roles with empty formSchema after service_copy. Audited write operation.

    Copies formSchema from source roles to target roles matched by name.
    Remaps registration IDs in formSchema panel registrations properties.

    Args:
        source_service_id: Original service UUID.
        target_service_id: Copied service UUID to repair.
        role_ids: Specific target role IDs to repair (default: all empty).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with repaired_count, skipped_count, repaired_roles, audit_id.
    """
    # Get authenticated user for audit
    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:
            # Step 1: Validate to find roles needing repair
            validation = await _validate_copy_roles(
                client, str(source_service_id), str(target_service_id)
            )

            candidates = validation["empty_form_schema"]

            # Filter to specific role IDs if provided
            if role_ids is not None:
                role_id_set = set(role_ids)
                candidates = [c for c in candidates if c["role_id"] in role_id_set]

            if not candidates:
                return {
                    "repaired_count": 0,
                    "skipped_count": 0,
                    "repaired_roles": [],
                    "message": "No roles need repair.",
                    "validation": validation,
                }

            # Step 2: Build registration ID mapping (source -> target)
            source_service = await client.get(
                "/service/{id}",
                path_params={"id": source_service_id},
                resource_type="service",
                resource_id=source_service_id,
            )
            target_service = await client.get(
                "/service/{id}",
                path_params={"id": target_service_id},
                resource_type="service",
                resource_id=target_service_id,
            )

            source_regs = source_service.get("registrations", [])
            target_regs = target_service.get("registrations", [])

            # Match registrations by name
            target_reg_by_name: dict[str, str] = {
                r["name"]: r["id"] for r in target_regs if r.get("name") and r.get("id")
            }
            reg_id_map: dict[str, str] = {}
            for sr in source_regs:
                s_name = sr.get("name")
                s_id = sr.get("id")
                if s_name and s_id and s_name in target_reg_by_name:
                    reg_id_map[s_id] = target_reg_by_name[s_name]

            # Step 3: Create audit record
            audit_logger = AuditLogger(db_path=resolve_db_path(instance))
            audit_id = await audit_logger.record_pending(
                user_email=user_email,
                operation_type="repair_roles",
                object_type="service",
                object_id=str(target_service_id),
                params={
                    "source_service_id": str(source_service_id),
                    "target_service_id": str(target_service_id),
                    "candidate_count": len(candidates),
                    "role_ids_filter": role_ids,
                },
            )

            # Step 4: Repair each role
            repaired_roles: list[dict[str, Any]] = []
            skipped_count = 0

            try:
                for candidate in candidates:
                    source_role_id = candidate["source_role_id"]
                    target_role_id = candidate["role_id"]

                    # Fetch source role full data
                    source_role = await client.get(
                        "/role/{role_id}",
                        path_params={"role_id": source_role_id},
                        resource_type="role",
                        resource_id=source_role_id,
                    )

                    source_form_schema = source_role.get("formSchema")
                    if not source_form_schema:
                        skipped_count += 1
                        continue

                    # Remap registration IDs
                    remapped_schema = _remap_registration_ids(
                        source_form_schema, reg_id_map
                    )

                    # Fetch target role to get current state for full PUT
                    target_role = await client.get(
                        "/role/{role_id}",
                        path_params={"role_id": target_role_id},
                        resource_type="role",
                        resource_id=target_role_id,
                    )

                    # Build full PUT payload (BPA requires full object)
                    put_payload = {
                        "id": target_role.get("id"),
                        "name": target_role.get("name"),
                        "shortName": target_role.get("shortName"),
                        "assignedTo": target_role.get("assignedTo"),
                        "description": target_role.get("description"),
                        "serviceId": target_role.get("serviceId"),
                        "startRole": target_role.get("startRole", False),
                        "visibleForApplicant": target_role.get(
                            "visibleForApplicant", True
                        ),
                        "sortOrderNumber": target_role.get("sortOrderNumber", 0),
                        "allowToConfirmPayments": target_role.get(
                            "allowToConfirmPayments", False
                        ),
                        "allowAccessToFinancialReports": target_role.get(
                            "allowAccessToFinancialReports", False
                        ),
                        "formSchema": remapped_schema,
                        "jsonDeterminants": target_role.get("jsonDeterminants"),
                    }

                    await client.put(
                        "/role",
                        json=put_payload,
                        resource_type="role",
                        resource_id=target_role_id,
                    )

                    repaired_roles.append(
                        {
                            "role_id": target_role_id,
                            "name": candidate["name"],
                            "source_role_id": source_role_id,
                            "component_count": candidate["source_component_count"],
                            "registrations_remapped": len(reg_id_map),
                        }
                    )

                await audit_logger.mark_success(
                    audit_id,
                    result={
                        "repaired_count": len(repaired_roles),
                        "skipped_count": skipped_count,
                        "repaired_role_ids": [r["role_id"] for r in repaired_roles],
                    },
                )

            except Exception as e:
                await audit_logger.mark_failed(audit_id, str(e))
                raise

    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=target_service_id)

    return {
        "repaired_count": len(repaired_roles),
        "skipped_count": skipped_count,
        "repaired_roles": repaired_roles,
        "audit_id": audit_id,
    }


def register_export_tools(mcp: Any) -> None:
    """Register export tools with the MCP server.

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

    # Read operations (export/transform, no mutation)
    mcp.tool(annotations=READ)(service_export_raw)
    mcp.tool(annotations=READ)(service_to_yaml)
    # Write operations (creates a new service)
    mcp.tool(annotations=WRITE)(service_copy)
    mcp.tool(annotations=WRITE)(service_copy_repair_roles)
