"""DS MCP write tools for file lifecycle management.

Tools for creating, filling, submitting, processing, paying,
and cleaning up application files programmatically.
"""

from __future__ import annotations

import asyncio
import json
import os
import re
from typing import Any

from fastmcp import Context
from fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

from mcp_eregistrations_ds.ds_client import DSClient, DSClientError, translate_error
from mcp_eregistrations_ds.ds_client.errors import (
    DSAuthenticationError,
    DSPermissionError,
)
from mcp_eregistrations_ds.server import mcp

# ---------------------------------------------------------------------------
# Action value mapping for file_process_action
# ---------------------------------------------------------------------------

# Delay before checking process state after a jump. Camunda processes
# restart asynchronously; 1s is enough for most BPMN topologies.
_POST_JUMP_VERIFY_DELAY = 1.0

_ACTION_MAP = {
    "approve": "filevalidated",
    "validate": "filevalidated",
    "sendback": "filedecline",
    "decline": "filedecline",
    "reject": "filereject",
}

# ---------------------------------------------------------------------------
# file_copy helpers
# ---------------------------------------------------------------------------

_AUTOGEN_FIELDS: set[str] = {
    "dossierNumber",
    "draftFileId",
    "userid",
    "kc_id",
    "kc_email",
    "kc_username",
    "kc_firstName",
    "kc_lastName",
    "kc_realm_access__roles",
    "username",
    "userfirstname",
    "userlastname",
    "userfullname",
    "starterEmail",
    "applicant-email",
    "applicantuserid",
    "applicantusername",
    "applicantfullname",
    "processId",
    "dossierDate",
    "dateNow",
    "dateTimeNow",
    "dateAfter1Year",
    "dateAfter2Years",
}


def _scrub_autogen_fields(
    data: dict[str, Any],
    exclude_fields: list[str] | None = None,
) -> tuple[dict[str, Any], list[str]]:
    """Remove auto-generated fields from file data.

    Args:
        data: The form data dict.
        exclude_fields: Additional field keys to scrub beyond _AUTOGEN_FIELDS.

    Returns (scrubbed_data, list_of_removed_field_names).
    """
    scrub_set = _AUTOGEN_FIELDS | set(exclude_fields or [])
    removed: list[str] = []
    scrubbed: dict[str, Any] = {}
    for key, value in data.items():
        if key in scrub_set:
            removed.append(key)
        else:
            scrubbed[key] = value
    return scrubbed, removed


def _is_document_field(value: Any) -> bool:
    """Check if a data field value matches the formio document upload pattern."""
    if not isinstance(value, list) or not value:
        return False
    first = value[0]
    if not isinstance(first, dict):
        return False
    return (
        first.get("storage") == "url"
        and isinstance(first.get("data"), dict)
        and "id" in first["data"]
    )


def _find_document_fields(
    data: dict[str, Any],
    prefix: str = "",
) -> dict[str, list[dict[str, Any]]]:
    """Recursively find document upload fields in form data.

    Walks top-level keys and grid rows (list-of-dicts).
    Returns {dotted_key: document_entries_list}.
    """
    found: dict[str, list[dict[str, Any]]] = {}
    for key, value in data.items():
        full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
        if _is_document_field(value):
            found[full_key] = value
        elif isinstance(value, list) and value and isinstance(value[0], dict):
            if not _is_document_field(value):
                for i, row in enumerate(value):
                    if isinstance(row, dict):
                        nested = _find_document_fields(row, prefix=f"{key}[{i}]")
                        found.update(nested)
    return found


def _parse_nested_path(path: str) -> list[str | int]:
    """Parse a dotted/bracketed path into segments.

    'directors[0].idDocument' → ['directors', 0, 'idDocument']
    'passportUpload'          → ['passportUpload']
    """
    segments: list[str | int] = []
    for part in re.split(r"\.|(?=\[)", path):
        if not part:
            continue
        m = re.match(r"\[(\d+)\]", part)
        if m:
            segments.append(int(m.group(1)))
        else:
            segments.append(part)
    return segments


