from __future__ import annotations

import asyncio
from enum import Enum
from typing import Any, Literal

from typing_extensions import Self

from cognite.client.data_classes._base import (
    CogniteFilter,
    CogniteResource,
    CogniteResourceList,
    CogniteResourceWithClientRef,
    InternalIdTransformerMixin,
)
from cognite.client.data_classes.transformations.common import TransformationDestination
from cognite.client.utils._async_helpers import run_sync
from cognite.client.utils._text import copy_doc_from_async


class TransformationJobStatus(str, Enum):
    RUNNING = "Running"
    CREATED = "Created"
    COMPLETED = "Completed"
    FAILED = "Failed"


class TransformationJobMetric(CogniteResource):
    """The transformation job metric resource allows following details of execution of a transformation run.

    Args:
        timestamp (int): Time of the last metric update.
        name (str): Name of the metric.
        count (int): Value of the metric.
    """

    def __init__(
        self,
        timestamp: int,
        name: str,
        count: int,
    ) -> None:
        self.timestamp = timestamp
        self.name = name
        self.count = count

    @classmethod
    def _load(cls, resource: dict[str, Any]) -> Self:
        return cls(
            timestamp=resource["timestamp"],
            name=resource["name"],
            count=resource["count"],
        )


class TransformationJobMetricList(CogniteResourceList[TransformationJobMetric], InternalIdTransformerMixin):
    _RESOURCE = TransformationJobMetric


