"""Component behaviour tools for BPA API.

This module provides MCP tools for managing component behaviours
in the BPA API. Component behaviours link determinants to form
components via effects that control component visibility, activation,
and other properties.

The BPA API does NOT have a direct endpoint to list all component
behaviours for a service. This module extracts behaviours from the
service export endpoint.

API Endpoints referenced:
- POST /download_service/{service_id} - Export service with componentBehaviours
- GET /service/{service_id}/behaviour/{component_key} - Get behaviour by component
- GET /behaviour/{id} - Get behaviour by ID
- POST /service/{service_id}/behaviour - Create component behaviour

Architecture (from bpa-determinants-research.md)::

    Form Component
        -> behaviourId -> componentBehaviours[id]
                            -> effects[]
                                -> jsonDeterminants (JSONLogic expression)
                                -> propertyEffects[]
                                    -> name: show|hide|enable|disable|etc.
                                    -> value: "true"|"false"
"""

from __future__ import annotations

import json
import logging
from typing import 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,
    BPANotFoundError,
    translate_error,
)
from mcp_eregistrations_bpa.config import resolve_db_path
from mcp_eregistrations_bpa.tools._determinant_shared import build_determinant_refs
from mcp_eregistrations_bpa.tools._form_shared import parse_form_schema
from mcp_eregistrations_bpa.tools.formio_helpers import find_component
from mcp_eregistrations_bpa.tools.forms import FORM_TYPES
from mcp_eregistrations_bpa.tools.large_response import large_response_handler

logger = logging.getLogger(__name__)

__all__ = [
    "componentbehaviour_list",
    "componentbehaviour_get",
    "componentbehaviour_get_by_component",
    "componentbehaviour_repair_metadata",
    "effect_create",
    "effect_delete",
    "register_behaviour_tools",
]

# Valid effect types for property effects
VALID_EFFECT_TYPES = ["activate", "deactivate", "show", "hide", "enable", "disable"]

# Valid logic operators for combining determinants
VALID_LOGIC_OPERATORS = ["AND", "OR"]


@large_response_handler(
    threshold_bytes=50 * 1024,  # 50KB threshold for list tools
    navigation={
        "list_all": "jq '.behaviours'",
        "find_by_key": "jq '.behaviours[] | select(.component_key==\"x\")'",
        "with_effects": "jq '.behaviours[] | select(.effect_count > 0)'",
    },
)
async def componentbehaviour_list(
    service_id: str | int, instance: str | None = None
) -> dict[str, Any]:
    """List component behaviours for a service.

    Extracts behaviours from service export (no direct BPA endpoint exists).
    Large responses (>50KB) are saved to file with navigation hints.

    Args:
        service_id: Service ID to list behaviours for.
        instance: Instance profile name (from instance_list).

    Returns:
        dict with behaviours (id, component_key, effect_count), total, service_id.
    """
    # Validate service_id
    if not service_id or (isinstance(service_id, str) and not service_id.strip()):
        raise ToolError(
            "'service_id' is required. Use 'service_list' to see available services."
        )

    try:
        async with BPAClient.for_instance(instance) as client:
            # First verify service exists
            try:
                await client.get(
                    "/service/{id}",
                    path_params={"id": service_id},
                    resource_type="service",
                    resource_id=service_id,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Service '{service_id}' not found. "
                    "Use 'service_list' to see available services."
                )

            # Fetch service export to get componentBehaviours
            # Use minimal export options - only need behaviours data
            export_options = {
                "serviceSelected": True,
                "registrationsSelected": True,
                "determinantsSelected": True,
                "guideFormSelected": True,
                "applicantFormSelected": True,
                "sendFileFormSelected": True,
                "paymentFormSelected": True,
                "costsSelected": False,
                "requirementsSelected": False,
                "resultsSelected": False,
                "activityConditionsSelected": False,
                "registrationLawsSelected": False,
                "serviceLocationsSelected": False,
                "serviceTutorialsSelected": False,
                "serviceTranslationsSelected": False,
                "catalogsSelected": False,
                "rolesSelected": False,
                "printDocumentsSelected": False,
                "botsSelected": False,
                "copyService": False,
            }

            try:
                export_data, _ = await client.download_service(
                    str(service_id),
                    options=export_options,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Service '{service_id}' not found. "
                    "Use 'service_list' to see available services."
                )

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

    # Extract behaviours from export data
    behaviours = _extract_behaviours_from_export(export_data)

    return {
        "behaviours": behaviours,
        "total": len(behaviours),
        "service_id": str(service_id),
    }


async def componentbehaviour_get(
    behaviour_id: str, instance: str | None = None
) -> dict[str, Any]:
    """Get behaviour configuration with parsed JSONLogic determinants.

    Args:
        behaviour_id: Behaviour UUID.
        instance: Instance profile name (from instance_list).

    Returns:
        dict with id, component_key, service_id, effects (id, determinants,
        property_effects).
    """
    # Validate behaviour_id
    if not behaviour_id or (isinstance(behaviour_id, str) and not behaviour_id.strip()):
        raise ToolError(
            "'behaviour_id' is required. "
            "Use 'componentbehaviour_list' or 'form_component_get' "
            "to find behaviour IDs."
        )

    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                data = await client.get(
                    "/behaviour/{id}",
                    path_params={"id": behaviour_id},
                    resource_type="behaviour",
                    resource_id=behaviour_id,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Behaviour '{behaviour_id}' not found. "
                    "Use 'componentbehaviour_list' or 'form_component_get' "
                    "to find behaviour IDs."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="behaviour", resource_id=behaviour_id)

    return _transform_behaviour(data)


