"""MCP tools for BPA service operations.

This module provides tools for listing, retrieving, creating, and updating BPA services.

Write operations follow the audit-before-write pattern:
1. Validate parameters (pre-flight, no audit record if validation fails)
2. Create PENDING audit record
3. Execute BPA API call
4. Update audit record to SUCCESS or FAILED

API Endpoints used:
- GET /service - List all services
- GET /service/{id} - Get service by ID
- POST /service - Create new service
- PUT /service - Update service
"""

from __future__ import annotations

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.large_response import large_response_handler

__all__ = [
    "service_list",
    "service_get",
    "service_create",
    "service_update",
    "service_publish",
    "service_activate",
    "service_delete",
    "register_service_tools",
]


@large_response_handler(
    threshold_bytes=50 * 1024,  # 50KB threshold for list tools
    navigation={
        "list_all": "jq '.services'",
        "find_by_name": "jq '.services[] | select(.name | contains(\"search\"))'",
        "find_by_status": "jq '.services[] | select(.status == \"ACTIVE\")'",
    },
)
async def service_list(
    limit: int = 0,
    offset: int = 0,
    instance: str | None = None,
) -> dict[str, Any]:
    """List all BPA services.

    Returns all services the authenticated user has access to.
    Each service includes id, name, status. Use service_get for registration details.
    Large responses (>50KB) are saved to file with navigation hints.

    Args:
        limit: Max services to return (default: 0 = all, no limit).
        offset: Number of services to skip (default: 0).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with services, total, has_more.
    """
    if offset < 0:
        offset = 0

    try:
        async with BPAClient.for_instance(instance) as client:
            services_data = await client.get_list("/service", resource_type="service")
    except BPAClientError as e:
        raise translate_error(e, resource_type="service")

    # Transform to consistent output format
    all_services = [
        {
            "id": svc.get("id"),
            "name": svc.get("name"),
            "status": svc.get("status"),
        }
        for svc in services_data
    ]

    total = len(all_services)

    # Apply pagination
    if limit > 0:
        paginated_services = all_services[offset : offset + limit]
        has_more = (offset + limit) < total
    elif offset > 0:
        paginated_services = all_services[offset:]
        has_more = False
    else:
        paginated_services = all_services
        has_more = False

    result: dict[str, Any] = {
        "services": paginated_services,
        "total": total,
        "has_more": has_more,
    }

    # Inform user when results are limited
    if limit > 0 and has_more:
        result["note"] = (
            f"Showing {len(paginated_services)} of {total} services. "
            f"Use offset={offset + limit} to see more, or limit=0 for all."
        )

    return result


async def service_get(
    service_id: str | int, instance: str | None = None
) -> dict[str, Any]:
    """Get details of a BPA service by ID.

    Returns complete service details including registrations summary.

    Args:
        service_id: The unique identifier of the service.
        instance: Instance profile name (from instance_list).

    Returns:
        dict: Complete service details including:
            - id, name, description, status, short_name
            - registrations: List of registration summaries
            - created_at, updated_at timestamps
    """
    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                service_data = 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."
                )

            # Extract registrations embedded in service response
            # Note: BPA API ignores serviceId param on /registration endpoint,
            # returning ALL registrations globally. The correct approach is to
            # use registrations already embedded in the service response.
            registrations_data = service_data.get("registrations", [])
    except ToolError:
        # Re-raise ToolError (from BPANotFoundError handling above)
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # Transform registrations to summary format (includes key per AC1)
    registrations = [
        {"id": reg.get("id"), "name": reg.get("name"), "key": reg.get("key")}
        for reg in registrations_data
    ]

    return {
        "id": service_data.get("id"),
        "name": service_data.get("name"),
        "description": service_data.get("description"),
        "status": service_data.get("status"),
        "short_name": service_data.get("shortName"),
        "registrations": registrations,
        "created_at": service_data.get("createdAt"),
        "updated_at": service_data.get("updatedAt"),
    }