def _nested_delete(data: Any, path: str) -> None:
    """Delete a value at a dotted/bracketed path in nested data."""
    segments = _parse_nested_path(path)
    obj = data
    for seg in segments[:-1]:
        obj = obj[seg]
    last = segments[-1]
    if isinstance(obj, dict):
        obj.pop(last, None)
    elif isinstance(obj, list) and isinstance(last, int):
        obj[last] = None


def _nested_set(data: Any, path: str, value: Any) -> None:
    """Set a value at a dotted/bracketed path in nested data."""
    segments = _parse_nested_path(path)
    obj = data
    for seg in segments[:-1]:
        obj = obj[seg]
    obj[segments[-1]] = value


# ---------------------------------------------------------------------------
# Task 1: file_create
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_create(
    service_id: str,
    business_entity_id: str | None = None,
    meta_data: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create a new application file for a service.

    Args:
        service_id: UUID of the service.
        business_entity_id: UUID of business entity (default: None).
        meta_data: Optional metadata string.
        instance: Target DS instance.

    Returns:
        dict with file_id, state, service_id, service_name, created_at.
    """
    body: dict[str, Any] = {"service_id": service_id}
    if business_entity_id is not None:
        body["business_entity_id"] = business_entity_id
    if meta_data is not None:
        body["metaData"] = meta_data

    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                "/backend/files",
                json=body,
            )
    except DSClientError as e:
        raise translate_error(e, resource_type="file") from e
    return result


# ---------------------------------------------------------------------------
# file_create_batch
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_create_batch(
    service_id: str,
    count: int,
    business_entity_id: str | None = None,
    meta_data: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create multiple application files for a service.

    Args:
        service_id: UUID of the service.
        count: Number of files to create (1-20).
        business_entity_id: UUID of business entity (default: None).
        meta_data: Optional metadata string (applied to all files).
        instance: Target DS instance.

    Returns:
        dict with created, failed, total_requested, total_created.
    """
    if not service_id or not service_id.strip():
        raise ToolError("service_id is required")
    if not 1 <= count <= 20:
        raise ToolError("count must be between 1 and 20")

    body: dict[str, Any] = {"service_id": service_id}
    if business_entity_id is not None:
        body["business_entity_id"] = business_entity_id
    if meta_data is not None:
        body["metaData"] = meta_data

    created: list[dict[str, Any]] = []
    failed: list[dict[str, Any]] = []

    try:
        async with DSClient.for_instance(instance) as client:
            for i in range(count):
                try:
                    result: dict[str, Any] = await client.post(
                        "/backend/files",
                        json=body,
                    )
                    created.append(result)
                except DSClientError as e:
                    failed.append({"index": i, "error": str(e)})
    except DSClientError as e:
        raise translate_error(e, resource_type="file") from e

    return {
        "created": created,
        "failed": failed,
        "total_requested": count,
        "total_created": len(created),
    }


# ---------------------------------------------------------------------------
# Task 2: file_set_data
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=True))
async def file_set_data(
    file_id: str,
    data: dict[str, Any],
    form_type: str = "WIZARD",
    skip_validation: bool = True,
    instance: str | None = None,
) -> dict[str, Any]:
    """Set form field values on an application file.

    Args:
        file_id: UUID of the file.
        data: Flat dict of field key-value pairs.
        form_type: Form type URL segment (default: WIZARD).
        skip_validation: Skip FormIO validation (default: True).
        instance: Target DS instance.

    Returns:
        dict with form_type, data_content.
    """
    params: dict[str, str] = {}
    if skip_validation:
        params["ignore_formio_validation"] = "1"

    body = {"data_content": json.dumps(data)}

    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.put(
                f"/backend/files/{file_id}/data/{form_type}",
                json=body,
                params=params,
            )
    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="file",
            resource_id=file_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Shared Formio document entry builder