def _extract_behaviours_from_export(
    export_data: dict[str, Any],
) -> list[dict[str, Any]]:
    """Extract and transform component behaviours from service export.

    Args:
        export_data: Raw service export data from BPA API.

    Returns:
        list: List of behaviour summaries with id, component_key, effect_count.
    """
    behaviours: list[dict[str, Any]] = []

    # Handle nested service structure (live API wraps in 'service' key)
    data = export_data
    if "service" in export_data and isinstance(export_data["service"], dict):
        data = export_data["service"]

    # Get componentBehaviours array
    component_behaviours = data.get("componentBehaviours", [])

    if not isinstance(component_behaviours, list):
        return behaviours

    for behaviour in component_behaviours:
        if not isinstance(behaviour, dict):
            continue

        behaviour_id = behaviour.get("id")
        component_key = behaviour.get("componentKey")
        effects = behaviour.get("effects", [])

        # Count effects
        effect_count = len(effects) if isinstance(effects, list) else 0

        behaviours.append(
            {
                "id": behaviour_id,
                "component_key": component_key,
                "effect_count": effect_count,
            }
        )

    return behaviours


async def componentbehaviour_get_by_component(
    service_id: str | int,
    component_key: str,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get behaviour configuration for a component with parsed JSONLogic.

    Args:
        service_id: Service containing the component.
        component_key: Form component key.
        instance: Instance profile name (from instance_list).

    Returns:
        dict with id, component_key, service_id, effects (id, determinants,
        property_effects).
    """
    # Validate parameters
    if not service_id or (isinstance(service_id, str) and not service_id.strip()):
        raise ToolError(
            "'service_id' is required. Use 'service_list' to see available services."
        )

    comp_key_empty = isinstance(component_key, str) and not component_key.strip()
    if not component_key or comp_key_empty:
        raise ToolError(
            "'component_key' is required. Use 'form_get' to see component keys."
        )

    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                data = await client.get(
                    "/service/{service_id}/behaviour/{component_key}",
                    path_params={
                        "service_id": service_id,
                        "component_key": component_key,
                    },
                    resource_type="behaviour",
                )
            except BPANotFoundError:
                raise ToolError(
                    f"No behaviour found for component '{component_key}' "
                    f"in service '{service_id}'. The component may not have "
                    "conditional logic configured."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="behaviour")

    # Pass service_id to transformer for context (AC1 requirement)
    return _transform_behaviour(data, service_id=service_id)


def _parse_jsonlogic_condition(condition: dict[str, Any]) -> dict[str, Any]:
    """Parse a single JSONLogic condition into readable format.

    Converts conditions like:
        {"==": [{"var": "data.field"}, value]}
    into:
        {"field": "data.field", "operator": "==", "value": value}

    Args:
        condition: A JSONLogic condition object.

    Returns:
        dict: Parsed condition with field, operator, value.
    """
    if not isinstance(condition, dict):
        return {"raw": condition}

    # Check for and/or logic operators (nested conditions)
    for logic_op in ["and", "or"]:
        if logic_op in condition:
            nested = condition[logic_op]
            if isinstance(nested, list):
                parsed_conditions = [_parse_jsonlogic_condition(c) for c in nested]
                return {
                    "logic": logic_op,
                    "conditions": parsed_conditions,
                }
            return {"logic": logic_op, "raw": nested}

    # Check for comparison operators
    comparison_ops = ["==", "!=", ">", "<", ">=", "<=", "in", "===", "!=="]
    for op in comparison_ops:
        if op in condition:
            args = condition[op]
            if isinstance(args, list) and len(args) >= 2:
                # First arg should be {"var": "data.field"}
                var_ref = args[0]
                value = args[1]

                field = None
                if isinstance(var_ref, dict) and "var" in var_ref:
                    field = var_ref["var"]
                elif isinstance(value, dict) and "var" in value:
                    # Swap if var is second
                    field = value["var"]
                    value = var_ref

                if field:
                    return {
                        "field": field,
                        "operator": op,
                        "value": value,
                    }
            return {"operator": op, "raw": args}

    # Check for unary operators (!, !!)
    if "!" in condition:
        arg = condition["!"]
        if isinstance(arg, list) and len(arg) == 1:
            arg = arg[0]
        if isinstance(arg, dict) and "var" in arg:
            return {
                "field": arg["var"],
                "operator": "isEmpty",
                "value": True,
            }
        return {"operator": "not", "raw": arg}

    if "!!" in condition:
        arg = condition["!!"]
        if isinstance(arg, list) and len(arg) == 1:
            arg = arg[0]
        if isinstance(arg, dict) and "var" in arg:
            return {
                "field": arg["var"],
                "operator": "isNotEmpty",
                "value": True,
            }
        return {"operator": "truthy", "raw": arg}

    # Check for 'if' conditions
    if "if" in condition:
        return {"operator": "if", "raw": condition["if"]}

    # Unknown structure - return as raw
    return {"raw": condition}


def _parse_jsonlogic_expression(expr: Any) -> list[dict[str, Any]]:
    """Parse a JSONLogic expression (which may be an array) into readable format.

    The jsonDeterminants field is typically a JSON string containing an array
    of JSONLogic expressions, e.g.:
        '[{"and": [{"==": [{"var": "data.field"}, true]}, ...]}]'

    Args:
        expr: Parsed JSON (list or dict) from jsonDeterminants.

    Returns:
        list: List of parsed determinant conditions.
    """
    if expr is None:
        return []

    # If it's a list, parse each element
    if isinstance(expr, list):
        return [_parse_jsonlogic_condition(item) for item in expr]

    # If it's a single dict, wrap in list
    if isinstance(expr, dict):
        return [_parse_jsonlogic_condition(expr)]

    # Return as raw for unexpected types
    return [{"raw": expr}]


def _parse_json_determinants(
    json_determinants: str | list[dict[str, Any]] | dict[str, Any] | None,
) -> list[dict[str, Any]]:
    """Parse jsonDeterminants field into readable structure.

    Handles:
    - String: Parse JSON first, then parse JSONLogic
    - List: Parse each JSONLogic expression
    - Dict: Parse single JSONLogic expression
    - None: Return empty list

    Args:
        json_determinants: Raw jsonDeterminants value from API.

    Returns:
        list: Parsed determinants with readable structure.
    """
    if json_determinants is None:
        return []

    # Parse string to JSON first
    if isinstance(json_determinants, str):
        if not json_determinants.strip():
            return []
        try:
            parsed = json.loads(json_determinants)
            return _parse_jsonlogic_expression(parsed)
        except json.JSONDecodeError:
            # Return raw string as fallback
            return [{"raw": json_determinants}]

    # Already parsed - process directly
    return _parse_jsonlogic_expression(json_determinants)


def _transform_behaviour(
    data: dict[str, Any], service_id: str | int | None = None
) -> dict[str, Any]:
    """Transform behaviour API response to snake_case format with readable JSONLogic.

    Args:
        data: Raw API response with camelCase keys.
        service_id: Optional service ID to include in response for context.
            If not provided, will attempt to extract from API response.

    Returns:
        dict: Transformed response with:
            - id: Behaviour UUID
            - component_key: Form component key
            - service_id: Service ID for context (if available)
            - effects: List with id, determinants (readable format), property_effects
    """
    effects: list[dict[str, Any]] = []

    raw_effects = data.get("effects", [])
    if isinstance(raw_effects, list):
        for effect in raw_effects:
            if not isinstance(effect, dict):
                continue

            # Parse jsonDeterminants into readable format
            json_determinants = effect.get("jsonDeterminants")
            parsed_determinants = _parse_json_determinants(json_determinants)

            # Transform property effects
            property_effects = []
            for pe in effect.get("propertyEffects", []):
                if isinstance(pe, dict):
                    property_effects.append(
                        {
                            "name": pe.get("name"),
                            "value": pe.get("value"),
                        }
                    )

            effects.append(
                {
                    "id": effect.get("id"),
                    "determinants": parsed_determinants,
                    "property_effects": property_effects,
                }
            )

    # Determine service_id: use provided value, or extract from API response
    resolved_service_id = service_id
    if resolved_service_id is None:
        # Try to extract from API response (may be serviceId or service_id)
        resolved_service_id = data.get("serviceId") or data.get("service_id")

    return {
        "id": data.get("id"),
        "component_key": data.get("componentKey"),
        "service_id": str(resolved_service_id) if resolved_service_id else None,
        "effects": effects,
    }


def _build_property_effects(
    effect_type: str, effect_value: bool
) -> list[dict[str, str]]:
    """Build propertyEffects array matching the frontend data contract.

    Returns 3 properties matching what the UI sends (activate, show, disabled).
    """
    # Semantic defaults from UI: activate=true, show=true, disabled=false
    activate = "true"
    show = "true"
    disabled = "false"

    if effect_type == "activate":
        activate = "true" if effect_value else "false"
    elif effect_type == "deactivate":
        activate = "false" if effect_value else "true"
    elif effect_type == "show":
        show = "true" if effect_value else "false"
    elif effect_type == "hide":
        show = "false" if effect_value else "true"
    elif effect_type == "enable":
        disabled = "false" if effect_value else "true"
    elif effect_type == "disable":
        disabled = "true" if effect_value else "false"

    return [
        {"name": "activate", "type": "boolean", "value": activate},
        {"name": "show", "type": "boolean", "value": show},
        {"name": "disabled", "type": "boolean", "value": disabled},
    ]


def _validate_effect_create_params(
    service_id: str | int,
    component_key: str,
    determinant_ids: list[str],
    logic: str,
    effect_type: str,
    effect_value: bool,
) -> None:
    """Validate effect_create parameters (pre-flight validation).

    Args:
        service_id: Service ID (required).
        component_key: Form component key (required).
        determinant_ids: List of determinant IDs (required, non-empty).
        logic: Logic operator (AND or OR).
        effect_type: Effect type (activate, deactivate, show, hide, enable, disable).
        effect_value: Boolean value for the effect.

    Raises:
        ToolError: If validation fails.
    """
    errors = []

    if not service_id or (isinstance(service_id, str) and not service_id.strip()):
        errors.append("'service_id' is required")

    if not component_key or (
        isinstance(component_key, str) and not component_key.strip()
    ):
        errors.append("'component_key' is required")

    if not determinant_ids:
        errors.append("'determinant_ids' must contain at least one determinant ID")
    elif not isinstance(determinant_ids, list):
        errors.append("'determinant_ids' must be a list of determinant IDs")
    elif len(determinant_ids) == 0:
        errors.append("'determinant_ids' must contain at least one determinant ID")

    if logic and logic.upper() not in VALID_LOGIC_OPERATORS:
        errors.append(f"'logic' must be one of: {', '.join(VALID_LOGIC_OPERATORS)}")

    if effect_type and effect_type.lower() not in VALID_EFFECT_TYPES:
        errors.append(f"'effect_type' must be one of: {', '.join(VALID_EFFECT_TYPES)}")

    if not isinstance(effect_value, bool):
        errors.append("'effect_value' must be a boolean (true or false)")

    if errors:
        error_msg = "; ".join(errors)
        raise ToolError(
            f"Cannot create effect: {error_msg}. "
            "Provide valid parameters for effect creation."
        )


async def _fetch_determinant_details(
    client: BPAClient, determinant_id: str
) -> dict[str, Any]:
    """Fetch determinant details from BPA API.

    Args:
        client: BPA client instance.
        determinant_id: ID of the determinant to fetch.

    Returns:
        dict: Determinant data from API.

    Raises:
        ToolError: If determinant not found.
    """
    try:
        return await client.get(
            "/determinant/{id}",
            path_params={"id": determinant_id},
            resource_type="determinant",
            resource_id=determinant_id,
        )
    except BPANotFoundError:
        raise ToolError(
            f"Determinant '{determinant_id}' not found. "
            "Use 'determinant_list' with service_id to see available determinants."
        )


async def _get_existing_behaviour(
    client: BPAClient, service_id: str | int, component_key: str
) -> dict[str, Any] | None:
    """Check if a behaviour already exists for a component.

    Args:
        client: BPA client instance.
        service_id: Service ID.
        component_key: Form component key.

    Returns:
        dict: Existing behaviour data, or None if not found.
    """
    try:
        return await client.get(
            "/service/{service_id}/behaviour/{component_key}",
            path_params={
                "service_id": service_id,
                "component_key": component_key,
            },
            resource_type="behaviour",
        )
    except BPANotFoundError:
        return None


async def _sync_behaviour_metadata_to_form(
    client: Any,
    service_id: str | int,
    component_key: str,
    behaviour_id: str | None,
    effects_ids: list[str],
    form_type: str = "applicant",
) -> bool:
    """Sync behaviourId and effectsIds to form component.

    The BPA frontend updates these properties via postMessage after saving
    behaviours. MCP must do this explicitly so the form builder shows
    the correct behaviour state.

    Best-effort: returns False on any error without raising.
    """
    try:
        form_config = FORM_TYPES.get(form_type)
        if not form_config:
            return False

        form_data = await client.get(
            form_config["get_endpoint"],
            path_params={"id": service_id},
            params={"reusable": "false"},
            resource_type="form",
        )
        form_schema = parse_form_schema(form_data)
        components = form_schema.get("components", [])
        comp_result = find_component(components, component_key)
        if comp_result is None:
            return False

        comp, _ = comp_result
        comp["behaviourId"] = behaviour_id or ""
        comp["effectsIds"] = effects_ids
        form_data["formSchema"] = form_schema
        await client.put(
            form_config["put_endpoint"],
            path_params={"form_id": form_data["id"]},
            json=form_data,
            resource_type="form",
        )
        return True
    except Exception:
        logger.debug(
            "Best-effort behaviour metadata sync failed for %s/%s",
            service_id,
            component_key,
            exc_info=True,
        )
        return False


async def _sync_behaviour_metadata_to_role_form(
    client: Any,
    role_id: str,
    component_key: str,
    behaviour_id: str | None,
    effects_ids: list[str],
) -> bool:
    """Sync behaviourId and effectsIds to a role form component.

    Role forms have independent formSchema stored on the Role entity,
    separate from the applicant form. The frontend reads effectsIds from
    the role form schema to display effect counters and hinting icons.

    Best-effort: returns False on any error without raising.
    """
    try:
        role_data = await client.get(
            "/role/{role_id}",
            path_params={"role_id": role_id},
            resource_type="role",
            resource_id=role_id,
        )

        form_schema = parse_form_schema(role_data)
        components = form_schema.get("components", [])
        if not components:
            return False

        comp_result = find_component(components, component_key)
        if comp_result is None:
            return False

        comp, _ = comp_result
        comp["behaviourId"] = behaviour_id or ""
        comp["effectsIds"] = effects_ids

        # Round-trip: PUT the role object back (matches applicant form pattern)
        role_data["formSchema"] = form_schema
        await client.put(
            "/role",
            json=role_data,
            resource_type="role",
            resource_id=role_data.get("id", role_id),
        )
        return True
    except Exception:
        logger.debug(
            "Best-effort role form metadata sync failed for role %s / %s",
            role_id,
            component_key,
            exc_info=True,
        )
        return False


async def effect_create(
    service_id: str | int,
    component_key: str,
    determinant_ids: list[str],
    logic: str = "AND",
    effect_type: str = "activate",
    effect_value: bool = True,
    role_id: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create effect linking determinants to a component. Audited write operation.

    Args:
        service_id: Parent service ID.
        component_key: Form component key to apply effect to.
        determinant_ids: Determinant IDs to combine (at least one).
        logic: "AND" (all match) or "OR" (any match). Default: "AND".
        effect_type: activate/deactivate/show/hide/enable/disable (default: activate).
        effect_value: Boolean value for effect (default: True).
        role_id: Role ID when component is in a role form (syncs effectsIds to role).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with behaviour_id, effect_id, component_key, determinant_count,
        effect_type, effect_value, logic, service_id, audit_id.
    """
    # Pre-flight validation (no audit record for validation failures)
    _validate_effect_create_params(
        service_id, component_key, determinant_ids, logic, effect_type, effect_value
    )

    # Normalize parameters
    logic = logic.upper()
    effect_type = effect_type.lower()

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

    # Use single BPAClient connection for all operations
    try:
        async with BPAClient.for_instance(instance) as client:
            # Verify parent service exists before creating audit record
            try:
                await client.get(
                    "/service/{id}",
                    path_params={"id": service_id},
                    resource_type="service",
                    resource_id=service_id,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Cannot create effect: Service '{service_id}' not found. "
                    "Use 'service_list' to see available services."
                )

            # Validate all determinants exist (fail fast before creating audit)
            for det_id in determinant_ids:
                await _fetch_determinant_details(client, det_id)

            # Build determinant reference JSON (UI format, not inline JSONLogic)
            json_determinants = build_determinant_refs(determinant_ids, logic)

            # Build property effects following UI visibility hierarchy
            # (from BPA-frontend determinant-triggers-field.component.html)
            #
            # UI semantic hierarchy (each level only visible if parent is true):
            #   Level 1: activate (always visible) - default=true
            #   Level 2: show (only if activate=true) - default=true
            #   Level 3: disabled (only if activate=true AND show=true) - default=false

            # Map effect_type to the 3 UI properties
            property_effects = _build_property_effects(effect_type, effect_value)

            # Build the new effect
            new_effect = {
                "jsonDeterminants": json_determinants,
                "propertyEffects": property_effects,
            }

            # Check if behaviour already exists for this component
            existing_behaviour = await _get_existing_behaviour(
                client, service_id, component_key
            )

            # Create audit record BEFORE API call (audit-before-write pattern)
            audit_logger = AuditLogger(db_path=resolve_db_path(instance))
            audit_id = await audit_logger.record_pending(
                user_email=user_email,
                operation_type="create",
                object_type="effect",
                params={
                    "service_id": str(service_id),
                    "component_key": component_key,
                    "determinant_ids": determinant_ids,
                    "logic": logic,
                    "effect_type": effect_type,
                    "effect_value": effect_value,
                    "existing_behaviour_id": existing_behaviour.get("id")
                    if existing_behaviour
                    else None,
                },
            )

            try:
                if existing_behaviour:
                    # Add effect to existing behaviour
                    existing_effects = existing_behaviour.get("effects", [])
                    existing_effects.append(new_effect)

                    payload = {
                        "id": existing_behaviour.get("id"),
                        "componentKey": component_key,
                        "effects": existing_effects,
                    }

                    # Update via PUT /behaviour
                    result = await client.put(
                        "/behaviour",
                        json=payload,
                        resource_type="behaviour",
                        resource_id=existing_behaviour.get("id"),
                    )

                    behaviour_id = existing_behaviour.get("id")
                else:
                    # Create new behaviour
                    payload = {
                        "componentKey": component_key,
                        "effects": [new_effect],
                    }

                    result = await client.post(
                        "/service/{service_id}/behaviour",
                        path_params={"service_id": service_id},
                        json=payload,
                        resource_type="behaviour",
                    )

                    behaviour_id = result.get("id")

                # Extract effect ID from result
                effects = result.get("effects", [])
                effect_id = None
                if effects:
                    # The new effect should be the last one
                    effect_id = effects[-1].get("id")

                # Save rollback state
                await audit_logger.save_rollback_state(
                    audit_id=audit_id,
                    object_type="effect",
                    object_id=str(effect_id) if effect_id else str(behaviour_id),
                    previous_state={
                        "behaviour_id": behaviour_id,
                        "effect_id": effect_id,
                        "component_key": component_key,
                        "service_id": str(service_id),
                        "was_new_behaviour": existing_behaviour is None,
                        "_operation": "create",
                    },
                )

                # Sync behaviour metadata to form component (best-effort)
                all_effect_ids = [e.get("id") for e in effects if e.get("id")]
                if role_id:
                    metadata_synced = await _sync_behaviour_metadata_to_role_form(
                        client,
                        role_id,
                        component_key,
                        behaviour_id,
                        all_effect_ids,
                    )
                else:
                    metadata_synced = await _sync_behaviour_metadata_to_form(
                        client,
                        service_id,
                        component_key,
                        behaviour_id,
                        all_effect_ids,
                    )

                # Mark audit as success
                await audit_logger.mark_success(
                    audit_id,
                    result={
                        "behaviour_id": behaviour_id,
                        "effect_id": effect_id,
                        "component_key": component_key,
                        "determinant_count": len(determinant_ids),
                    },
                )

                return {
                    "behaviour_id": behaviour_id,
                    "effect_id": effect_id,
                    "component_key": component_key,
                    "determinant_count": len(determinant_ids),
                    "effect_type": effect_type,
                    "effect_value": effect_value,
                    "logic": logic,
                    "service_id": str(service_id),
                    "audit_id": audit_id,
                    "metadata_synced": metadata_synced,
                }

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

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


def _validate_effect_delete_params(
    behaviour_id: str,
    service_id: str | int,
    form_type: str = "applicant",
) -> None:
    """Validate effect_delete parameters (pre-flight validation).

    Args:
        behaviour_id: Behaviour ID to delete (required).
        service_id: Service containing the component (required).
        form_type: Form type (must be a valid key in FORM_TYPES).

    Raises:
        ToolError: If validation fails.
    """
    errors = []

    if not behaviour_id or (isinstance(behaviour_id, str) and not behaviour_id.strip()):
        errors.append(
            "'behaviour_id' is required. "
            "Use 'componentbehaviour_list' or 'componentbehaviour_get' "
            "to see available behaviours"
        )

    if not service_id or (isinstance(service_id, str) and not service_id.strip()):
        errors.append("'service_id' is required")

    if form_type not in FORM_TYPES:
        valid_types = ", ".join(sorted(FORM_TYPES.keys()))
        errors.append(f"'form_type' must be one of: {valid_types}")

    if errors:
        raise ToolError("; ".join(errors) + ".")


async def effect_delete(
    behaviour_id: str,
    service_id: str | int,
    form_type: str = "applicant",
    role_id: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Delete a behaviour/effect from a component. Audited write operation.

    Clears stale behaviourId from the form component after deletion (best-effort).

    Args:
        behaviour_id: Behaviour UUID to delete.
        service_id: Service containing the component.
        form_type: Form type (applicant, guide, send_file, payment; default: applicant).
        role_id: Role ID when component is in a role form (clears effectsIds from role).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with deleted (bool), behaviour_id, deleted_behaviour,
        metadata_cleared, audit_id.
    """
    # Pre-flight validation (no audit record for validation failures)
    _validate_effect_delete_params(behaviour_id, service_id, form_type)

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

    # Use single BPAClient connection for all operations
    try:
        async with BPAClient.for_instance(instance) as client:
            # Capture current state for rollback BEFORE making changes
            try:
                previous_state = await client.get(
                    "/behaviour/{id}",
                    path_params={"id": behaviour_id},
                    resource_type="behaviour",
                    resource_id=behaviour_id,
                )
            except BPANotFoundError:
                raise ToolError(
                    f"[BEHAVIOUR_NOT_FOUND] Behaviour '{behaviour_id}' not found. "
                    "Use 'componentbehaviour_list' or 'componentbehaviour_get' "
                    "to see available behaviours."
                )

            # Create audit record BEFORE API call (audit-before-write pattern)
            audit_logger = AuditLogger(db_path=resolve_db_path(instance))
            audit_id = await audit_logger.record_pending(
                user_email=user_email,
                operation_type="delete",
                object_type="behaviour",
                object_id=str(behaviour_id),
                params={
                    "service_id": str(service_id),
                    "form_type": form_type,
                },
            )

            # Save rollback state for undo capability
            await audit_logger.save_rollback_state(
                audit_id=audit_id,
                object_type="behaviour",
                object_id=str(behaviour_id),
                previous_state=previous_state,  # Full state for recreation
            )

            try:
                await client.delete(
                    "/behaviour/{behaviour_id}",
                    path_params={"behaviour_id": behaviour_id},
                    resource_type="behaviour",
                    resource_id=behaviour_id,
                )

                # Clear stale behaviourId and effectsIds from component
                component_key = previous_state.get("componentKey")
                metadata_cleared = False
                if component_key:
                    if role_id:
                        metadata_cleared = await _sync_behaviour_metadata_to_role_form(
                            client,
                            role_id,
                            component_key,
                            behaviour_id=None,
                            effects_ids=[],
                        )
                    else:
                        metadata_cleared = await _sync_behaviour_metadata_to_form(
                            client,
                            service_id,
                            component_key,
                            behaviour_id=None,
                            effects_ids=[],
                            form_type=form_type,
                        )

                # Mark audit as success
                await audit_logger.mark_success(
                    audit_id,
                    result={
                        "deleted": True,
                        "behaviour_id": str(behaviour_id),
                        "metadata_cleared": metadata_cleared,
                    },
                )

                return {
                    "deleted": True,
                    "behaviour_id": str(behaviour_id),
                    "deleted_behaviour": {
                        "id": previous_state.get("id"),
                        "component_key": previous_state.get("componentKey"),
                        "effect_count": len(previous_state.get("effects", [])),
                    },
                    "metadata_cleared": metadata_cleared,
                    "audit_id": audit_id,
                }

            except BPAClientError as e:
                # Mark audit as failed
                await audit_logger.mark_failed(audit_id, str(e))
                raise translate_error(
                    e, resource_type="behaviour", resource_id=behaviour_id
                )

    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="behaviour", resource_id=behaviour_id)


async def componentbehaviour_repair_metadata(
    service_id: str | int,
    dry_run: bool = True,
    instance: str | None = None,
) -> dict[str, Any]:
    """Repair missing effectsIds/behaviourId on form components.

    Audited write operation.

    Scans all forms (applicant + roles) for components that have behaviours
    in the backend but missing metadata in the form schema. The frontend
    needs effectsIds to display effect counters and hinting icons.

    Args:
        service_id: Service ID to repair.
        dry_run: If True, report issues without fixing (default: True).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with scanned, issues_found, fixed (if not dry_run), issues list.
    """
    if not service_id or (isinstance(service_id, str) and not service_id.strip()):
        raise ToolError(
            "'service_id' is required. Use 'service_list' to see available services."
        )

    if not dry_run:
        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:
            # Export for behaviours + applicant form (roles have no ID in export)
            export_options = {
                "serviceSelected": True,
                "registrationsSelected": True,
                "determinantsSelected": True,
                "guideFormSelected": True,
                "applicantFormSelected": True,
                "sendFileFormSelected": True,
                "paymentFormSelected": True,
                "rolesSelected": False,  # Export roles have no ID — fetch separately
                "costsSelected": False,
                "requirementsSelected": False,
                "resultsSelected": False,
                "activityConditionsSelected": False,
                "registrationLawsSelected": False,
                "serviceLocationsSelected": False,
                "serviceTutorialsSelected": False,
                "serviceTranslationsSelected": False,
                "catalogsSelected": False,
                "printDocumentsSelected": False,
                "botsSelected": False,
                "copyService": False,
            }

            try:
                export_data, _ = await client.download_service(
                    str(service_id), options=export_options
                )
            except BPANotFoundError:
                raise ToolError(
                    f"Service '{service_id}' not found. "
                    "Use 'service_list' to see available services."
                )

            # Unwrap nested service structure
            data = export_data
            if "service" in export_data and isinstance(export_data["service"], dict):
                data = export_data["service"]

            # Build behaviour map: component_key -> {behaviour_id, effect_ids}
            behaviour_map: dict[str, dict[str, Any]] = {}
            for beh in data.get("componentBehaviours", []):
                if not isinstance(beh, dict):
                    continue
                comp_key = beh.get("componentKey")
                effects = beh.get("effects", [])
                if comp_key and effects:
                    effect_ids = [
                        e.get("id")
                        for e in effects
                        if isinstance(e, dict) and e.get("id")
                    ]
                    if effect_ids:
                        behaviour_map[comp_key] = {
                            "behaviour_id": beh.get("id"),
                            "effect_ids": effect_ids,
                        }

            issues: list[dict[str, Any]] = []
            forms_to_save: dict[str, Any] = {}

            # --- Applicant form ---
            applicant_form = data.get("applicantForm")
            if isinstance(applicant_form, dict):
                schema = parse_form_schema(applicant_form)
                _scan_form_components(schema, behaviour_map, "applicant", None, issues)
                if not dry_run and any(i["form_label"] == "applicant" for i in issues):
                    _apply_fixes_to_schema(schema, behaviour_map)
                    applicant_form["formSchema"] = schema
                    forms_to_save["applicant"] = {
                        "type": "form",
                        "endpoint": "/applicant-form/{form_id}",
                        "form_id": applicant_form.get("id"),
                        "payload": applicant_form,
                    }

            # --- Role forms (fetched individually — export has no role IDs) ---
            role_list = await client.get_list(
                "/service/{service_id}/role",
                path_params={"service_id": service_id},
                resource_type="role",
            )
            for role_summary in role_list:
                if not isinstance(role_summary, dict):
                    continue
                role_id = role_summary.get("id")
                role_name = role_summary.get("name", "Unknown")
                if not role_id:
                    continue

                # Fetch full role to get formSchema
                try:
                    role_data = await client.get(
                        "/role/{role_id}",
                        path_params={"role_id": role_id},
                        resource_type="role",
                        resource_id=role_id,
                    )
                except BPANotFoundError:
                    continue

                role_schema = parse_form_schema(role_data)
                components = role_schema.get("components", [])
                if not components:
                    continue

                label = f"role:{role_name}"
                pre_count = len(issues)
                _scan_form_components(
                    role_schema, behaviour_map, label, role_id, issues
                )

                if not dry_run and len(issues) > pre_count:
                    _apply_fixes_to_schema(role_schema, behaviour_map)
                    role_data["formSchema"] = role_schema
                    forms_to_save[f"role:{role_id}"] = {
                        "type": "role",
                        "role_id": role_id,
                        "role_name": role_name,
                        "payload": role_data,
                    }

            # --- Apply fixes ---
            fixed = 0
            errors: list[str] = []
            if not dry_run and forms_to_save:
                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="behaviour_metadata_repair",
                    params={
                        "service_id": str(service_id),
                        "forms_to_fix": len(forms_to_save),
                        "issues_found": len(issues),
                    },
                )

                try:
                    for key, save_info in forms_to_save.items():
                        try:
                            if save_info["type"] == "form":
                                await client.put(
                                    save_info["endpoint"],
                                    path_params={"form_id": save_info["form_id"]},
                                    json=save_info["payload"],
                                    resource_type="form",
                                )
                            elif save_info["type"] == "role":
                                await client.put(
                                    "/role",
                                    json=save_info["payload"],
                                    resource_type="role",
                                    resource_id=save_info["role_id"],
                                )
                            fixed += sum(
                                1
                                for i in issues
                                if i.get("role_id") == save_info.get("role_id")
                                or (
                                    save_info["type"] == "form"
                                    and i["form_label"] == "applicant"
                                )
                            )
                        except Exception as e:
                            errors.append(f"{key}: {e}")

                    await audit_logger.mark_success(
                        audit_id,
                        result={
                            "fixed": fixed,
                            "errors": len(errors),
                        },
                    )
                except Exception:
                    await audit_logger.mark_failed(audit_id, "batch repair failed")
                    raise

            return {
                "service_id": str(service_id),
                "behaviours_scanned": len(behaviour_map),
                "issues_found": len(issues),
                "fixed": fixed if not dry_run else None,
                "dry_run": dry_run,
                "issues": issues[:50],  # Cap for readability
                "errors": errors if errors else None,
            }

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


def _scan_form_components(
    schema: dict[str, Any],
    behaviour_map: dict[str, dict[str, Any]],
    form_label: str,
    role_id: str | None,
    issues: list[dict[str, Any]],
) -> None:
    """Scan form components for missing/stale effectsIds metadata."""
    components = schema.get("components", [])
    _walk_components(components, behaviour_map, form_label, role_id, issues)


def _walk_components(
    components: list[dict[str, Any]],
    behaviour_map: dict[str, dict[str, Any]],
    form_label: str,
    role_id: str | None,
    issues: list[dict[str, Any]],
) -> None:
    """Recursively walk form.io components checking effectsIds."""
    for comp in components:
        if not isinstance(comp, dict):
            continue
        key = comp.get("key")
        if key and key in behaviour_map:
            expected = behaviour_map[key]
            current_effects = comp.get("effectsIds", [])
            current_behaviour = comp.get("behaviourId", "")

            if (
                set(current_effects) != set(expected["effect_ids"])
                or current_behaviour != expected["behaviour_id"]
            ):
                issues.append(
                    {
                        "component_key": key,
                        "form_label": form_label,
                        "role_id": role_id,
                        "expected_behaviour_id": expected["behaviour_id"],
                        "expected_effects_ids": expected["effect_ids"],
                        "current_behaviour_id": current_behaviour or None,
                        "current_effects_ids": current_effects or None,
                    }
                )

        # Recurse into children
        for child_key in ("components", "columns", "rows", "tabs"):
            children = comp.get(child_key, [])
            if isinstance(children, list):
                for item in children:
                    if isinstance(item, dict):
                        _walk_components(
                            [item], behaviour_map, form_label, role_id, issues
                        )
                    elif isinstance(item, list):
                        _walk_components(
                            item, behaviour_map, form_label, role_id, issues
                        )


def _apply_fixes_to_schema(
    schema: dict[str, Any],
    behaviour_map: dict[str, dict[str, Any]],
) -> None:
    """Apply effectsIds/behaviourId fixes to all matching components in schema."""
    components = schema.get("components", [])
    _apply_fixes_walk(components, behaviour_map)


def _apply_fixes_walk(
    components: list[dict[str, Any]],
    behaviour_map: dict[str, dict[str, Any]],
) -> None:
    """Recursively fix effectsIds on components."""
    for comp in components:
        if not isinstance(comp, dict):
            continue
        key = comp.get("key")
        if key and key in behaviour_map:
            expected = behaviour_map[key]
            comp["behaviourId"] = expected["behaviour_id"]
            comp["effectsIds"] = expected["effect_ids"]

        for child_key in ("components", "columns", "rows", "tabs"):
            children = comp.get(child_key, [])
            if isinstance(children, list):
                for item in children:
                    if isinstance(item, dict):
                        _apply_fixes_walk([item], behaviour_map)
                    elif isinstance(item, list):
                        _apply_fixes_walk(item, behaviour_map)


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

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

    # Read operations
    mcp.tool(annotations=READ)(componentbehaviour_list)
    mcp.tool(annotations=READ)(componentbehaviour_get)
    mcp.tool(annotations=READ)(componentbehaviour_get_by_component)
    # Write operations
    mcp.tool(annotations=WRITE)(effect_create)
    mcp.tool(annotations=DESTRUCTIVE)(effect_delete)
    mcp.tool(annotations=WRITE)(componentbehaviour_repair_metadata)
