# type: ignore # this is riddled with typing issues -- fix this later.
import asyncio
import time
from contextlib import nullcontext
from pathlib import Path
from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, Union
from uuid import UUID

from ..._utils import Endpoint
from ...tracing import traced
from ..common import BaseService, FolderContext, UiPathApiConfig, UiPathExecutionContext
from ..errors import OperationFailedException, OperationNotCompleteException
from .documents import (
    ActionPriority,
    ClassificationResponse,
    ClassificationResult,
    ExtractionResponse,
    ExtractionResponseIXP,
    FileContent,
    ProjectType,
    StartExtractionResponse,
    StartOperationResponse,
    ValidateClassificationAction,
    ValidateExtractionAction,
)

POLLING_INTERVAL = 2  # seconds
POLLING_TIMEOUT = 300  # seconds


def _must_not_be_provided(**kwargs: Any) -> None:
    for name, value in kwargs.items():
        if value is not None:
            raise ValueError(f"`{name}` must not be provided")


def _must_be_provided(**kwargs: Any) -> None:
    for name, value in kwargs.items():
        if value is None:
            raise ValueError(f"`{name}` must be provided")


def _exactly_one_must_be_provided(**kwargs: Any) -> None:
    provided = [name for name, value in kwargs.items() if value is not None]
    if len(provided) != 1:
        raise ValueError(
            f"Exactly one of `{', '.join(kwargs.keys())}` must be provided"
        )


def _validate_classify_params(
    project_type: ProjectType,
    tag: Optional[str],
    project_name: Optional[str],
    file: Optional[FileContent],
    file_path: Optional[str],
) -> None:
    _exactly_one_must_be_provided(file=file, file_path=file_path)
    if project_type == ProjectType.PRETRAINED:
        _must_not_be_provided(
            project_name=project_name,
            tag=tag,
        )
    else:
        _must_be_provided(
            project_name=project_name,
            tag=tag,
        )


def _validate_extract_params_and_get_project_type(
    tag: Optional[str],
    project_name: Optional[str],
    file: Optional[FileContent],
    file_path: Optional[str],
    classification_result: Optional[ClassificationResult],
    project_type: Optional[ProjectType],
    document_type_name: Optional[str],
) -> ProjectType:
    if file or file_path:
        _exactly_one_must_be_provided(file=file, file_path=file_path)
        _must_be_provided(project_type=project_type)
        _must_not_be_provided(
            classification_result=classification_result,
        )

        if project_type == ProjectType.PRETRAINED:
            _must_not_be_provided(tag=tag, project_name=project_name)
            _must_be_provided(document_type_name=document_type_name)
        elif project_type == ProjectType.MODERN:
            _must_be_provided(
                project_name=project_name,
                tag=tag,
                document_type_name=document_type_name,
            )
        else:
            _must_be_provided(project_name=project_name, tag=tag)
            _must_not_be_provided(document_type_name=document_type_name)
    else:
        _must_be_provided(classification_result=classification_result)
        _must_not_be_provided(
            tag=tag,
            project_name=project_name,
            project_type=project_type,
            file=file,
            file_path=file_path,
            document_type_name=document_type_name,
        )
        project_type = classification_result.project_type

    return project_type


