import copy
import os
import shutil
import tempfile
import uuid
from contextlib import asynccontextmanager, contextmanager
from pathlib import Path
from typing import Any, AsyncIterator, Iterator, Tuple, overload

import httpx
from httpx import Response
from httpx._types import RequestContent

from ..._utils import Endpoint, RequestSpec, header_folder
from ..._utils._ssl_context import get_httpx_client_kwargs
from ..._utils.constants import TEMP_ATTACHMENTS_FOLDER
from ...tracing import traced
from ..attachments import Attachment, AttachmentMode, BlobFileAccessInfo
from ..common import BaseService, FolderContext, UiPathApiConfig, UiPathExecutionContext


def _upload_attachment_input_processor(inputs: dict[str, Any]) -> dict[str, Any]:
    """Process attachment upload inputs to avoid logging large content."""
    processed_inputs = inputs.copy()
    if "source_path" in processed_inputs:
        processed_inputs["source_path"] = f"<File at {processed_inputs['source_path']}>"
    if "content" in processed_inputs:
        if isinstance(processed_inputs["content"], str):
            processed_inputs["content"] = "<Redacted String Content>"
        else:
            processed_inputs["content"] = "<Redacted Binary Content>"
    return processed_inputs


class AttachmentsService(FolderContext, BaseService):
    """Service for managing UiPath attachments.

    Attachments allow you to upload and download files to be used within UiPath
    processes, actions, and other UiPath services.

    Reference: https://docs.uipath.com/orchestrator/reference/api-attachments
    """

    def __init__(
        self, config: UiPathApiConfig, execution_context: UiPathExecutionContext
    ) -> None:
        super().__init__(config=config, execution_context=execution_context)
        self._temp_dir = os.path.join(tempfile.gettempdir(), TEMP_ATTACHMENTS_FOLDER)

    @traced(name="attachments_open", run_type="uipath")
    @contextmanager
    def open(
        self,
        *,
        attachment: Attachment,
        mode: AttachmentMode = AttachmentMode.READ,
        content: RequestContent | None = None,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> Iterator[Tuple[Attachment, Response]]:
        """Open an attachment.

        Args:
            attachment (Attachment): The attachment to open.
            mode (AttachmentMode): The mode to use.
            content (RequestContent | None): An optional request content to upload.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            str: The name of the downloaded attachment.

        Raises:
            Exception: If the download fails and no local file is found.
        """
        try:
            if mode == AttachmentMode.READ:
                assert attachment.id, "Attachment ID is required to open an attachment."
                spec = self._retrieve_download_uri_spec(
                    key=attachment.id,
                    folder_key=folder_key,
                    folder_path=folder_path,
                )
            else:
                spec = self._create_attachment_and_retrieve_upload_uri_spec(
                    name=attachment.full_name,
                    folder_key=folder_key,
                    folder_path=folder_path,
                )

            result = self.request(
                spec.method,
                url=spec.endpoint,
                params=spec.params,
                headers=spec.headers,
                json=spec.json,
            ).json()
            resource: Attachment = copy.deepcopy(attachment)
            resource.id = uuid.UUID(result["Id"])

            resource_uri = result["BlobFileAccess"]["Uri"]
            headers = {
                key: value
                for key, value in zip(
                    result["BlobFileAccess"]["Headers"]["Keys"],
                    result["BlobFileAccess"]["Headers"]["Values"],
                    strict=False,
                )
            }

            if result["BlobFileAccess"]["RequiresAuth"]:
                raise Exception(
                    "Attachment access not supported via UiPath Coded Agents."
                )
            else:
                http_verb = "GET" if mode == AttachmentMode.READ else "PUT"
                with httpx.Client(**get_httpx_client_kwargs()) as client:
                    with client.stream(
                        http_verb,
                        resource_uri,
                        headers=headers,
                        content=content,
                    ) as response:
                        yield resource, response
        except Exception as e:
            # Re-raise the original exception if we can't find it locally
            raise Exception(f"Attachment access failed with error: {e}") from e

    @traced(name="attachments_open", run_type="uipath")
    @asynccontextmanager
    async def open_async(
        self,
        *,
        attachment: Attachment,
        mode: AttachmentMode = AttachmentMode.READ,
        content: RequestContent | None = None,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> AsyncIterator[Tuple[Attachment, Response]]:
        """Open an attachment asynchronously.

        Args:
            attachment (Attachment): The attachment to open.
            mode (AttachmentMode): The mode to use.
            content (RequestContent): An optional request content to upload.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            str: The name of the downloaded attachment.

        Raises:
            Exception: If the download fails and no local file is found.
        """
        try:
            if mode == AttachmentMode.READ:
                assert attachment.id, "Attachment ID is required to open an attachment."
                spec = self._retrieve_download_uri_spec(
                    key=attachment.id,
                    folder_key=folder_key,
                    folder_path=folder_path,
                )
            else:
                spec = self._create_attachment_and_retrieve_upload_uri_spec(
                    name=attachment.full_name,
                    folder_key=folder_key,
                    folder_path=folder_path,
                )

            result = (
                await self.request_async(
                    spec.method,
                    url=spec.endpoint,
                    params=spec.params,
                    headers=spec.headers,
                    json=spec.json,
                )
            ).json()
            resource: Attachment = copy.deepcopy(attachment)
            resource.id = uuid.UUID(result["Id"])

            resource_uri = result["BlobFileAccess"]["Uri"]
            headers = {
                key: value
                for key, value in zip(
                    result["BlobFileAccess"]["Headers"]["Keys"],
                    result["BlobFileAccess"]["Headers"]["Values"],
                    strict=False,
                )
            }

            if result["BlobFileAccess"]["RequiresAuth"]:
                raise Exception(
                    "Attachment access not supported via UiPath Coded Agents."
                )
            else:
                http_verb = "GET" if mode == AttachmentMode.READ else "PUT"
                async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
                    async with client.stream(
                        http_verb,
                        resource_uri,
                        headers=headers,
                        content=content,
                    ) as response:
                        yield resource, response
        except Exception as e:
            # Re-raise the original exception if we can't find it locally
            raise Exception(f"Attachment access failed with error: {e}") from e

    @traced(name="attachments_download", run_type="uipath")
    def download(
        self,
        *,
        key: uuid.UUID,
        destination_path: str,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> str:
        """Download an attachment.

        This method downloads an attachment from UiPath to a local file.
        If the attachment is not found in UiPath (404 error), it will check
        for a local file in the temporary directory that matches the UUID.

        Note:
            The local file fallback functionality is intended for local development
            and debugging purposes only.

        Args:
            key (uuid.UUID): The key of the attachment to download.
            destination_path (str): The local path where the attachment will be saved.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            str: The name of the downloaded attachment.

        Raises:
            Exception: If the download fails and no local file is found.

        Examples:
            ```python
            from uipath.platform import UiPath

            client = UiPath()

            attachment_name = client.attachments.download(
                key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000"),
                destination_path="path/to/save/document.pdf"
            )
            print(f"Downloaded attachment: {attachment_name}")
            ```
        """
        try:
            spec = self._retrieve_download_uri_spec(
                key=key,
                folder_key=folder_key,
                folder_path=folder_path,
            )

            result = self.request(
                spec.method,
                url=spec.endpoint,
                params=spec.params,
                headers=spec.headers,
            ).json()

            # Get the attachment name
            attachment_name = result["Name"]

            download_uri = result["BlobFileAccess"]["Uri"]
            headers = {
                key: value
                for key, value in zip(
                    result["BlobFileAccess"]["Headers"]["Keys"],
                    result["BlobFileAccess"]["Headers"]["Values"],
                    strict=False,
                )
            }

            with open(destination_path, "wb") as file:
                if result["BlobFileAccess"]["RequiresAuth"]:
                    response = self.request(
                        "GET", download_uri, headers=headers, stream=True
                    )
                    for chunk in response.iter_bytes(chunk_size=8192):
                        file.write(chunk)
                else:
                    with httpx.Client(**get_httpx_client_kwargs()) as client:
                        with client.stream(
                            "GET", download_uri, headers=headers
                        ) as response:
                            for chunk in response.iter_bytes(chunk_size=8192):
                                file.write(chunk)

            return attachment_name
        except Exception as e:
            # If not found in UiPath, check local storage
            if "404" in str(e):
                # Check if file exists in temp directory
                if os.path.exists(self._temp_dir):
                    # Look for any file starting with our UUID
                    pattern = f"{key}_*"
                    matching_files = list(Path(self._temp_dir).glob(pattern))

                    if matching_files:
                        # Get the full filename
                        local_file = matching_files[0]

                        # Extract the original name from the filename (part after UUID_)
                        file_name = os.path.basename(local_file)
                        original_name = file_name[len(f"{key}_") :]

                        # Copy the file to the destination
                        shutil.copy2(local_file, destination_path)

                        return original_name

            # Re-raise the original exception if we can't find it locally
            raise Exception(
                f"Attachment with key {key} not found in UiPath or local storage"
            ) from e

    @traced(name="attachments_download", run_type="uipath")
    async def download_async(
        self,
        *,
        key: uuid.UUID,
        destination_path: str,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> str:
        """Download an attachment asynchronously.

        This method asynchronously downloads an attachment from UiPath to a local file.
        If the attachment is not found in UiPath (404 error), it will check
        for a local file in the temporary directory that matches the UUID.

        Note:
            The local file fallback functionality is intended for local development
            and debugging purposes only.

        Args:
            key (uuid.UUID): The key of the attachment to download.
            destination_path (str): The local path where the attachment will be saved.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            str: The name of the downloaded attachment.

        Raises:
            Exception: If the download fails and no local file is found.

        Examples:
            ```python
            import asyncio
            from uipath.platform import UiPath

            client = UiPath()

            async def main():
                attachment_name = await client.attachments.download_async(
                    key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000"),
                    destination_path="path/to/save/document.pdf"
                )
                print(f"Downloaded attachment: {attachment_name}")
            ```
        """
        try:
            spec = self._retrieve_download_uri_spec(
                key=key,
                folder_key=folder_key,
                folder_path=folder_path,
            )

            result = (
                await self.request_async(
                    spec.method,
                    url=spec.endpoint,
                    params=spec.params,
                    headers=spec.headers,
                )
            ).json()

            # Get the attachment name
            attachment_name = result["Name"]

            download_uri = result["BlobFileAccess"]["Uri"]
            headers = {
                key: value
                for key, value in zip(
                    result["BlobFileAccess"]["Headers"]["Keys"],
                    result["BlobFileAccess"]["Headers"]["Values"],
                    strict=False,
                )
            }

            with open(destination_path, "wb") as file:
                if result["BlobFileAccess"]["RequiresAuth"]:
                    response = await self.request_async(
                        "GET", download_uri, headers=headers, stream=True
                    )
                    async for chunk in response.aiter_bytes(chunk_size=8192):
                        file.write(chunk)
                else:
                    async with httpx.AsyncClient(**get_httpx_client_kwargs()) as client:
                        async with client.stream(
                            "GET", download_uri, headers=headers
                        ) as response:
                            async for chunk in response.aiter_bytes(chunk_size=8192):
                                file.write(chunk)

            return attachment_name
        except Exception as e:
            # If not found in UiPath, check local storage
            if "404" in str(e):
                # Check if file exists in temp directory
                if os.path.exists(self._temp_dir):
                    # Look for any file starting with our UUID
                    pattern = f"{key}_*"
                    matching_files = list(Path(self._temp_dir).glob(pattern))

                    if matching_files:
                        # Get the full filename
                        local_file = matching_files[0]

                        # Extract the original name from the filename (part after UUID_)
                        file_name = os.path.basename(local_file)
                        original_name = file_name[len(f"{key}_") :]

                        # Copy the file to the destination
                        shutil.copy2(local_file, destination_path)

                        return original_name

            # Re-raise the original exception if we can't find it locally
            raise Exception(
                f"Attachment with key {key} not found in UiPath or local storage"
            ) from e

    @overload
    def upload(
        self,
        *,
        name: str,
        content: str | bytes,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID: ...

    @overload
    def upload(
        self,
        *,
        name: str,
        source_path: str,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID: ...

    @traced(
        name="attachments_upload",
        run_type="uipath",
        input_processor=_upload_attachment_input_processor,
    )
    def upload(
        self,
        *,
        name: str,
        content: str | bytes | None = None,
        source_path: str | None = None,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID:
        """Upload a file or content to UiPath as an attachment.

        This method uploads content to UiPath and makes it available as an attachment.
        You can either provide a file path or content in memory.

        Args:
            name (str): The name of the attachment file.
            content (str | bytes | None): The content to upload (string or bytes).
            source_path (str | None): The local path of the file to upload.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            uuid.UUID: The UUID of the created attachment.

        Raises:
            ValueError: If neither content nor source_path is provided, or if both are provided.
            Exception: If the upload fails.

        Examples:
            ```python
            from uipath.platform import UiPath

            client = UiPath()

            # Upload a file from disk
            attachment_key = client.attachments.upload(
                name="my-document.pdf",
                source_path="path/to/local/document.pdf",
            )
            print(f"Uploaded attachment with key: {attachment_key}")

            # Upload content from memory
            attachment_key = client.attachments.upload(
                name="notes.txt",
                content="This is a text file content",
            )
            print(f"Uploaded attachment with key: {attachment_key}")
            ```
        """
        # Validate input parameters
        if not (content or source_path):
            raise ValueError("Content or source_path is required")
        if content and source_path:
            raise ValueError("Content and source_path are mutually exclusive")

        spec = self._create_attachment_and_retrieve_upload_uri_spec(
            name=name,
            folder_key=folder_key,
            folder_path=folder_path,
        )

        result = self.request(
            spec.method,
            url=spec.endpoint,
            params=spec.params,
            headers=spec.headers,
            json=spec.json,
        ).json()

        # Get the ID from the response and convert to UUID
        attachment_key = uuid.UUID(result["Id"])

        upload_uri = result["BlobFileAccess"]["Uri"]
        headers = {
            key: value
            for key, value in zip(
                result["BlobFileAccess"]["Headers"]["Keys"],
                result["BlobFileAccess"]["Headers"]["Values"],
                strict=False,
            )
        }

        if source_path:
            # Upload from file
            with open(source_path, "rb") as file:
                file_content = file.read()
                if result["BlobFileAccess"]["RequiresAuth"]:
                    self.request(
                        "PUT", upload_uri, headers=headers, content=file_content
                    )
                else:
                    with httpx.Client(**get_httpx_client_kwargs()) as client:
                        client.put(upload_uri, headers=headers, content=file_content)
        else:
            # Upload from memory
            # Convert string to bytes if needed
            if isinstance(content, str):
                content = content.encode("utf-8")

            if result["BlobFileAccess"]["RequiresAuth"]:
                self.request("PUT", upload_uri, headers=headers, content=content)
            else:
                with httpx.Client(**get_httpx_client_kwargs()) as client:
                    client.put(upload_uri, headers=headers, content=content)

        return attachment_key

    @overload
    async def upload_async(
        self,
        *,
        name: str,
        content: str | bytes,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID: ...

    @overload
    async def upload_async(
        self,
        *,
        name: str,
        source_path: str,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID: ...

    @traced(
        name="attachments_upload",
        run_type="uipath",
        input_processor=_upload_attachment_input_processor,
    )
    async def upload_async(
        self,
        *,
        name: str,
        content: str | bytes | None = None,
        source_path: str | None = None,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> uuid.UUID:
        """Upload a file or content to UiPath as an attachment asynchronously.

        This method asynchronously uploads content to UiPath and makes it available as an attachment.
        You can either provide a file path or content in memory.

        Args:
            name (str): The name of the attachment file.
            content (str | bytes | None): The content to upload (string or bytes).
            source_path (str | None): The local path of the file to upload.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            uuid.UUID: The UUID of the created attachment.

        Raises:
            ValueError: If neither content nor source_path is provided, or if both are provided.
            Exception: If the upload fails.

        Examples:
            ```python
            import asyncio
            from uipath.platform import UiPath

            client = UiPath()

            async def main():
                # Upload a file from disk
                attachment_key = await client.attachments.upload_async(
                    name="my-document.pdf",
                    source_path="path/to/local/document.pdf",
                )
                print(f"Uploaded attachment with key: {attachment_key}")

                # Upload content from memory
                attachment_key = await client.attachments.upload_async(
                    name="notes.txt",
                    content="This is a text file content",
                )
                print(f"Uploaded attachment with key: {attachment_key}")
            ```
        """
        # Validate input parameters
        if not (content or source_path):
            raise ValueError("Content or source_path is required")
        if content and source_path:
            raise ValueError("Content and source_path are mutually exclusive")

        spec = self._create_attachment_and_retrieve_upload_uri_spec(
            name=name,
            folder_key=folder_key,
            folder_path=folder_path,
        )

        result = (
            await self.request_async(
                spec.method,
                url=spec.endpoint,
                params=spec.params,
                headers=spec.headers,
                json=spec.json,
            )
        ).json()

        # Get the ID from the response and convert to UUID
        attachment_key = uuid.UUID(result["Id"])

        upload_uri = result["BlobFileAccess"]["Uri"]
        headers = {
            key: value
            for key, value in zip(
                result["BlobFileAccess"]["Headers"]["Keys"],
                result["BlobFileAccess"]["Headers"]["Values"],
                strict=False,
            )
        }

        if source_path:
            # Upload from file
            with open(source_path, "rb") as file:
                file_content = file.read()
                if result["BlobFileAccess"]["RequiresAuth"]:
                    await self.request_async(
                        "PUT", upload_uri, headers=headers, content=file_content
                    )
                else:
                    with httpx.Client(**get_httpx_client_kwargs()) as client:
                        client.put(upload_uri, headers=headers, content=file_content)
        else:
            # Upload from memory
            # Convert string to bytes if needed
            if isinstance(content, str):
                content = content.encode("utf-8")

            if result["BlobFileAccess"]["RequiresAuth"]:
                await self.request_async(
                    "PUT", upload_uri, headers=headers, content=content
                )
            else:
                with httpx.Client(**get_httpx_client_kwargs()) as client:
                    client.put(upload_uri, headers=headers, content=content)

        return attachment_key

    @traced(name="attachments_get_blob_uri", run_type="uipath")
    def get_blob_file_access_uri(
        self,
        *,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> BlobFileAccessInfo:
        """Get the BlobFileAccess information for an attachment.

        This method retrieves the blob storage URI and filename for downloading
        an attachment without actually downloading the file.

        Args:
            key (uuid.UUID): The key of the attachment.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            BlobFileAccessInfo: Object containing the blob storage URI and attachment name.

        Raises:
            Exception: If the attachment is not found or the request fails.

        Examples:
            ```python
            from uipath.platform import UiPath

            client = UiPath()

            info = client.attachments.get_blob_file_access_uri(
                key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000")
            )
            print(f"Attachment ID: {info.id}")
            print(f"Blob URI: {info.uri}")
            print(f"File name: {info.name}")
            ```
        """
        spec = self._retrieve_download_uri_spec(
            key=key,
            folder_key=folder_key,
            folder_path=folder_path,
        )

        result = self.request(
            spec.method,
            url=spec.endpoint,
            params=spec.params,
            headers=spec.headers,
        ).json()

        return BlobFileAccessInfo(
            id=key,
            uri=result["BlobFileAccess"]["Uri"],
            name=result["Name"],
        )

    @traced(name="attachments_get_blob_uri", run_type="uipath")
    async def get_blob_file_access_uri_async(
        self,
        *,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> BlobFileAccessInfo:
        """Get the BlobFileAccess information for an attachment asynchronously.

        This method asynchronously retrieves the blob storage URI and filename
        for downloading an attachment without actually downloading the file.

        Args:
            key (uuid.UUID): The key of the attachment.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Returns:
            BlobFileAccessInfo: Object containing the blob storage URI and attachment name.

        Raises:
            Exception: If the attachment is not found or the request fails.

        Examples:
            ```python
            import asyncio
            from uipath.platform import UiPath

            client = UiPath()

            async def main():
                info = await client.attachments.get_blob_file_access_uri_async(
                    key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000")
                )
                print(f"Attachment ID: {info.id}")
                print(f"Blob URI: {info.uri}")
                print(f"File name: {info.name}")
            ```
        """
        spec = self._retrieve_download_uri_spec(
            key=key,
            folder_key=folder_key,
            folder_path=folder_path,
        )

        result = (
            await self.request_async(
                spec.method,
                url=spec.endpoint,
                params=spec.params,
                headers=spec.headers,
            )
        ).json()

        return BlobFileAccessInfo(
            id=key,
            uri=result["BlobFileAccess"]["Uri"],
            name=result["Name"],
        )

    @traced(name="attachments_delete", run_type="uipath")
    def delete(
        self,
        *,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> None:
        """Delete an attachment.

        This method deletes an attachment from UiPath.
        If the attachment is not found in UiPath (404 error), it will check
        for a local file in the temporary directory that matches the UUID.

        Note:
            The local file fallback functionality is intended for local development
            and debugging purposes only.

        Args:
            key (uuid.UUID): The key of the attachment to delete.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Raises:
            Exception: If the deletion fails and no local file is found.

        Examples:
            ```python
            from uipath.platform import UiPath

            client = UiPath()

            client.attachments.delete(
                key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000")
            )
            print("Attachment deleted successfully")
            ```
        """
        try:
            spec = self._delete_attachment_spec(
                key=key,
                folder_key=folder_key,
                folder_path=folder_path,
            )

            self.request(
                spec.method,
                url=spec.endpoint,
                headers=spec.headers,
            )
        except Exception as e:
            # If not found in UiPath, check local storage
            if "404" in str(e):
                # Check if file exists in temp directory
                if os.path.exists(self._temp_dir):
                    # Look for any file starting with our UUID
                    pattern = f"{key}_*"
                    matching_files = list(Path(self._temp_dir).glob(pattern))

                    if matching_files:
                        # Delete all matching files
                        for file_path in matching_files:
                            os.remove(file_path)
                        return

            # Re-raise the original exception if we can't find it locally
            raise Exception(
                f"Attachment with key {key} not found in UiPath or local storage"
            ) from e

    @traced(name="attachments_delete", run_type="uipath")
    async def delete_async(
        self,
        *,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> None:
        """Delete an attachment asynchronously.

        This method asynchronously deletes an attachment from UiPath.
        If the attachment is not found in UiPath (404 error), it will check
        for a local file in the temporary directory that matches the UUID.

        Note:
            The local file fallback functionality is intended for local development
            and debugging purposes only.

        Args:
            key (uuid.UUID): The key of the attachment to delete.
            folder_key (str | None): The key of the folder. Override the default one set in the SDK config.
            folder_path (str | None): The path of the folder. Override the default one set in the SDK config.

        Raises:
            Exception: If the deletion fails and no local file is found.

        Examples:
            ```python
            import asyncio
            from uipath.platform import UiPath

            client = UiPath()

            async def main():
                await client.attachments.delete_async(
                    key=uuid.UUID("123e4567-e89b-12d3-a456-426614174000")
                )
                print("Attachment deleted successfully")
            ```
        """
        try:
            spec = self._delete_attachment_spec(
                key=key,
                folder_key=folder_key,
                folder_path=folder_path,
            )

            await self.request_async(
                spec.method,
                url=spec.endpoint,
                headers=spec.headers,
            )
        except Exception as e:
            # If not found in UiPath, check local storage
            if "404" in str(e):
                # Check if file exists in temp directory
                if os.path.exists(self._temp_dir):
                    # Look for any file starting with our UUID
                    pattern = f"{key}_*"
                    matching_files = list(Path(self._temp_dir).glob(pattern))

                    if matching_files:
                        # Delete all matching files
                        for file_path in matching_files:
                            os.remove(file_path)
                        return

            # Re-raise the original exception if we can't find it locally
            raise Exception(
                f"Attachment with key {key} not found in UiPath or local storage"
            ) from e

    @property
    def custom_headers(self) -> dict[str, str]:
        """Return custom headers for API requests."""
        return self.folder_headers

    def _create_attachment_and_retrieve_upload_uri_spec(
        self,
        name: str,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> RequestSpec:
        return RequestSpec(
            method="POST",
            endpoint=Endpoint("/orchestrator_/odata/Attachments"),
            json={
                "Name": name,
            },
            headers={
                **header_folder(folder_key, folder_path),
            },
        )

    def _retrieve_download_uri_spec(
        self,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> RequestSpec:
        return RequestSpec(
            method="GET",
            endpoint=Endpoint(f"/orchestrator_/odata/Attachments({key})"),
            headers={
                **header_folder(folder_key, folder_path),
            },
        )

    def _delete_attachment_spec(
        self,
        key: uuid.UUID,
        folder_key: str | None = None,
        folder_path: str | None = None,
    ) -> RequestSpec:
        return RequestSpec(
            method="DELETE",
            endpoint=Endpoint(f"/orchestrator_/odata/Attachments({key})"),
            headers={
                **header_folder(folder_key, folder_path),
            },
        )