def _validate_service_create_params(
    name: str,
    description: str | None,
    short_name: str | None,
) -> dict[str, Any]:
    """Validate service_create parameters (pre-flight).

    Returns validated params dict or raises ToolError if invalid.
    No audit record is created for validation failures.

    Args:
        name: Service name (required).
        description: Service description (optional).
        short_name: Short name for the service (optional).

    Returns:
        dict: Validated parameters ready for API call.

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

    if not name or not name.strip():
        errors.append("'name' is required and cannot be empty")

    if name and len(name.strip()) > 255:
        errors.append("'name' must be 255 characters or less")

    if short_name and len(short_name.strip()) > 50:
        errors.append("'short_name' must be 50 characters or less")

    if errors:
        error_msg = "; ".join(errors)
        raise ToolError(f"Cannot create service: {error_msg}. Check required fields.")

    params: dict[str, Any] = {
        "name": name.strip(),
        "active": True,  # Services are active by default
    }
    if description:
        params["description"] = description.strip()
    if short_name:
        params["shortName"] = short_name.strip()

    return params


async def service_create(
    name: str,
    description: str | None = None,
    short_name: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create a new BPA service. Audited write operation.

    Services are created as active by default.

    Args:
        name: Service name.
        description: Optional description.
        short_name: Optional short name.
        instance: Instance profile name (from instance_list).

    Returns:
        dict with id, name, description, status, short_name, active, created_at,
        audit_id.
    """
    # Pre-flight validation (no audit record for validation failures)
    validated_params = _validate_service_create_params(name, description, short_name)

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

    # 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="service",
        params=validated_params,
    )

    try:
        async with BPAClient.for_instance(instance) as client:
            service_data = await client.post(
                "/service",
                json=validated_params,
                resource_type="service",
            )

        # Save rollback state (for create, save ID to enable deletion on rollback)
        created_id = service_data.get("id")
        await audit_logger.save_rollback_state(
            audit_id=audit_id,
            object_type="service",
            object_id=str(created_id),
            previous_state={
                "id": created_id,
                "name": service_data.get("name"),
                "description": service_data.get("description"),
                "shortName": service_data.get("shortName"),
                "_operation": "create",  # Marker for rollback to know to DELETE
            },
        )

        # Mark audit as success
        await audit_logger.mark_success(
            audit_id,
            result={
                "service_id": created_id,
                "name": service_data.get("name"),
            },
        )

        return {
            "id": created_id,
            "name": service_data.get("name"),
            "description": service_data.get("description"),
            "status": service_data.get("status"),
            "short_name": service_data.get("shortName"),
            "active": service_data.get("active", True),
            "created_at": service_data.get("createdAt"),
            "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="service")


def _validate_service_update_params(
    service_id: str | int,
    name: str | None,
    description: str | None,
    short_name: str | None,
) -> dict[str, Any]:
    """Validate service_update parameters (pre-flight).

    Returns validated params dict or raises ToolError if invalid.

    Args:
        service_id: ID of service to update (required).
        name: New name (optional).
        description: New description (optional).
        short_name: New short name (optional).

    Returns:
        dict: Validated parameters ready for API call.

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

    if not service_id:
        errors.append("'service_id' is required")

    if name is not None and not name.strip():
        errors.append("'name' cannot be empty when provided")

    if name and len(name.strip()) > 255:
        errors.append("'name' must be 255 characters or less")

    if short_name and len(short_name.strip()) > 50:
        errors.append("'short_name' must be 50 characters or less")

    # At least one field must be provided for update
    if name is None and description is None and short_name is None:
        errors.append(
            "At least one field (name, description, short_name) must be provided"
        )

    if errors:
        error_msg = "; ".join(errors)
        raise ToolError(f"Cannot update service: {error_msg}. Check required fields.")

    params: dict[str, Any] = {"id": service_id}
    if name is not None:
        params["name"] = name.strip()
    if description is not None:
        params["description"] = description.strip()
    if short_name is not None:
        params["shortName"] = short_name.strip()

    return params


async def service_update(
    service_id: str | int,
    name: str | None = None,
    description: str | None = None,
    short_name: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Update an existing BPA service. Audited write operation.

    Args:
        service_id: Service ID to update.
        name: New name (optional).
        description: New description (optional).
        short_name: New short name (optional).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with id, name, description, status, short_name, updated_at,
        previous_state, audit_id.
    """
    # Pre-flight validation (no audit record for validation failures)
    validated_params = _validate_service_update_params(
        service_id, name, description, short_name
    )

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

    # Capture current state for rollback BEFORE making changes
    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                previous_state = 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."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # 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="update",
        object_type="service",
        object_id=str(service_id),
        params={
            "changes": validated_params,
        },
    )

    # Save rollback state for undo capability
    await audit_logger.save_rollback_state(
        audit_id=audit_id,
        object_type="service",
        object_id=str(service_id),
        previous_state={
            "id": previous_state.get("id"),
            "name": previous_state.get("name"),
            "description": previous_state.get("description"),
            "shortName": previous_state.get("shortName"),
        },
    )

    try:
        async with BPAClient.for_instance(instance) as client:
            # BPA API requires name in PUT body - merge previous state with changes
            put_body = {
                "id": service_id,
                "name": validated_params.get("name", previous_state.get("name")),
            }
            if "description" in validated_params:
                put_body["description"] = validated_params["description"]
            elif previous_state.get("description"):
                put_body["description"] = previous_state["description"]
            if "shortName" in validated_params:
                put_body["shortName"] = validated_params["shortName"]
            elif previous_state.get("shortName"):
                put_body["shortName"] = previous_state["shortName"]

            await client.put(
                "/service",
                json=put_body,
                resource_type="service",
                resource_id=service_id,
            )

            # Backend PUT /service returns void — re-fetch to get current state
            try:
                service_data = await client.get(
                    "/service/{id}",
                    path_params={"id": service_id},
                    resource_type="service",
                    resource_id=service_id,
                )
            except BPAClientError:
                # PUT succeeded but re-fetch failed — use put_body as fallback
                service_data = put_body

        # Mark audit as success
        await audit_logger.mark_success(
            audit_id,
            result={
                "service_id": service_data.get("id"),
                "name": service_data.get("name"),
                "changes_applied": {
                    k: v for k, v in validated_params.items() if k != "id"
                },
            },
        )

        return {
            "id": service_data.get("id"),
            "name": service_data.get("name"),
            "description": service_data.get("description"),
            "status": service_data.get("status"),
            "short_name": service_data.get("shortName"),
            "updated_at": service_data.get("updatedAt"),
            "previous_state": {
                "name": previous_state.get("name"),
                "description": previous_state.get("description"),
                "short_name": previous_state.get("shortName"),
            },
            "audit_id": audit_id,
        }

    except BPAClientError as e:
        # Mark audit as failed (only if PUT itself failed)
        await audit_logger.mark_failed(audit_id, str(e))
        raise translate_error(e, resource_type="service", resource_id=service_id)