class DocumentsService(FolderContext, BaseService):
    """Service for managing UiPath DocumentUnderstanding Document Operations.

    This service provides methods to extract data from documents using UiPath's Document Understanding capabilities.

    !!! warning "Preview Feature"
        This function is currently experimental.
        Behavior and parameters are subject to change in future versions.
    """

    def __init__(
        self,
        config: UiPathApiConfig,
        execution_context: UiPathExecutionContext,
        polling_interval: float = POLLING_INTERVAL,
        polling_timeout: float = POLLING_TIMEOUT,
    ) -> None:
        super().__init__(config=config, execution_context=execution_context)
        self.polling_interval = polling_interval
        self.polling_timeout = polling_timeout

    def _get_common_headers(self) -> Dict[str, str]:
        return {
            "X-UiPath-Internal-ConsumptionSourceType": "CodedAgents",
        }

    def _get_project_id_by_name(
        self, project_name: str, project_type: ProjectType
    ) -> str:
        response = self.request(
            "GET",
            url=Endpoint("/du_/api/framework/projects"),
            params={"api-version": 1.1, "type": project_type.value},
            headers=self._get_common_headers(),
        )

        try:
            return next(
                project["id"]
                for project in response.json()["projects"]
                if project["name"] == project_name
            )
        except StopIteration:
            raise ValueError(f"Project '{project_name}' not found.") from None

    async def _get_project_id_by_name_async(
        self, project_name: str, project_type: ProjectType
    ) -> str:
        response = await self.request_async(
            "GET",
            url=Endpoint("/du_/api/framework/projects"),
            params={"api-version": 1.1, "type": project_type.value},
            headers=self._get_common_headers(),
        )

        try:
            return next(
                project["id"]
                for project in response.json()["projects"]
                if project["name"] == project_name
            )
        except StopIteration:
            raise ValueError(f"Project '{project_name}' not found.") from None

    def _get_project_tags(self, project_id: str) -> Set[str]:
        response = self.request(
            "GET",
            url=Endpoint(f"/du_/api/framework/projects/{project_id}/tags"),
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        )
        return {tag["name"] for tag in response.json().get("tags", [])}

    async def _get_project_tags_async(self, project_id: str) -> Set[str]:
        response = await self.request_async(
            "GET",
            url=Endpoint(f"/du_/api/framework/projects/{project_id}/tags"),
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        )
        return {tag["name"] for tag in response.json().get("tags", [])}

    def _get_document_id(
        self,
        project_id: Optional[str],
        file: Optional[FileContent],
        file_path: Optional[str],
        classification_result: Optional[ClassificationResult],
    ) -> str:
        if classification_result is not None:
            return classification_result.document_id

        document_id = self._start_digitization(
            project_id=project_id,
            file=file,
            file_path=file_path,
        )
        self._wait_for_digitization(
            project_id=project_id,
            document_id=document_id,
        )

        return document_id

    async def _get_document_id_async(
        self,
        project_id: Optional[str],
        file: Optional[FileContent],
        file_path: Optional[str],
        classification_result: Optional[ClassificationResult],
    ) -> str:
        if classification_result is not None:
            return classification_result.document_id

        document_id = await self._start_digitization_async(
            project_id=project_id,
            file=file,
            file_path=file_path,
        )
        await self._wait_for_digitization_async(
            project_id=project_id,
            document_id=document_id,
        )

        return document_id

    def _get_project_id_and_tag(
        self,
        tag: Optional[str],
        project_name: Optional[str],
        project_type: Optional[ProjectType],
        classification_result: Optional[ClassificationResult],
    ) -> Tuple[str, Optional[str]]:
        if project_name is not None:
            project_id = self._get_project_id_by_name(
                project_name,
                project_type,
            )
        elif project_type == ProjectType.PRETRAINED:
            return str(UUID(int=0)), None
        else:
            project_id = classification_result.project_id
            tag = classification_result.tag

        tags = self._get_project_tags(project_id)
        if tag not in tags:
            raise ValueError(
                f"Tag '{tag}' not found in project '{project_name}'. Available tags: {tags}"
            )

        return project_id, tag

    async def _get_project_id_and_tag_async(
        self,
        tag: Optional[str],
        project_name: Optional[str],
        project_type: Optional[ProjectType],
        classification_result: Optional[ClassificationResult],
    ) -> Tuple[str, Optional[str]]:
        if project_name is not None:
            project_id = await self._get_project_id_by_name_async(
                project_name,
                project_type,
            )
        elif project_type == ProjectType.PRETRAINED:
            return str(UUID(int=0)), None
        else:
            project_id = classification_result.project_id
            tag = classification_result.tag

        tags = await self._get_project_tags_async(project_id)
        if tag not in tags:
            raise ValueError(
                f"Tag '{tag}' not found in project '{project_name}'. Available tags: {tags}"
            )

        return project_id, tag

    def _start_digitization(
        self,
        project_id: str,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> str:
        with open(Path(file_path), "rb") if file_path else nullcontext(file) as handle:
            return self.request(
                "POST",
                url=Endpoint(
                    f"/du_/api/framework/projects/{project_id}/digitization/start"
                ),
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
                files={"File": handle},
            ).json()["documentId"]

    async def _start_digitization_async(
        self,
        project_id: str,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> str:
        with open(Path(file_path), "rb") if file_path else nullcontext(file) as handle:
            return (
                await self.request_async(
                    "POST",
                    url=Endpoint(
                        f"/du_/api/framework/projects/{project_id}/digitization/start"
                    ),
                    params={"api-version": 1.1},
                    headers=self._get_common_headers(),
                    files={"File": handle},
                )
            ).json()["documentId"]

    def _wait_for_digitization(self, project_id: str, document_id: str) -> None:
        def result_getter() -> Tuple[str, Optional[str], Optional[str]]:
            result = self.request(
                method="GET",
                url=Endpoint(
                    f"/du_/api/framework/projects/{project_id}/digitization/result/{document_id}"
                ),
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

    async def _wait_for_digitization_async(
        self, project_id: str, document_id: str
    ) -> None:
        async def result_getter() -> Tuple[str, Optional[str], Optional[str]]:
            result = (
                await self.request_async(
                    method="GET",
                    url=Endpoint(
                        f"/du_/api/framework/projects/{project_id}/digitization/result/{document_id}"
                    ),
                    params={"api-version": 1.1},
                    headers=self._get_common_headers(),
                )
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

    def _get_document_type_id(
        self,
        project_id: str,
        document_type_name: Optional[str],
        project_type: ProjectType,
        classification_result: Optional[ClassificationResult],
    ) -> str:
        if project_type == ProjectType.IXP:
            return str(UUID(int=0))

        if classification_result is not None:
            return classification_result.document_type_id

        response = self.request(
            "GET",
            url=Endpoint(f"/du_/api/framework/projects/{project_id}/document-types"),
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        )

        try:
            return next(
                extractor["id"]
                for extractor in response.json().get("documentTypes", [])
                if extractor["name"].lower() == document_type_name.lower()
            )
        except StopIteration:
            raise ValueError(
                f"Document type '{document_type_name}' not found."
            ) from None

    async def _get_document_type_id_async(
        self,
        project_id: str,
        document_type_name: Optional[str],
        project_type: ProjectType,
        classification_result: Optional[ClassificationResult],
    ) -> str:
        if project_type == ProjectType.IXP:
            return str(UUID(int=0))

        if classification_result is not None:
            return classification_result.document_type_id

        response = await self.request_async(
            "GET",
            url=Endpoint(f"/du_/api/framework/projects/{project_id}/document-types"),
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        )

        try:
            return next(
                extractor["id"]
                for extractor in response.json().get("documentTypes", [])
                if extractor["name"].lower() == document_type_name.lower()
            )
        except StopIteration:
            raise ValueError(
                f"Document type '{document_type_name}' not found."
            ) from None

    def _start_extraction(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        document_id: str,
    ) -> StartExtractionResponse:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/start"
            )

        operation_id = self.request(
            "POST",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
            json={"documentId": document_id},
        ).json()["operationId"]

        return StartExtractionResponse(
            operation_id=operation_id,
            document_id=document_id,
            project_id=project_id,
            tag=tag,
        )

    async def _start_extraction_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        document_id: str,
    ) -> StartExtractionResponse:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/start"
            )

        operation_id = (
            await self.request_async(
                "POST",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
                json={"documentId": document_id},
            )
        ).json()["operationId"]

        return StartExtractionResponse(
            operation_id=operation_id,
            document_id=document_id,
            project_id=project_id,
            tag=tag,
        )

    def _wait_for_operation(
        self,
        result_getter: Callable[[], Tuple[Any, Optional[Any], Optional[Any]]],
        wait_statuses: List[str],
        success_status: str,
    ) -> Any:
        start_time = time.monotonic()
        status = wait_statuses[0]
        result = None

        while (
            status in wait_statuses
            and (time.monotonic() - start_time) < self.polling_timeout
        ):
            status, error, result = result_getter()
            time.sleep(self.polling_interval)

        if status != success_status:
            if time.monotonic() - start_time >= self.polling_timeout:
                raise TimeoutError("Operation timed out.")
            raise RuntimeError(
                f"Operation failed with status: {status}, error: {error}"
            )

        return result

    async def _wait_for_operation_async(
        self,
        result_getter: Callable[
            [], Awaitable[Tuple[Any, Optional[Any], Optional[Any]]]
        ],
        wait_statuses: List[str],
        success_status: str,
    ) -> Any:
        start_time = time.monotonic()
        status = wait_statuses[0]
        result = None

        while (
            status in wait_statuses
            and (time.monotonic() - start_time) < self.polling_timeout
        ):
            status, error, result = await result_getter()
            await asyncio.sleep(self.polling_interval)

        if status != success_status:
            if time.monotonic() - start_time >= self.polling_timeout:
                raise TimeoutError("Operation timed out.")
            raise RuntimeError(
                f"Operation failed with status: {status}, error: {error}"
            )

        return result

    def _wait_for_extraction(
        self,
        project_id: str,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
        project_type: ProjectType,
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        def result_getter() -> Tuple[str, str, Any]:
            if project_type == ProjectType.PRETRAINED:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}"
                )
            else:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}"
                )

            result = self.request(
                method="GET",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        extraction_response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        extraction_response["projectId"] = project_id
        extraction_response["tag"] = tag
        extraction_response["documentTypeId"] = document_type_id
        extraction_response["projectType"] = project_type

        if project_type == ProjectType.IXP:
            return ExtractionResponseIXP.model_validate(extraction_response)

        return ExtractionResponse.model_validate(extraction_response)

    async def _wait_for_extraction_async(
        self,
        project_id: str,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
        project_type: ProjectType,
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        async def result_getter() -> Tuple[str, str, Any]:
            if project_type == ProjectType.PRETRAINED:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/extraction/result/{operation_id}"
                )
            else:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}"
                )

            result = (
                await self.request_async(
                    method="GET",
                    url=url,
                    params={"api-version": 1.1},
                    headers=self._get_common_headers(),
                )
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        extraction_response = await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        extraction_response["projectId"] = project_id
        extraction_response["tag"] = tag
        extraction_response["documentTypeId"] = document_type_id
        extraction_response["projectType"] = project_type

        if project_type == ProjectType.IXP:
            return ExtractionResponseIXP.model_validate(extraction_response)

        return ExtractionResponse.model_validate(extraction_response)

    def _start_classification(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_id: str,
    ) -> str:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classification/start"
            )

        return self.request(
            "POST",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
            json={"documentId": document_id},
        ).json()["operationId"]

    async def _start_classification_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_id: str,
    ) -> str:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classification/start"
            )

        return (
            await self.request_async(
                "POST",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
                json={"documentId": document_id},
            )
        ).json()["operationId"]

    def _wait_for_classification(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> List[ClassificationResult]:
        def result_getter() -> Tuple[str, Optional[str], Optional[str]]:
            if project_type == ProjectType.PRETRAINED:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}"
                )
            else:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/{tag}/classification/result/{operation_id}"
                )

            result = self.request(
                method="GET",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        classification_response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )
        for classification_result in classification_response["classificationResults"]:
            classification_result["ProjectId"] = project_id
            classification_result["ProjectType"] = project_type
            classification_result["Tag"] = tag

        return ClassificationResponse.model_validate(
            classification_response
        ).classification_results

    async def _wait_for_classification_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> List[ClassificationResult]:
        async def result_getter() -> Tuple[str, Optional[str], Optional[str]]:
            if project_type == ProjectType.PRETRAINED:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/classification/result/{operation_id}"
                )
            else:
                url = Endpoint(
                    f"/du_/api/framework/projects/{project_id}/{tag}/classification/result/{operation_id}"
                )

            result = (
                await self.request_async(
                    method="GET",
                    url=url,
                    params={"api-version": 1.1},
                    headers=self._get_common_headers(),
                )
            ).json()
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        classification_response = await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )
        for classification_result in classification_response["classificationResults"]:
            classification_result["ProjectId"] = project_id
            classification_result["ProjectType"] = project_type
            classification_result["Tag"] = tag

        return ClassificationResponse.model_validate(
            classification_response
        ).classification_results

    @traced(name="documents_classify", run_type="uipath")
    def classify(
        self,
        project_type: ProjectType,
        tag: Optional[str] = None,
        project_name: Optional[str] = None,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> List[ClassificationResult]:
        """Classify a document using a DU Modern project.

        Args:
            project_type (ProjectType): Type of the project.
            project_name (str, optional): Name of the [DU Modern](https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/about-document-understanding) project. Must be provided if `project_type` is not `ProjectType.PRETRAINED`.
            tag (str, optional): Tag of the published project version. Must be provided if `project_type` is not `ProjectType.PRETRAINED`.
            file (FileContent, optional): The document file to be classified.
            file_path (str, optional): Path to the document file to be classified.

        Note:
            Either `file` or `file_path` must be provided, but not both.

        Returns:
            List[ClassificationResult]: A list of classification results.

        Examples:
            ```python
            Modern DU project:
            with open("path/to/document.pdf", "rb") as file:
                classification_results = service.classify(
                    project_name="MyModernProjectName",
                    tag="Production",
                    file=file,
                )

            Pretrained project:
            with open("path/to/document.pdf", "rb") as file:
                classification_results = service.classify(
                    project_type=ProjectType.PRETRAINED,
                    file=file,
                )
            ```
        """
        _validate_classify_params(
            project_type=project_type,
            tag=tag,
            project_name=project_name,
            file=file,
            file_path=file_path,
        )

        project_id, tag = self._get_project_id_and_tag(
            tag=tag,
            project_name=project_name,
            project_type=project_type,
            classification_result=None,
        )

        document_id = self._get_document_id(
            project_id=project_id,
            file=file,
            file_path=file_path,
            classification_result=None,
        )

        operation_id = self._start_classification(
            project_id=project_id,
            project_type=project_type,
            tag=tag,
            document_id=document_id,
        )
        return self._wait_for_classification(
            project_id=project_id,
            project_type=project_type,
            tag=tag,
            operation_id=operation_id,
        )

    @traced(name="documents_classify_async", run_type="uipath")
    async def classify_async(
        self,
        project_type: ProjectType,
        tag: Optional[str] = None,
        project_name: Optional[str] = None,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> List[ClassificationResult]:
        """Asynchronously version of the [`classify`][uipath.platform.documents._documents_service.DocumentsService.classify] method."""
        _validate_classify_params(
            project_type=project_type,
            tag=tag,
            project_name=project_name,
            file=file,
            file_path=file_path,
        )

        project_id, tag = await self._get_project_id_and_tag_async(
            tag=tag,
            project_name=project_name,
            project_type=project_type,
            classification_result=None,
        )

        document_id = await self._get_document_id_async(
            project_id=project_id,
            file=file,
            file_path=file_path,
            classification_result=None,
        )

        operation_id = await self._start_classification_async(
            project_id=project_id,
            project_type=project_type,
            tag=tag,
            document_id=document_id,
        )

        return await self._wait_for_classification_async(
            project_id=project_id,
            project_type=project_type,
            tag=tag,
            operation_id=operation_id,
        )

    @traced(name="documents_start_ixp_extraction", run_type="uipath")
    def start_ixp_extraction(
        self,
        project_name: str,
        tag: str,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> StartExtractionResponse:
        """Start an IXP extraction process without waiting for results (non-blocking).

        This method uploads the file as an attachment and starts the extraction process,
        returning immediately without waiting for the extraction to complete.
        Use this for async workflows where you want to receive results via callback/webhook.

        Args:
            project_name (str): Name of the IXP project.
            tag (str): Tag of the published project version (e.g., "staging").
            file (FileContent, optional): The document file to be processed.
            file_path (str, optional): Path to the document file to be processed.

        Note:
            Either `file` or `file_path` must be provided, but not both.

        Returns:
            ExtractionStartResponse: Contains the operation_id, document_id, project_id, and tag

        Examples:
            ```python
            start_response = uipath.documents.start_ixp_extraction(
                project_name="MyIXPProjectName",
                tag="staging",
                file_path="path/to/document.pdf",
            )
            # start_response.operation_id can be used to poll for results later
            ```
        """
        _exactly_one_must_be_provided(file=file, file_path=file_path)

        project_id = self._get_project_id_by_name(project_name, ProjectType.IXP)

        document_id = self._start_digitization(
            project_id=project_id,
            file=file,
            file_path=file_path,
        )

        return self._start_extraction(
            project_id=project_id,
            project_type=ProjectType.IXP,
            tag=tag,
            document_type_id=str(UUID(int=0)),
            document_id=document_id,
        )

    @traced(name="documents_start_ixp_extraction_async", run_type="uipath")
    async def start_ixp_extraction_async(
        self,
        project_name: str,
        tag: str,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
    ) -> StartExtractionResponse:
        """Asynchronous version of the [`start_ixp_extraction`][uipath.platform.documents._documents_service.DocumentsService.start_ixp_extraction] method."""
        _exactly_one_must_be_provided(file=file, file_path=file_path)

        project_id = await self._get_project_id_by_name_async(
            project_name, ProjectType.IXP
        )

        document_id = await self._start_digitization_async(
            project_id=project_id,
            file=file,
            file_path=file_path,
        )

        return await self._start_extraction_async(
            project_id=project_id,
            project_type=ProjectType.IXP,
            tag=tag,
            document_type_id=str(UUID(int=0)),
            document_id=document_id,
        )

    def _retrieve_operation_result(
        self,
        url: Endpoint,
        operation_id: str,
        operation_name: str,
    ) -> Dict:
        response = self.request(
            method="GET",
            url=url,
            params={"api-version": "1.1"},
            headers=self._get_common_headers(),
        ).json()

        status = response.get("status")
        if status in ["NotStarted", "Running"]:
            raise OperationNotCompleteException(
                operation_id=operation_id,
                status=response.get("status"),
                operation_name=operation_name,
            )

        if status != "Succeeded":
            raise OperationFailedException(
                operation_id=operation_id,
                status=status,
                error=response.get("error"),
                operation_name=operation_name,
            )

        return response.get("result")

    async def _retrieve_operation_result_async(
        self,
        url: Endpoint,
        operation_id: str,
        operation_name: str,
    ) -> Dict:
        response = (
            await self.request_async(
                method="GET",
                url=url,
                params={"api-version": "1.1"},
                headers=self._get_common_headers(),
            )
        ).json()

        status = response.get("status")
        if status in ["NotStarted", "Running"]:
            raise OperationNotCompleteException(
                operation_id=operation_id,
                status=response.get("status"),
                operation_name=operation_name,
            )

        if status != "Succeeded":
            raise OperationFailedException(
                operation_id=operation_id,
                status=status,
                error=response.get("error"),
                operation_name=operation_name,
            )

        return response.get("result")

    @traced(name="documents_retrieve_ixp_extraction_result", run_type="uipath")
    def retrieve_ixp_extraction_result(
        self,
        project_id: str,
        tag: str,
        operation_id: str,
    ) -> ExtractionResponseIXP:
        """Retrieve the result of an IXP extraction operation (single-shot, non-blocking).

        This method retrieves the result of an IXP extraction that was previously started
        with `start_ixp_extraction`. It does not poll - it makes a single request and
        returns the result if available, or raises an exception if not complete.

        Args:
            project_id (str): The ID of the IXP project.
            tag (str): The tag of the published project version.
            operation_id (str): The operation ID returned from `start_ixp_extraction`.

        Returns:
            ExtractionResponseIXP: The extraction response containing the extracted data.

        Raises:
            OperationNotCompleteException: If the extraction is not yet complete.
            OperationFailedException: If the extraction operation failed.

        Examples:
            ```python
            # After receiving a callback/webhook that extraction is complete:
            result = service.retrieve_ixp_extraction_result(
                project_id=start_response.project_id,
                tag=start_response.tag,
                operation_id=start_response.operation_id,
            )
            ```
        """
        document_type_id = str(UUID(int=0))

        url = Endpoint(
            f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}"
        )

        extraction_response = self._retrieve_operation_result(
            url=url,
            operation_id=operation_id,
            operation_name="IXP extraction",
        )

        extraction_response["projectId"] = project_id
        extraction_response["tag"] = tag
        extraction_response["documentTypeId"] = document_type_id
        extraction_response["projectType"] = ProjectType.IXP

        return ExtractionResponseIXP.model_validate(extraction_response)

    @traced(name="documents_retrieve_ixp_extraction_result_async", run_type="uipath")
    async def retrieve_ixp_extraction_result_async(
        self,
        project_id: str,
        tag: str,
        operation_id: str,
    ) -> ExtractionResponseIXP:
        """Asynchronous version of the [`retrieve_ixp_extraction_result`][uipath.platform.documents._documents_service.DocumentsService.retrieve_ixp_extraction_result] method."""
        document_type_id = str(UUID(int=0))

        url = Endpoint(
            f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/extraction/result/{operation_id}"
        )

        extraction_response = await self._retrieve_operation_result_async(
            url=url,
            operation_id=operation_id,
            operation_name="IXP extraction",
        )

        extraction_response["projectId"] = project_id
        extraction_response["tag"] = tag
        extraction_response["documentTypeId"] = document_type_id
        extraction_response["projectType"] = ProjectType.IXP

        return ExtractionResponseIXP.model_validate(extraction_response)

    @traced(name="documents_extract", run_type="uipath")
    def extract(
        self,
        tag: Optional[str] = None,
        project_name: Optional[str] = None,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
        classification_result: Optional[ClassificationResult] = None,
        project_type: Optional[ProjectType] = None,
        document_type_name: Optional[str] = None,
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        """Extract predicted data from a document using an DU Modern/IXP project.

        Args:
            project_name (str, optional): Name of the [IXP](https://docs.uipath.com/ixp/automation-cloud/latest/overview/managing-projects#creating-a-new-project)/[DU Modern](https://docs.uipath.com/document-understanding/automation-cloud/latest/user-guide/about-document-understanding) project. Must be provided if `classification_result` is not provided.
            tag (str): Tag of the published project version. Must be provided if `classification_result` is not provided and `project_type` is not `ProjectType.PRETRAINED`.
            file (FileContent, optional): The document file to be processed. Must be provided if `classification_result` is not provided.
            file_path (str, optional): Path to the document file to be processed. Must be provided if `classification_result` is not provided.
            project_type (ProjectType, optional): Type of the project. Must be provided if `project_name` is provided.
            document_type_name (str, optional): Document type name associated with the extractor to be used for extraction. Required if `project_type` is `ProjectType.MODERN` and `project_name` is provided.
            classification_result (ClassificationResult, optional): The classification result obtained from a previous classification step. If provided, `project_name`, `project_type`, `file`, `file_path`, and `document_type_name` must not be provided.

        Note:
            Either `file` or `file_path` must be provided, but not both.

        Returns:
            Union[ExtractionResponse, ExtractionResponseIXP]: The extraction response containing the extracted data.

        Examples:
            IXP projects:
            ```python
            with open("path/to/document.pdf", "rb") as file:
                extraction_response = service.extract(
                    project_name="MyIXPProjectName",
                    tag="live",
                    file=file,
                )
            ```

            DU Modern projects (providing document type name):
            ```python
            with open("path/to/document.pdf", "rb") as file:
                extraction_response = service.extract(
                    project_name="MyModernProjectName",
                    tag="Production",
                    file=file,
                    project_type=ProjectType.MODERN,
                    document_type_name="Receipts",
                )
            ```

            DU Modern projects (using existing classification result):
            ```python
            with open("path/to/document.pdf", "rb") as file:
                classification_results = uipath.documents.classify(
                    tag="Production",
                    project_name="MyModernProjectName",
                    file=file,
                )

            extraction_result = uipath.documents.extract(
                classification_result=max(classification_results, key=lambda result: result.confidence),
            )
            ```
        """
        project_type = _validate_extract_params_and_get_project_type(
            tag=tag,
            project_name=project_name,
            file=file,
            file_path=file_path,
            classification_result=classification_result,
            project_type=project_type,
            document_type_name=document_type_name,
        )

        project_id, tag = self._get_project_id_and_tag(
            tag=tag,
            project_name=project_name,
            project_type=project_type,
            classification_result=classification_result,
        )

        document_id = self._get_document_id(
            project_id=project_id,
            file=file,
            file_path=file_path,
            classification_result=classification_result,
        )

        document_type_id = self._get_document_type_id(
            project_id=project_id,
            document_type_name=document_type_name,
            project_type=project_type,
            classification_result=classification_result,
        )

        operation_id = self._start_extraction(
            project_id=project_id,
            project_type=project_type,
            tag=tag,
            document_type_id=document_type_id,
            document_id=document_id,
        ).operation_id

        return self._wait_for_extraction(
            project_id=project_id,
            tag=tag,
            document_type_id=document_type_id,
            operation_id=operation_id,
            project_type=project_type,
        )

    @traced(name="documents_extract_async", run_type="uipath")
    async def extract_async(
        self,
        tag: Optional[str] = None,
        project_name: Optional[str] = None,
        file: Optional[FileContent] = None,
        file_path: Optional[str] = None,
        classification_result: Optional[ClassificationResult] = None,
        project_type: Optional[ProjectType] = None,
        document_type_name: Optional[str] = None,
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        """Asynchronously version of the [`extract`][uipath.platform.documents._documents_service.DocumentsService.extract] method."""
        project_type = _validate_extract_params_and_get_project_type(
            tag=tag,
            project_name=project_name,
            file=file,
            file_path=file_path,
            classification_result=classification_result,
            project_type=project_type,
            document_type_name=document_type_name,
        )

        project_id, tag = await self._get_project_id_and_tag_async(
            tag=tag,
            project_name=project_name,
            project_type=project_type,
            classification_result=classification_result,
        )

        document_id = await self._get_document_id_async(
            project_id=project_id,
            file=file,
            file_path=file_path,
            classification_result=classification_result,
        )

        document_type_id = await self._get_document_type_id_async(
            project_id=project_id,
            document_type_name=document_type_name,
            project_type=project_type,
            classification_result=classification_result,
        )

        operation_id = (
            await self._start_extraction_async(
                project_id=project_id,
                project_type=project_type,
                tag=tag,
                document_type_id=document_type_id,
                document_id=document_id,
            )
        ).operation_id

        return await self._wait_for_extraction_async(
            project_id=project_id,
            tag=tag,
            document_type_id=document_type_id,
            operation_id=operation_id,
            project_type=project_type,
        )

    def _start_classification_validation(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        classification_results: List[ClassificationResult],
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> str:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start"
            )

        return self.request(
            "POST",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
            json={
                "classificationResults": [
                    cr.model_dump() for cr in classification_results
                ],
                "documentId": classification_results[0].document_id,
                "actionTitle": action_title,
                "actionPriority": action_priority,
                "actionCatalog": action_catalog,
                "actionFolder": action_folder,
                "storageBucketName": storage_bucket_name,
                "storageBucketDirectoryPath": storage_bucket_directory_path,
            },
        ).json()["operationId"]

    async def _start_classification_validation_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        classification_results: List[ClassificationResult],
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> str:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/start"
            )

        return (
            await self.request_async(
                "POST",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
                json={
                    "classificationResults": [
                        cr.model_dump() for cr in classification_results
                    ],
                    "documentId": classification_results[0].document_id,
                    "actionTitle": action_title,
                    "actionPriority": action_priority,
                    "actionCatalog": action_catalog,
                    "actionFolder": action_folder,
                    "storageBucketName": storage_bucket_name,
                    "storageBucketDirectoryPath": storage_bucket_directory_path,
                },
            )
        ).json()["operationId"]

    def _start_extraction_validation(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        action_title: str,
        action_priority: Optional[ActionPriority],
        action_catalog: Optional[str],
        action_folder: Optional[str],
        storage_bucket_name: Optional[str],
        storage_bucket_directory_path: Optional[str],
        extraction_response: ExtractionResponse,
    ) -> StartOperationResponse:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/start"
            )

        operation_id = self.request(
            "POST",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
            json={
                "extractionResult": extraction_response.extraction_result.model_dump(),
                "documentId": extraction_response.extraction_result.document_id,
                "actionTitle": action_title,
                "actionPriority": action_priority,
                "actionCatalog": action_catalog,
                "actionFolder": action_folder,
                "storageBucketName": storage_bucket_name,
                "allowChangeOfDocumentType": True,
                "storageBucketDirectoryPath": storage_bucket_directory_path,
            },
        ).json()["operationId"]

        return StartOperationResponse(
            operation_id=operation_id,
            document_id=extraction_response.extraction_result.document_id,
            project_id=project_id,
            tag=tag,
        )

    async def _start_extraction_validation_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        action_title: str,
        action_priority: Optional[ActionPriority],
        action_catalog: Optional[str],
        action_folder: Optional[str],
        storage_bucket_name: Optional[str],
        storage_bucket_directory_path: Optional[str],
        extraction_response: ExtractionResponse,
    ) -> StartOperationResponse:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/start"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/start"
            )

        operation_id = (
            await self.request_async(
                "POST",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
                json={
                    "extractionResult": extraction_response.extraction_result.model_dump(),
                    "documentId": extraction_response.extraction_result.document_id,
                    "actionTitle": action_title,
                    "actionPriority": action_priority,
                    "actionCatalog": action_catalog,
                    "actionFolder": action_folder,
                    "storageBucketName": storage_bucket_name,
                    "allowChangeOfDocumentType": True,
                    "storageBucketDirectoryPath": storage_bucket_directory_path,
                },
            )
        ).json()["operationId"]

        return StartOperationResponse(
            operation_id=operation_id,
            document_id=extraction_response.extraction_result.document_id,
            project_id=project_id,
            tag=tag,
        )

    @traced(name="documents_start_ixp_extraction_validation", run_type="uipath")
    def start_ixp_extraction_validation(
        self,
        extraction_response: ExtractionResponseIXP,
        action_title: str,
        action_catalog: Optional[str] = None,
        action_priority: Optional[ActionPriority] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> StartOperationResponse:
        """Start an IXP extraction validation action without waiting for results (non-blocking).

        Args:
            extraction_response (ExtractionResponseIXP): The extraction response from the IXP extraction process.
            action_title (str): The title of the validation action.
            action_catalog (str, optional): The catalog of the validation action.
            action_priority (ActionPriority, optional): The priority of the validation action.
            action_folder (str, optional): The folder of the validation action.
            storage_bucket_name (str, optional): The name of the storage bucket where validation data will be stored.
            storage_bucket_directory_path (str, optional): The directory path within the storage bucket.

        Returns:
            StartOperationResponse: Contains the operation_id, document_id, project_id, and tag.

        Examples:
            ```python
            start_operation_response = service.start_ixp_extraction_validation(
                action_title="Validate IXP Extraction",
                action_priority=ActionPriority.HIGH,
                action_catalog="DefaultCatalog",
                action_folder="Validations",
                storage_bucket_name="my-storage-bucket",
                storage_bucket_directory_path="validations/ixp",
                extraction_response=extraction_response,
            )
            # start_operation_response can be used to poll for validation results later
            ```
        """
        return self._start_extraction_validation(
            project_id=extraction_response.project_id,
            project_type=ProjectType.IXP,
            tag=extraction_response.tag,
            document_type_id=str(UUID(int=0)),
            action_title=action_title,
            action_priority=action_priority,
            action_catalog=action_catalog,
            action_folder=action_folder,
            storage_bucket_name=storage_bucket_name,
            storage_bucket_directory_path=storage_bucket_directory_path,
            extraction_response=extraction_response,
        )

    @traced(
        name="documents_start_ixp_extraction_validation_async",
        run_type="uipath",
    )
    async def start_ixp_extraction_validation_async(
        self,
        extraction_response: ExtractionResponseIXP,
        action_title: str,
        action_catalog: Optional[str] = None,
        action_priority: Optional[ActionPriority] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> StartOperationResponse:
        """Asynchronous version of the [`start_ixp_extraction_validation`][uipath.platform.documents._documents_service.DocumentsService.start_ixp_extraction_validation] method."""
        return await self._start_extraction_validation_async(
            project_id=extraction_response.project_id,
            project_type=ProjectType.IXP,
            tag=extraction_response.tag,
            document_type_id=str(UUID(int=0)),
            action_title=action_title,
            action_priority=action_priority,
            action_catalog=action_catalog,
            action_folder=action_folder,
            storage_bucket_name=storage_bucket_name,
            storage_bucket_directory_path=storage_bucket_directory_path,
            extraction_response=extraction_response,
        )

    @traced(
        name="documents_retrieve_ixp_extraction_validation_result",
        run_type="uipath",
    )
    def retrieve_ixp_extraction_validation_result(
        self,
        project_id: str,
        tag: str,
        operation_id: str,
    ) -> ValidateExtractionAction:
        """Retrieve the result of an IXP create validate extraction action operation (single-shot, non-blocking).

        This method retrieves the result of an IXP create validate extraction action that was previously started
        with `start_ixp_extraction_validation`. It does not poll - it makes a single request and
        returns the result if available, or raises an exception if not complete.

        Args:
            operation_id (str): The operation ID returned from `start_ixp_extraction_validation`.
            project_id (str): The ID of the IXP project.
            tag (str): The tag of the published project version.

        Returns:
            ValidateExtractionAction: The validation action

        Raises:
            OperationNotCompleteException: If the validation action is not yet complete.
            OperationFailedException: If the validation action has failed.

        Examples:
            ```python
            # After receiving a callback/webhook that validation is complete:
            validation_result = service.retrieve_ixp_extraction_validation_result(
                operation_id=start_operation_response.operation_id,
                project_id=start_operation_response.project_id,
                tag=start_operation_response.tag,
            )
            ```
        """
        document_type_id = str(UUID(int=0))

        url = Endpoint(
            f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}"
        )

        result = self._retrieve_operation_result(
            url=url,
            operation_id=operation_id,
            operation_name="IXP Create Validate Extraction Action",
        )

        result["projectId"] = project_id
        result["projectType"] = ProjectType.IXP
        result["tag"] = tag
        result["documentTypeId"] = str(UUID(int=0))
        result["operationId"] = operation_id

        return ValidateExtractionAction.model_validate(result)

    @traced(
        name="documents_retrieve_ixp_extraction_validation_result_async",
        run_type="uipath",
    )
    async def retrieve_ixp_extraction_validation_result_async(
        self,
        project_id: str,
        tag: str,
        operation_id: str,
    ) -> ValidateExtractionAction:
        """Asynchronous version of the [`retrieve_ixp_extraction_validation_result`][uipath.platform.documents._documents_service.DocumentsService.retrieve_ixp_extraction_validation_result] method."""
        document_type_id = str(UUID(int=0))

        url = Endpoint(
            f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}"
        )

        result = await self._retrieve_operation_result_async(
            url=url,
            operation_id=operation_id,
            operation_name="IXP Create Validate Extraction Action",
        )

        result["projectId"] = project_id
        result["projectType"] = ProjectType.IXP
        result["tag"] = tag
        result["documentTypeId"] = str(UUID(int=0))
        result["operationId"] = operation_id

        return ValidateExtractionAction.model_validate(result)

    def _get_classification_validation_result(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> Dict:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/result/{operation_id}"
            )

        return self.request(
            method="GET",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        ).json()

    async def _get_classification_validation_result_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> Dict:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/classifiers/ml-classification/validation/result/{operation_id}"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/classifiers/validation/result/{operation_id}"
            )

        return (
            await self.request_async(
                method="GET",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
            )
        ).json()

    def _get_extraction_validation_result(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
    ) -> Dict:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}"
            )

        return self.request(
            method="GET",
            url=url,
            params={"api-version": 1.1},
            headers=self._get_common_headers(),
        ).json()

    async def _get_extraction_validation_result_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
    ) -> Dict:
        if project_type == ProjectType.PRETRAINED:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/extractors/{document_type_id}/validation/result/{operation_id}"
            )
        else:
            url = Endpoint(
                f"/du_/api/framework/projects/{project_id}/{tag}/document-types/{document_type_id}/validation/result/{operation_id}"
            )

        return (
            await self.request_async(
                method="GET",
                url=url,
                params={"api-version": 1.1},
                headers=self._get_common_headers(),
            )
        ).json()

    def _wait_for_create_validate_classification_action(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> ValidateClassificationAction:
        def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]:
            result = self._get_classification_validation_result(
                project_id=project_id,
                project_type=project_type,
                tag=tag,
                operation_id=operation_id,
            )
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        response["projectId"] = project_id
        response["projectType"] = project_type
        response["tag"] = tag
        response["operationId"] = operation_id
        return ValidateClassificationAction.model_validate(response)

    async def _wait_for_create_validate_classification_action_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        operation_id: str,
    ) -> ValidateClassificationAction:
        async def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]:
            result = await self._get_classification_validation_result_async(
                project_id=project_id,
                project_type=project_type,
                tag=tag,
                operation_id=operation_id,
            )
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        response = await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        response["projectId"] = project_id
        response["projectType"] = project_type
        response["tag"] = tag
        response["operationId"] = operation_id
        return ValidateClassificationAction.model_validate(response)

    def _wait_for_create_validate_extraction_action(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
    ) -> ValidateExtractionAction:
        def result_getter() -> Tuple[Any, Optional[Any], Optional[Any]]:
            result = self._get_extraction_validation_result(
                project_id=project_id,
                project_type=project_type,
                tag=tag,
                document_type_id=document_type_id,
                operation_id=operation_id,
            )
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        response["projectId"] = project_id
        response["projectType"] = project_type
        response["tag"] = tag
        response["documentTypeId"] = document_type_id
        response["operationId"] = operation_id
        return ValidateExtractionAction.model_validate(response)

    async def _wait_for_create_validate_extraction_action_async(
        self,
        project_id: str,
        project_type: ProjectType,
        tag: Optional[str],
        document_type_id: str,
        operation_id: str,
    ) -> ValidateExtractionAction:
        async def result_getter_async() -> Tuple[Any, Optional[Any], Optional[Any]]:
            result = await self._get_extraction_validation_result_async(
                project_id=project_id,
                project_type=project_type,
                tag=tag,
                document_type_id=document_type_id,
                operation_id=operation_id,
            )
            return (
                result["status"],
                result.get("error", None),
                result.get("result", None),
            )

        response = await self._wait_for_operation_async(
            result_getter=result_getter_async,
            wait_statuses=["NotStarted", "Running"],
            success_status="Succeeded",
        )

        response["projectId"] = project_id
        response["projectType"] = project_type
        response["tag"] = tag
        response["documentTypeId"] = document_type_id
        response["operationId"] = operation_id
        return ValidateExtractionAction.model_validate(response)

    @traced(name="documents_create_validate_classification_action", run_type="uipath")
    def create_validate_classification_action(
        self,
        classification_results: List[ClassificationResult],
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> ValidateClassificationAction:
        """Create a validate classification action for a document based on the classification results. More details about validation actions can be found in the [official documentation](https://docs.uipath.com/ixp/automation-cloud/latest/user-guide/validating-classifications).

        Args:
            classification_results (List[ClassificationResult]): The classification results to be validated, typically obtained from the [`classify`][uipath.platform.documents._documents_service.DocumentsService.classify] method.
            action_title (str): Title of the action.
            action_priority (ActionPriority, optional): Priority of the action.
            action_catalog (str, optional): Catalog of the action.
            action_folder (str, optional): Folder of the action.
            storage_bucket_name (str, optional): Name of the storage bucket.
            storage_bucket_directory_path (str, optional): Directory path in the storage bucket.

        Returns:
            ValidateClassificationAction: The created validate classification action.

        Examples:
            ```python
            validation_action = service.create_validate_classification_action(
                action_title="Test Validation Action",
                action_priority=ActionPriority.MEDIUM,
                action_catalog="default_du_actions",
                action_folder="Shared",
                storage_bucket_name="du_storage_bucket",
                storage_bucket_directory_path="TestDirectory",
                classification_results=classification_results,
            )
            ```
        """
        if not classification_results:
            raise ValueError("`classification_results` must not be empty")

        operation_id = self._start_classification_validation(
            project_id=classification_results[0].project_id,
            project_type=classification_results[0].project_type,
            tag=classification_results[0].tag,
            action_title=action_title,
            action_priority=action_priority,
            action_catalog=action_catalog,
            action_folder=action_folder,
            storage_bucket_name=storage_bucket_name,
            storage_bucket_directory_path=storage_bucket_directory_path,
            classification_results=classification_results,
        )

        return self._wait_for_create_validate_classification_action(
            project_id=classification_results[0].project_id,
            project_type=classification_results[0].project_type,
            tag=classification_results[0].tag,
            operation_id=operation_id,
        )

    @traced(name="documents_create_validate_classification_action", run_type="uipath")
    async def create_validate_classification_action_async(
        self,
        classification_results: List[ClassificationResult],
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> ValidateClassificationAction:
        """Asynchronous version of the [`create_validation_action`][uipath.platform.documents._documents_service.DocumentsService.create_validate_classification_action] method."""
        if not classification_results:
            raise ValueError("`classification_results` must not be empty")

        operation_id = await self._start_classification_validation_async(
            project_id=classification_results[0].project_id,
            project_type=classification_results[0].project_type,
            tag=classification_results[0].tag,
            action_title=action_title,
            action_priority=action_priority,
            action_catalog=action_catalog,
            action_folder=action_folder,
            storage_bucket_name=storage_bucket_name,
            storage_bucket_directory_path=storage_bucket_directory_path,
            classification_results=classification_results,
        )

        return await self._wait_for_create_validate_classification_action_async(
            project_id=classification_results[0].project_id,
            project_type=classification_results[0].project_type,
            tag=classification_results[0].tag,
            operation_id=operation_id,
        )

    @traced(name="documents_create_validate_extraction_action", run_type="uipath")
    def create_validate_extraction_action(
        self,
        extraction_response: ExtractionResponse,
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> ValidateExtractionAction:
        """Create a validate extraction action for a document based on the extraction response. More details about validation actions can be found in the [official documentation](https://docs.uipath.com/ixp/automation-cloud/latest/user-guide/validating-extractions).

        Args:
            extraction_response (ExtractionResponse): The extraction result to be validated, typically obtained from the [`extract`][uipath.platform.documents._documents_service.DocumentsService.extract] method.
            action_title (str): Title of the action.
            action_priority (ActionPriority, optional): Priority of the action.
            action_catalog (str, optional): Catalog of the action.
            action_folder (str, optional): Folder of the action.
            storage_bucket_name (str, optional): Name of the storage bucket.
            storage_bucket_directory_path (str, optional): Directory path in the storage bucket.

        Returns:
            ValidateClassificationAction: The created validation action.

        Examples:
            ```python
            validation_action = service.create_validate_extraction_action(
                action_title="Test Validation Action",
                action_priority=ActionPriority.MEDIUM,
                action_catalog="default_du_actions",
                action_folder="Shared",
                storage_bucket_name="du_storage_bucket",
                storage_bucket_directory_path="TestDirectory",
                extraction_response=extraction_response,
            )
            ```
        """
        operation_id = self._start_extraction_validation(
            project_id=extraction_response.project_id,
            project_type=extraction_response.project_type,
            tag=extraction_response.tag,
            document_type_id=extraction_response.document_type_id,
            action_title=action_title,
            action_priority=action_priority,
            action_catalog=action_catalog,
            action_folder=action_folder,
            storage_bucket_name=storage_bucket_name,
            storage_bucket_directory_path=storage_bucket_directory_path,
            extraction_response=extraction_response,
        ).operation_id

        return self._wait_for_create_validate_extraction_action(
            project_id=extraction_response.project_id,
            project_type=extraction_response.project_type,
            tag=extraction_response.tag,
            document_type_id=extraction_response.document_type_id,
            operation_id=operation_id,
        )

    @traced(name="documents_create_validate_extraction_action_async", run_type="uipath")
    async def create_validate_extraction_action_async(
        self,
        extraction_response: ExtractionResponse,
        action_title: str,
        action_priority: Optional[ActionPriority] = None,
        action_catalog: Optional[str] = None,
        action_folder: Optional[str] = None,
        storage_bucket_name: Optional[str] = None,
        storage_bucket_directory_path: Optional[str] = None,
    ) -> ValidateExtractionAction:
        """Asynchronous version of the [`create_validation_action`][uipath.platform.documents._documents_service.DocumentsService.create_validate_extraction_action] method."""
        operation_id = (
            await self._start_extraction_validation_async(
                project_id=extraction_response.project_id,
                project_type=extraction_response.project_type,
                tag=extraction_response.tag,
                document_type_id=extraction_response.document_type_id,
                action_title=action_title,
                action_priority=action_priority,
                action_catalog=action_catalog,
                action_folder=action_folder,
                storage_bucket_name=storage_bucket_name,
                storage_bucket_directory_path=storage_bucket_directory_path,
                extraction_response=extraction_response,
            )
        ).operation_id

        return await self._wait_for_create_validate_extraction_action_async(
            project_id=extraction_response.project_id,
            project_type=extraction_response.project_type,
            tag=extraction_response.tag,
            document_type_id=extraction_response.document_type_id,
            operation_id=operation_id,
        )

    @traced(name="documents_get_validate_classification_result", run_type="uipath")
    def get_validate_classification_result(
        self, validation_action: ValidateClassificationAction
    ) -> List[ClassificationResult]:
        """Get the result of a validate classification action.

        Note:
            This method will block until the validation action is completed, meaning the user has completed the validation in UiPath Action Center.

        Args:
            validation_action (ValidateClassificationAction): The validation action to get the result for, typically obtained from the [`create_validate_classification_action`][uipath.platform.documents._documents_service.DocumentsService.create_validate_classification_action] method.

        Returns:
            List[ClassificationResult]: The validated classification results.

        Examples:
            ```python
            validated_results = service.get_validate_classification_result(validate_classification_action)
            ```
        """

        def result_getter() -> Tuple[str, None, Any]:
            result = self._get_classification_validation_result(
                project_id=validation_action.project_id,
                project_type=validation_action.project_type,
                tag=validation_action.tag,
                operation_id=validation_action.operation_id,
            )
            return (result["result"]["actionStatus"], None, result["result"])

        response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["Unassigned", "Pending"],
            success_status="Completed",
        )
        classification_results = []
        for cr in response["validatedClassificationResults"]:
            cr["ProjectId"] = validation_action.project_id
            cr["ProjectType"] = validation_action.project_type
            cr["Tag"] = validation_action.tag
            classification_results.append(ClassificationResult.model_validate(cr))

        return classification_results

    @traced(
        name="documents_get_validate_classification_result_async", run_type="uipath"
    )
    async def get_validate_classification_result_async(
        self, validation_action: ValidateClassificationAction
    ) -> List[ClassificationResult]:
        """Asynchronous version of the [`get_validation_result`][uipath.platform.documents._documents_service.DocumentsService.get_validate_classification_result] method."""

        async def result_getter() -> Tuple[str, None, Any]:
            result = await self._get_classification_validation_result_async(
                project_id=validation_action.project_id,
                project_type=validation_action.project_type,
                tag=validation_action.tag,
                operation_id=validation_action.operation_id,
            )
            return (result["result"]["actionStatus"], None, result["result"])

        response = await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["Unassigned", "Pending"],
            success_status="Completed",
        )
        classification_results = []
        for cr in response["validatedClassificationResults"]:
            cr["ProjectId"] = validation_action.project_id
            cr["ProjectType"] = validation_action.project_type
            cr["Tag"] = validation_action.tag
            classification_results.append(ClassificationResult.model_validate(cr))

        return classification_results

    @traced(name="documents_get_validate_extraction_result", run_type="uipath")
    def get_validate_extraction_result(
        self, validation_action: ValidateExtractionAction
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        """Get the result of a validate extraction action.

        Note:
            This method will block until the validation action is completed, meaning the user has completed the validation in UiPath Action Center.

        Args:
            validation_action (ValidateClassificationAction): The validation action to get the result for, typically obtained from the [`create_validate_extraction_action`][uipath.platform.documents._documents_service.DocumentsService.create_validate_extraction_action] method.

        Returns:
            Union[ExtractionResponse, ExtractionResponseIXP]: The validated extraction response.

        Examples:
            ```python
            validated_result = service.get_validate_extraction_result(validate_extraction_action)
            ```
        """

        def result_getter() -> Tuple[str, None, Any]:
            result = self._get_extraction_validation_result(
                project_id=validation_action.project_id,
                project_type=validation_action.project_type,
                tag=validation_action.tag,
                document_type_id=validation_action.document_type_id,
                operation_id=validation_action.operation_id,
            )
            return (result["result"]["actionStatus"], None, result["result"])

        response = self._wait_for_operation(
            result_getter=result_getter,
            wait_statuses=["Unassigned", "Pending"],
            success_status="Completed",
        )
        response["extractionResult"] = response.pop("validatedExtractionResults")
        response["projectId"] = validation_action.project_id
        response["tag"] = validation_action.tag
        response["documentTypeId"] = validation_action.document_type_id
        response["projectType"] = validation_action.project_type

        if validation_action.project_type == ProjectType.IXP:
            return ExtractionResponseIXP.model_validate(response)

        return ExtractionResponse.model_validate(response)

    @traced(name="documents_get_validate_extraction_result_async", run_type="uipath")
    async def get_validate_extraction_result_async(
        self, validation_action: ValidateExtractionAction
    ) -> Union[ExtractionResponse, ExtractionResponseIXP]:
        """Asynchronous version of the [`get_validation_result`][uipath.platform.documents._documents_service.DocumentsService.get_validate_extraction_result] method."""

        async def result_getter() -> Tuple[str, None, Any]:
            result = await self._get_extraction_validation_result_async(
                project_id=validation_action.project_id,
                project_type=validation_action.project_type,
                tag=validation_action.tag,
                document_type_id=validation_action.document_type_id,
                operation_id=validation_action.operation_id,
            )
            return (result["result"]["actionStatus"], None, result["result"])

        response = await self._wait_for_operation_async(
            result_getter=result_getter,
            wait_statuses=["Unassigned", "Pending"],
            success_status="Completed",
        )
        response["extractionResult"] = response.pop("validatedExtractionResults")
        response["projectId"] = validation_action.project_id
        response["tag"] = validation_action.tag
        response["documentTypeId"] = validation_action.document_type_id
        response["projectType"] = validation_action.project_type

        if validation_action.project_type == ProjectType.IXP:
            return ExtractionResponseIXP.model_validate(response)

        return ExtractionResponse.model_validate(response)