# ---------------------------------------------------------------------------


def _build_formio_entry(
    upload_result: dict[str, Any],
    *,
    original_name: str = "",
    fallback_size: int = 0,
    fallback_type: str = "application/octet-stream",
) -> dict[str, Any]:
    """Build a Formio-compatible document entry from a DS upload result.

    Used by both file_upload_document and file_copy to ensure
    consistent entry shape that the UI recognizes.
    """
    return {
        "url": upload_result.get("url", ""),
        "data": {"id": upload_result.get("id")},
        "name": upload_result.get("file_name", original_name),
        "size": upload_result.get("file_size_bytes") or fallback_size,
        "type": upload_result.get("file_type", fallback_type),
        "storage": "url",
        "originalName": original_name or upload_result.get("file_name", ""),
    }


# ---------------------------------------------------------------------------
# Task 3: file_upload_document
# ---------------------------------------------------------------------------


def _read_file_bytes(path: str) -> bytes:
    """Read file bytes (called via asyncio.to_thread to avoid blocking)."""
    with open(path, "rb") as f:
        return f.read()


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_upload_document(
    file_id: str,
    file_path: str,
    document_requirement_id: str,
    document_name: str | None = None,
    form_type: str = "WIZARD",
    instance: str | None = None,
) -> dict[str, Any]:
    """Upload a document to an application file.

    Uploads to DS storage AND patches the Formio submission data
    so the document appears in the UI. The document_requirement_id
    becomes the document_id on the backend and the Formio field key.

    Args:
        file_id: UUID of the file.
        file_path: Local path to the file to upload.
        document_requirement_id: BPA requirement key (becomes document_id
            and Formio field key, e.g. "articlesOfIncorporation").
        document_name: Human-readable label (e.g. "Articles of incorporation").
        form_type: Form type URL segment (default: WIZARD).
        instance: Target DS instance.

    Returns:
        dict with id, file_name, file_type, document_id, url, data_patched.
    """
    if not os.path.exists(file_path):
        raise ToolError(f"File not found: {file_path}")

    file_name = os.path.basename(file_path)
    file_data = await asyncio.to_thread(_read_file_bytes, file_path)

    params: dict[str, str] = {}
    if document_name:
        params["document_name"] = document_name

    try:
        async with DSClient.for_instance(instance) as client:
            # Step 1: Upload to DS document store
            result: dict[str, Any] = await client.upload(
                f"/backend/files/{file_id}/document",
                file_data=file_data,
                file_name=file_name,
                field_name="file",
                extra_fields={"name": document_requirement_id},
                params=params or None,
            )

            # Step 2: Patch Formio submission data so UI shows the document
            formio_entry = _build_formio_entry(
                result,
                original_name=file_name,
                fallback_size=len(file_data),
            )
            try:
                # Read current form data
                file_detail = await client.get(f"/backend/admin/files/{file_id}")
                current_data: dict[str, Any] = file_detail.get("data") or {}
                if isinstance(current_data, list):
                    current_data = current_data[0] if current_data else {}

                # Append to existing doc list or create new one
                existing = current_data.get(document_requirement_id, [])
                if isinstance(existing, list):
                    existing.append(formio_entry)
                else:
                    existing = [formio_entry]
                current_data[document_requirement_id] = existing

                # Save back
                await client.put(
                    f"/backend/files/{file_id}/data/{form_type}",
                    json={"data_content": json.dumps(current_data)},
                    params={"ignore_formio_validation": "1"},
                )
                result["data_patched"] = True
            except Exception as exc:
                # Upload succeeded but data patch failed — document is in DS
                # but won't show in UI until data is saved
                result["data_patched"] = False
                result["data_patch_error"] = str(exc)

    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="document",
            resource_id=file_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 4: file_submit
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_submit(
    file_id: str,
    data: dict[str, Any] | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Submit an application file — starts the Camunda process.

    File must have required data/documents set. Services with
    digital payment require file_pay before submit.

    Args:
        file_id: UUID of the file to submit.
        data: Optional final form data.
        instance: Target DS instance.

    Returns:
        dict with file_id, state, process_instance_id, service_id.
    """
    body = {"data_content": json.dumps(data or {})}

    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                f"/backend/files/{file_id}/start_process",
                json=body,
            )
    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="file",
            resource_id=file_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 5: file_process_action
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_process_action(
    process_id: str,
    role_name: str,
    action: str,
    form_data: dict[str, Any] | None = None,
    form_type: str = "WIZARD",
    dry_run: bool = False,
    instance: str | None = None,
) -> dict[str, Any]:
    """Process a file at a desk — approve, send back, or reject.

    For resubmit after sendback: role_name="applicant", action="approve".
    Services with payment require completed payment before approve.

    Args:
        process_id: Camunda process instance ID.
        role_name: Desk/role name (e.g. "technicalEvaluation").
        action: "approve", "sendback", or "reject".
        form_data: Optional desk-specific field values.
        form_type: Form type URL segment (default: WIZARD).
        dry_run: Save data without completing task (default: False).
        instance: Target DS instance.

    Returns:
        dict with processing result.
    """
    action_value = _ACTION_MAP.get(action)
    if not action_value:
        raise ToolError(
            f"Invalid action '{action}'. Use: {', '.join(_ACTION_MAP.keys())}"
        )

    data = dict(form_data or {})
    data[f"{role_name}selection"] = action_value

    body: dict[str, Any] = {"data": data}
    path = f"/backend/process/{process_id}/role-forms/{role_name}/{form_type}"

    try:
        async with DSClient.for_instance(instance) as client:
            if dry_run:
                result: dict[str, Any] = await client.patch(
                    path,
                    json=body,
                )
            else:
                result = await client.put(path, json=body)
    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="process",
            resource_id=process_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 6: file_pay
# ---------------------------------------------------------------------------


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=False, idempotentHint=False))
async def file_pay(
    file_id: str,
    provider_code: str,
    provider_data: dict[str, Any] | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create a payment transaction for a file.

    Args:
        file_id: UUID of the file to pay for.
        provider_code: Payment provider (e.g. "pagadito", "fedapay").
        provider_data: Optional provider-specific data.
        instance: Target DS instance.

    Returns:
        dict with transaction_id, total_cost, cost_currency, status.
    """
    body = {
        "data": {"fileId": file_id},
        "providerData": provider_data or {},
    }

    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                f"/backend/payment_transaction/{provider_code}",
                json=body,
            )
    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="payment",
            resource_id=file_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 7: file_restart_process