async def service_publish(
    service_id: str | int, instance: str | None = None
) -> dict[str, Any]:
    """Publish a BPA service to make it visible in the frontend.

    Audited write operation.

    Args:
        service_id: Service ID to publish.
        instance: Instance profile name (from instance_list).

    Returns:
        dict with service_id, published (bool), audit_id.
    """
    # Pre-flight validation
    if not service_id:
        raise ToolError("'service_id' is required. Provide the service ID to publish.")

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

    # Verify service exists
    try:
        async with BPAClient.for_instance(instance) as client:
            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."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # Create audit record BEFORE API call
    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="service",
        object_id=str(service_id),
        params={"action": "publish"},
    )

    try:
        # Publish can take 30-60s on large services.  Use a long
        # timeout and disable retries to avoid triggering the
        # backend's "already publishing" concurrency guard, which
        # crashes with a service_id NOT NULL constraint violation.
        client = BPAClient.for_instance(instance)
        client.timeout = 120.0
        client.max_retries = 0
        async with client:
            await client.post(
                "/service/{id}/publish",
                path_params={"id": service_id},
                json={
                    "externalServerId": None,
                    "title": None,
                    "description": "-",
                    "typesOfPublication": [],
                },
                resource_type="service",
            )

        # Mark audit as success
        await audit_logger.mark_success(
            audit_id,
            result={"service_id": service_id, "action": "published"},
        )

        return {
            "service_id": str(service_id),
            "published": True,
            "audit_id": audit_id,
        }

    except BPAClientError as e:
        await audit_logger.mark_failed(audit_id, str(e))
        raise translate_error(e, resource_type="service", resource_id=service_id)