class TransformationJob(CogniteResourceWithClientRef):
    """The transformation job resource allows following the status of execution of a transformation run.

    Args:
        id (int): A server-generated ID for the object.
        uuid (str): A server-generated UUID for the object.
        status (TransformationJobStatus): Status of the job.
        transformation_id (int): Server-generated ID of the transformation.
        transformation_external_id (str): external ID of the transformation.
        source_project (str): Name of the CDF project the data will be read from.
        destination_project (str): Name of the CDF project the data will be written to.
        destination (TransformationDestination): No description.
        conflict_mode (Literal['abort', 'delete', 'update', 'upsert']): What to do in case of id collisions: either "abort", "upsert", "update" or "delete".
        query (str): Query of the transformation that is being executed.
        ignore_null_fields (bool): Indicates how null values are handled on updates: ignore or set null.
        created_time (int): Time when the job was created.
        error (str | None): Error message from the server.
        started_time (int | None): Time when the job started running.
        finished_time (int | None): Time when the job finished running.
        last_seen_time (int | None): Time of the last status update from the job.
    """

    def __init__(
        self,
        id: int,
        uuid: str,
        status: TransformationJobStatus,
        transformation_id: int,
        transformation_external_id: str,
        source_project: str,
        destination_project: str,
        destination: TransformationDestination,
        conflict_mode: Literal["abort", "delete", "update", "upsert"],
        query: str,
        ignore_null_fields: bool,
        created_time: int,
        error: str | None = None,
        started_time: int | None = None,
        finished_time: int | None = None,
        last_seen_time: int | None = None,
    ) -> None:
        self.id = id
        self.uuid = uuid
        self.status = status
        self.transformation_id = transformation_id
        self.transformation_external_id = transformation_external_id
        self.source_project = source_project
        self.destination_project = destination_project
        self.destination = destination
        self.conflict_mode = conflict_mode
        self.query = query
        self.error = error
        self.ignore_null_fields = ignore_null_fields
        self.created_time = created_time
        self.started_time = started_time
        self.finished_time = finished_time
        self.last_seen_time = last_seen_time

    async def update_async(self) -> None:
        """`Get updated job status.`"""
        if self.id is None:
            raise ValueError("Unable to update, TransformationJob is missing 'id'")

        updated = await self._cognite_client.transformations.jobs.retrieve(id=self.id)
        if updated is None:
            raise RuntimeError("Unable to update the transformation. Has it been deleted?")

        self.status = updated.status
        self.error = updated.error
        self.started_time = updated.started_time
        self.finished_time = updated.finished_time
        self.last_seen_time = updated.last_seen_time

    @copy_doc_from_async(update_async)
    def update(self) -> None:
        return run_sync(self.update_async())

    async def cancel_async(self) -> None:
        if self.transformation_id is None:
            await self._cognite_client.transformations.cancel(
                transformation_external_id=self.transformation_external_id
            )
        else:
            await self._cognite_client.transformations.cancel(transformation_id=self.transformation_id)

    @copy_doc_from_async(cancel_async)
    def cancel(self) -> None:
        return run_sync(self.cancel_async())

    async def metrics_async(self) -> TransformationJobMetricList:
        """`Get job metrics.`"""
        assert self.id is not None
        return await self._cognite_client.transformations.jobs.list_metrics(self.id)

    @copy_doc_from_async(metrics_async)
    def metrics(self) -> TransformationJobMetricList:
        return run_sync(self.metrics_async())

    async def wait_async(self, polling_interval: float = 5, timeout: float | None = None) -> TransformationJob:
        """`Waits for the job to finish.`

        Args:
            polling_interval (float): time (s) to wait between job status updates, default is one second.
            timeout (float | None): maximum time (s) to wait, default is None (infinite time). Once the timeout is reached, it returns with the current status.

        Returns:
            TransformationJob: The transformation job (itself).

        Examples:
            Run transformations 1 and 2 in parallel, and run 3 once they finish successfully:

                >>> from cognite.client import CogniteClient, AsyncCogniteClient
                >>> client = CogniteClient()
                >>> # async_client = AsyncCogniteClient()  # another option
                >>>
                >>> job1 = client.transformations.run(id=1, wait=False)
                >>> job2 = client.transformations.run(id=2, wait=False)
                >>> job1.wait()
                >>> job2.wait()
                >>> if TransformationJobStatus.FAILED not in [job1.status, job2.status]:
                >>>     client.transformations.run(id = 3, wait = False)

            Wait on transformation for 5 minutes and do something if still running:

                >>>
                >>> job = client.transformations.run(id=1, wait=False)
                >>> job.wait(timeout=5.0 * 60)
                >>> match job.status:
                >>>     case TransformationJobStatus.FAILED:
                >>> # do something if job failed
                >>>     case TransformationJobStatus.COMPLETED:
                >>> # do something if job completed successfully
                >>>     case _:
                >>> # do something if job is still running
        """
        await self.update_async()
        if timeout is None:
            timeout = float("inf")
        waited = 0.0
        while waited < timeout and self.status not in [
            TransformationJobStatus.FAILED,
            TransformationJobStatus.COMPLETED,
        ]:
            to_wait = min(timeout - waited, polling_interval)
            await asyncio.sleep(to_wait)
            await self.update_async()
            waited += polling_interval
        return self

    @copy_doc_from_async(wait_async)
    def wait(self, polling_interval: float = 5, timeout: float | None = None) -> TransformationJob:
        return run_sync(self.wait_async(polling_interval, timeout))

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = super().dump(camel_case)
        if self.destination:
            output["destination"] = self.destination.type
        if self.status:
            output["status"] = self.status.value
        return output

    @classmethod
    def _load(cls, resource: dict) -> TransformationJob:
        if isinstance(resource["destination"], dict):
            destination = TransformationDestination._load(resource["destination"])
        else:
            destination = TransformationDestination(type=resource["destination"])
        return cls(
            id=resource["id"],
            uuid=resource["uuid"],
            status=TransformationJobStatus(resource["status"]),
            transformation_id=resource["transformationId"],
            transformation_external_id=resource["transformationExternalId"],
            source_project=resource["sourceProject"],
            destination_project=resource["destinationProject"],
            destination=destination,
            conflict_mode=resource["conflictMode"],
            query=resource["query"],
            error=resource.get("error"),
            ignore_null_fields=resource["ignoreNullFields"],
            created_time=resource["createdTime"],
            started_time=resource.get("startedTime"),
            finished_time=resource.get("finishedTime"),
            last_seen_time=resource.get("lastSeenTime"),
        )

    def __hash__(self) -> int:
        return hash(self.id)


class TransformationJobList(CogniteResourceList[TransformationJob], InternalIdTransformerMixin):
    _RESOURCE = TransformationJob


class TransformationJobFilter(CogniteFilter):
    """TransformationJobFilter

    Args:
        transformation_id (int | None):  Filter jobs by transformation internal numeric ID.
        transformation_external_id (str | None): Filter jobs by transformation external ID.
    """

    def __init__(self, transformation_id: int | None = None, transformation_external_id: str | None = None) -> None:
        self.transformation_id = transformation_id
        self.transformation_external_id = transformation_external_id