# ---------------------------------------------------------------------------


@mcp.tool(
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
        idempotentHint=False,
    )
)
async def file_jump_to_role(
    process_definition_id: str,
    process_instance_id: str,
    target_role: str,
    instance: str | None = None,
) -> dict[str, Any]:
    """Jump a live process to resume after a specific role.

    Cancels current tasks and creates new ones after target_role.
    Process must be active (not ended). Use process_task_variables
    or process_history to find BPMN activityIds. Note: the
    activityId is NOT the same as BPA short_name or DS shortname.

    Requires superuser, superinspector, or ds_file_manage_any.

    Args:
        process_definition_id: Camunda process definition ID.
        process_instance_id: Camunda process instance ID.
        target_role: BPMN activityId to jump to (e.g. "factCheck").
        instance: Target DS instance.

    Returns:
        dict with modification result, plus:
        - process_ended (bool): whether process ended after jump.
        - warning (str): present when process auto-completed past target.
        - verification_skipped (bool): set if post-jump check failed.
        - verification_error (str): reason verification was skipped.
    """
    data: dict[str, Any] = {
        "ended": False,
        "instructions": [
            {"type": "startAfterActivity", "activityId": target_role},
        ],
        "processInstanceIds": [process_instance_id],
        "initialVariables": False,
        "skipCustomListeners": False,
        "withoutBusinessKey": False,
    }

    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.post(
                f"/api/v1/restart-process/{process_definition_id}/",
                json={"data": data},
            )

            # Post-jump verification: check if process actually stopped
            # at the target role or auto-completed past it
            await asyncio.sleep(_POST_JUMP_VERIFY_DELAY)
            try:
                process_state: dict[str, Any] = await client.get(
                    f"/backend/process/{process_instance_id}"
                )
                ended = process_state.get("ended", False)
                if ended:
                    result["warning"] = (
                        "Process auto-completed past target role and ended. "
                        "The BPMN may lack user tasks at this activity. "
                        "Use process_get to inspect final state."
                    )
                    result["process_ended"] = True
                else:
                    result["process_ended"] = False
            except DSClientError as verify_err:
                # Verification is best-effort; don't fail the jump
                result["verification_skipped"] = True
                result["verification_error"] = str(verify_err)

    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="process",
            resource_id=process_definition_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 8: file_delete_process