async def service_activate(
    service_id: str | int,
    active: bool = True,
    instance: str | None = None,
) -> dict[str, Any]:
    """Activate or deactivate a BPA service. Audited write operation.

    Args:
        service_id: Service ID to activate/deactivate.
        active: True to activate, False to deactivate (default: True).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with service_id, active (bool), audit_id.
    """
    # Pre-flight validation
    if not service_id:
        raise ToolError(
            "'service_id' is required. Provide the service ID to activate/deactivate."
        )

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

    # Verify service exists
    try:
        async with BPAClient.for_instance(instance) as client:
            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."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # Create audit record BEFORE API call
    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="service",
        object_id=str(service_id),
        params={"action": "activate", "active": active},
    )

    try:
        async with BPAClient.for_instance(instance) as client:
            await client.put(
                "/service/{service_id}/activate/{active}",
                path_params={"service_id": service_id, "active": str(active).lower()},
                resource_type="service",
                resource_id=service_id,
            )

        # Mark audit as success
        await audit_logger.mark_success(
            audit_id,
            result={"service_id": service_id, "active": active},
        )

        return {
            "service_id": str(service_id),
            "active": active,
            "audit_id": audit_id,
        }

    except BPAClientError as e:
        await audit_logger.mark_failed(audit_id, str(e))
        raise translate_error(e, resource_type="service", resource_id=service_id)


async def service_delete(
    service_id: str | int,
    confirm_name: str,
    instance: str | None = None,
) -> dict[str, Any]:
    """Delete a BPA service. Audited write operation.

    DESTRUCTIVE: Permanently removes the service and all child resources
    (forms, determinants, bots, roles). Requires confirm_name to match
    the service name exactly.

    Args:
        service_id: Service ID to delete.
        confirm_name: Must match the service name exactly (safety check).
        instance: Instance profile name (from instance_list).

    Returns:
        dict with deleted, service_id, deleted_service, audit_id.
    """
    # Pre-flight validation (no audit record for validation failures)
    if not service_id:
        raise ToolError("'service_id' is required. Provide the service ID to delete.")
    if not confirm_name or not confirm_name.strip():
        raise ToolError(
            "'confirm_name' is required. Provide the exact service name to confirm "
            "deletion."
        )

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

    # Capture current state for rollback BEFORE making changes
    try:
        async with BPAClient.for_instance(instance) as client:
            try:
                previous_state = 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."
                )
    except ToolError:
        raise
    except BPAClientError as e:
        raise translate_error(e, resource_type="service", resource_id=service_id)

    # Safety gate: confirm_name must match actual service name (case-sensitive)
    actual_name = previous_state.get("name", "")
    if confirm_name != actual_name:
        raise ToolError(
            f"Service name mismatch: confirm_name '{confirm_name}' does not match "
            f"actual name '{actual_name}'. Provide the exact service name to confirm "
            "deletion."
        )

    # 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="service",
        object_id=str(service_id),
        params={"confirm_name": confirm_name},
    )

    # Save rollback state for undo capability
    await audit_logger.save_rollback_state(
        audit_id=audit_id,
        object_type="service",
        object_id=str(service_id),
        previous_state=previous_state,
    )

    try:
        async with BPAClient.for_instance(instance) as client:
            await client.delete(
                "/service/{id}",
                path_params={"id": service_id},
                resource_type="service",
                resource_id=service_id,
            )

        # Mark audit as success
        await audit_logger.mark_success(
            audit_id,
            result={"service_id": service_id, "name": actual_name, "deleted": True},
        )

        # Invalidate form cache for all form types
        try:
            from mcp_eregistrations_bpa.db.form_cache import invalidate_all_forms

            db_path = resolve_db_path(instance)
            await invalidate_all_forms(str(service_id), db_path)
        except Exception:
            pass

        return {
            "deleted": True,
            "service_id": str(service_id),
            "deleted_service": {
                "id": previous_state.get("id"),
                "name": previous_state.get("name"),
                "status": previous_state.get("status"),
            },
            "audit_id": audit_id,
        }

    except BPAClientError as e:
        await audit_logger.mark_failed(audit_id, str(e))
        raise translate_error(e, resource_type="service", resource_id=service_id)


def register_service_tools(mcp: Any) -> None:
    """Register service 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)(service_list)
    mcp.tool(annotations=READ)(service_get)
    # Write operations (audit-before-write pattern)
    mcp.tool(annotations=WRITE)(service_create)
    mcp.tool(annotations=WRITE)(service_update)
    mcp.tool(annotations=DESTRUCTIVE)(service_publish)
    mcp.tool(annotations=DESTRUCTIVE)(service_activate)
    mcp.tool(annotations=DESTRUCTIVE)(service_delete)