# ---------------------------------------------------------------------------


@mcp.tool(
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=True,
    )
)
async def file_delete_process(
    process_instance_id: str,
    instance: str | None = None,
) -> dict[str, Any]:
    """Delete a file and its process — DESTRUCTIVE, removes all data.

    Deletes DS file, documents, certificates AND Camunda process.
    Requires superinspector or ds_file_manage_any permission.

    Args:
        process_instance_id: Camunda process instance ID.
        instance: Target DS instance.

    Returns:
        Empty dict on success.
    """
    try:
        async with DSClient.for_instance(instance) as client:
            result: dict[str, Any] = await client.delete(
                f"/api/v1/delete-process/{process_instance_id}/",
            )
    except DSClientError as e:
        raise translate_error(
            e,
            resource_type="process",
            resource_id=process_instance_id,
        ) from e
    return result


# ---------------------------------------------------------------------------
# Task 9: file_copy
# ---------------------------------------------------------------------------


@mcp.tool(
    annotations=ToolAnnotations(
        readOnlyHint=False,
        destructiveHint=False,
        idempotentHint=False,
    )
)
async def file_copy(
    source_file_id: str,
    service_id: str | None = None,
    include_documents: bool = True,
    form_type: str = "WIZARD",
    exclude_fields: list[str] | None = None,
    instance: str | None = None,
    ctx: Context | None = None,
) -> dict[str, Any]:
    """Copy an application file — clones form data and documents.

    Creates a new file with the same data as the source. Documents
    are re-uploaded (not referenced). Auto-generated fields are
    scrubbed and regenerated by the backend.

    Args:
        source_file_id: UUID of the file to copy from.
        service_id: Override target service (default: source's service).
        include_documents: Re-upload documents (default: True).
        form_type: Form type URL segment (default: WIZARD).
        exclude_fields: Extra field keys to scrub beyond the built-in list.
        instance: Target DS instance.

    Returns:
        dict with file_id, source_file_id, service_name,
        documents_copied, documents_failed, fields_scrubbed, warnings.
    """
    try:
        async with DSClient.for_instance(instance) as client:
            # Step 1: Read source file
            try:
                source = await client.get(f"/backend/admin/files/{source_file_id}")
            except DSClientError as e:
                raise ToolError(
                    f"Could not read source file '{source_file_id}': "
                    f"source not found or inaccessible. "
                    f"Verify file_id with file_get."
                ) from e

            source_data: dict[str, Any] = source.get("data", {})
            # Handle list-shaped data (take first entry)
            if isinstance(source_data, list):
                source_data = source_data[0] if source_data else {}
            target_service_id = service_id or source.get("service_id")
            source_service_id = source.get("service_id")
            service_name = source.get("service_name", "")

            # Cross-service warning
            warnings: list[str] = []
            if service_id and service_id != source_service_id:
                warnings.append(
                    f"Cross-service copy: source service {source_service_id} "
                    f"differs from target {service_id}. "
                    f"Form data may not match target service schema."
                )

            if ctx:
                await ctx.report_progress(
                    progress=0.1,
                    total=1.0,
                    message=f"Read source file {source_file_id}",
                )

            # Step 2: Scrub auto-generated fields
            scrubbed_data, fields_scrubbed = _scrub_autogen_fields(
                source_data, exclude_fields=exclude_fields
            )

            # Step 3: Identify and strip document fields (including nested)
            doc_fields = _find_document_fields(scrubbed_data)
            for field_key in doc_fields:
                if not include_documents:
                    _nested_delete(scrubbed_data, field_key)
                else:
                    _nested_delete(scrubbed_data, field_key)

            # Step 4: Create new file
            new_file = await client.post(
                "/backend/files",
                json={"service_id": target_service_id},
            )
            new_file_id = new_file["file_id"]

            try:
                # Step 5: Re-upload documents
                documents_copied = 0
                documents_failed: list[dict[str, str]] = []

                if include_documents and doc_fields:
                    total_docs = sum(len(v) for v in doc_fields.values())
                    for field_key, doc_entries in doc_fields.items():
                        new_entries: list[dict[str, Any]] = []
                        for entry in doc_entries:
                            try:
                                doc_url = entry.get("url", "")
                                file_bytes, content_type = await client.get_bytes(
                                    doc_url
                                )
                                upload_result = await client.upload(
                                    f"/backend/files/{new_file_id}/document",
                                    file_data=file_bytes,
                                    file_name=entry.get(
                                        "originalName",
                                        entry.get("name", "file"),
                                    ),
                                    field_name="file",
                                    extra_fields={"name": field_key},
                                )
                                new_entries.append(
                                    _build_formio_entry(
                                        upload_result,
                                        original_name=entry.get(
                                            "originalName",
                                            entry.get("name", ""),
                                        ),
                                        fallback_size=entry.get("size", 0),
                                        fallback_type=entry.get("type", content_type),
                                    )
                                )
                                documents_copied += 1
                                if ctx:
                                    await ctx.report_progress(
                                        progress=0.3
                                        + 0.5 * documents_copied / total_docs,
                                        total=1.0,
                                        message=f"Uploaded {documents_copied}"
                                        f"/{total_docs} documents",
                                    )
                            except (
                                DSAuthenticationError,
                                DSPermissionError,
                            ) as auth_exc:
                                # Auth/permission errors won't resolve by
                                # retrying — fail fast with clear guidance.
                                raise ToolError(
                                    f"Document upload failed: {auth_exc}. "
                                    "Use ds_auth_login to re-authenticate, "
                                    "then retry file_copy."
                                ) from auth_exc
                            except Exception as exc:
                                documents_failed.append(
                                    {
                                        "field": field_key,
                                        "error": str(exc),
                                    }
                                )
                        if new_entries:
                            _nested_set(scrubbed_data, field_key, new_entries)

                # Step 6: Set data on new file
                await client.put(
                    f"/backend/files/{new_file_id}/data/{form_type}",
                    json={"data_content": json.dumps(scrubbed_data)},
                    params={"ignore_formio_validation": "1"},
                )
            except Exception as e:
                # Cleanup orphan file
                try:
                    await client.delete(f"/backend/files/{new_file_id}")
                except Exception:
                    pass
                raise ToolError(
                    f"Copy failed after file creation: {e}. Orphan file: {new_file_id}"
                ) from e

            return {
                "file_id": new_file_id,
                "source_file_id": source_file_id,
                "service_id": target_service_id,
                "service_name": service_name,
                "state": "NEW",
                "documents_copied": documents_copied,
                "documents_failed": documents_failed,
                "fields_scrubbed": fields_scrubbed,
                "warnings": warnings,
            }
    except DSClientError as e:
        raise translate_error(e, resource_type="file") from e
