"""Module containing the Customisation class and related errors."""

from __future__ import annotations

import sys
import time
from contextlib import contextmanager
from datetime import datetime
from typing import TYPE_CHECKING

import requests

from eumdac.errors import EumdacError, eumdac_raise_for_status
from eumdac.request import get, patch, put

if TYPE_CHECKING:  # pragma: no cover
    if sys.version_info < (3, 9):
        from typing import Generator, Iterable, Mapping, MutableMapping
    else:
        from collections.abc import MutableMapping, Mapping, Iterable, Generator
    from types import TracebackType
    from typing import IO, Any, Optional, Type

    from eumdac.datatailor import DataTailor

import eumdac.common


class Customisation:
    """Customisation job from Data Tailor, used for managing the job lifecycle.

    The customisation job in Data Tailor needs to exist before instancing this class.
    Use the DataTailor class to create new Customisation instances or get existing ones, then use the received instance to check the job details and manage its life-cycle.

    Attributes
    ----------
    - `_id`: *str*
        Customisation id in Data Tailor. The string representation of an instance will be its id.
    - `datatailor`: *DataTailor*
        DataTailor instance that owns this customisation.
    - `update_margin`: *float, default 0.5*
        Seconds to wait before requesting an update on the customisation details.

    Properties
    -----------
    - `status`: *str*
        Status of the job in Data Tailor (`QUEUED`, `RUNNING`, `DONE`, `KILLED`, `FAILED`).
    - `progress`: *int*
        Progress of the running job, in percentage from 0 to 100.
    - `duration`: *int*
        Time taken by the running job, in seconds.
    - `logfile`: *str*
        Log of the job execution.
    - `outputs`: *Iterable[str]*
        List of outputs generated by the finished job.

    Methods
    -------
    - `stream_output(output : str)`: *IO[bytes]*
        Return a stream of the byts of `output` which can be used for downloading.
    - `stream_output_iter_content(output : str)`: *Iterable[bytes]*
        Return an iterable stream of the bytes of `output` which can be used for downloading.
    - `kill()`
        Cancel the QUEUED or RUNNING job, putting it in the KILLED status.
    - `delete()`
        Remove the DONE, KILLED, or FAILED job from Data Tailor.
    """

    _id: str
    datatailor: DataTailor
    update_margin: float = 0.5  # seconds
    _properties: Optional[MutableMapping[str, Any]] = None
    _deleted: bool = False
    _killed: bool = False
    _last_update: float = 0
    _creation_time_format: str = "%Y%m%dT%H%M%SZ"

    def __init__(self, customisation_id: str, datatailor: DataTailor) -> None:
        """Prepare the Customisation instance for the job with `id` from `datatailor`.

        Arguments
        ---------
        - `customisation_id`: *str*
            Id of the job that this Customisation instance represents.
        - `datatailor`: *DataTailor*
            Data Tailor instance that owns this customisation.
        """

        self._id = customisation_id
        self.datatailor = datatailor

    @classmethod
    def from_properties(
        cls, properties: Mapping[str, Any], datatailor: DataTailor
    ) -> Customisation:
        """Prepare a Customisation from `properties`"""
        _properties = {**properties}
        instance = cls(_properties.pop("id"), datatailor)
        instance._last_update = time.time()
        instance._properties = _properties
        return instance

    def __str__(self) -> str:
        """Return the customisation job id."""
        return self._id

    def __repr__(self) -> str:
        return f"{self.__class__}({self._id})"

    def __enter__(self) -> Customisation:
        return self

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        exc_traceback: Optional[TracebackType],
    ) -> None:
        self.delete()

    def _update_properties(self) -> None:
        """Refresh customisation details.

        Raises
        ------
        AlreadyDeletedCustomisationException, if the job is not found in Data Tailor.
        """

        if self._deleted:
            raise AlreadyDeletedCustomisationError("Customisation already deleted.")
        now = time.time()
        expired = now - self._last_update > self.update_margin
        if expired or self._properties is None:
            url = self.datatailor.urls.get(
                "tailor", "customisation", vars={"customisation_id": self._id}
            )
            response = get(url, auth=self.datatailor.token.auth, headers=eumdac.common.headers)
            eumdac_raise_for_status(
                "Failed to get customisation", response, UnableToGetCustomisationError
            )
            self._properties = response.json()[self._id]
            self._last_update = now

    @property
    def creation_time(self) -> datetime:
        """Customisation job creation time."""
        self._update_properties()
        return datetime.strptime(
            self._properties["creation_time"], self._creation_time_format  # type: ignore[index]
        )

    @property
    def backend(self) -> str:
        """Data Tailor backend used for the customisation."""
        self._update_properties()
        return self._properties["backend_id"]  # type: ignore[index]

    @property
    def product_type(self) -> str:
        """Product type of the customisation."""
        self._update_properties()
        return self._properties["product_id"]  # type: ignore[index]

    @property
    def processing_steps(self) -> Iterable[str]:
        """List of processing steps involved in the customisation job."""
        self._update_properties()
        return self._properties["required_processing_steps"]  # type: ignore[index]

    @property
    def status(self) -> str:
        """Customisation job status ('QUEUED', 'RUNNING', 'DONE', 'KILLED', 'FAILED')."""
        self._update_properties()
        return self._properties["status"]  # type: ignore[index]

    @property
    def progress(self) -> int:
        """Customisation job progress."""
        self._update_properties()
        return self._properties["progress"]  # type: ignore[index]

    @property
    def duration(self) -> int:
        """Elapsed time for the customisation job."""
        self._update_properties()
        return self._properties["processing_duration"]  # type: ignore[index]

    @property
    def outputs(self) -> Iterable[str]:
        """List of outputs of the finished customisation job."""
        self._update_properties()
        return self._properties["output_products"]  # type: ignore[index]

    @property
    def logfile(self) -> str:
        """Log of the job execution, in a multiline str.

        Raises
        ------
        AlreadyDeletedCustomisationException if the job is not found in Data Tailor.
        CustomisationError, if the log can't be retrieved.
        """

        if self._deleted:
            raise AlreadyDeletedCustomisationError("Customisation already deleted.")
        url = self.datatailor.urls.get(
            "tailor", "customisation log", vars={"customisation_id": self._id}
        )
        response = get(
            url,
            auth=self.datatailor.token.auth,
            headers=eumdac.common.headers,
        )
        eumdac_raise_for_status("Failed to get customisation log", response, CustomisationError)
        return response.json()["log_content"]

    @contextmanager
    def _download_response(self, output: str) -> Generator[requests.Response, None, None]:
        """Prepare the download response for `output`.

        Raises
        ------
        AlreadyDeletedCustomisationException if the job is not found in Data Tailor.
        CustomisationError, if the download response can't be retrieved.
        """

        if self._deleted:
            raise AlreadyDeletedCustomisationError("Customisation already deleted.")
        if output not in self.outputs:
            raise ValueError(f"{output} not in {self.outputs}")
        url = self.datatailor.urls.get("tailor", "download")
        auth = self.datatailor.token.auth
        params = {"path": output}
        with get(
            url,
            auth=auth,
            params=params,
            stream=True,
            headers=eumdac.common.headers,
        ) as response:
            eumdac_raise_for_status("Failed to get tailor download", response, CustomisationError)
            response.raw.name = output.split("/")[-1]
            response.raw.decode_content = True
            yield response

    @contextmanager
    def stream_output_iter_content(
        self, output: str, chunks: int = 1024 * 1024
    ) -> Generator[Iterable[bytes], None, None]:
        """Return an interable stream of the bytes of `output` which can be used for downloading."""
        with self._download_response(output) as resp:
            yield resp.iter_content(chunks)

    @contextmanager
    def stream_output(self, output: str) -> Generator[IO[bytes], None, None]:
        """Return a stream of the byts of `output` which can be used for downloading."""
        with self._download_response(output) as resp:
            yield resp.raw

    def delete(self) -> None:
        """Remove the DONE, KILLED, or FAILED job from Data Tailor.

        Raises
        ------
        CustomisationError, if the deletion fails.
        """

        if not self._deleted:
            url = self.datatailor.urls.get("tailor", "delete")
            payload = {"uuids": [self._id]}
            auth = self.datatailor.token.auth
            response = patch(url, auth=auth, json=payload, headers=eumdac.common.headers)
            eumdac_raise_for_status("Failed to patch tailor delete", response, CustomisationError)
            self._deleted = True

    def kill(self) -> None:
        """Cancel the QUEUED or RUNNING job, putting it in the KILLED status.

        Raises
        ------
        CustomisationError, if the cancelling fails.
        """

        if not self._killed:
            url = self.datatailor.urls.get(
                "tailor", "customisation", vars={"customisation_id": self._id}
            )
            auth = self.datatailor.token.auth
            response = put(url, json={"status": "killed"}, auth=auth, headers=eumdac.common.headers)
            eumdac_raise_for_status(
                "Failed to cancel tailor customisation", response, CustomisationError
            )
            self._killed = True


class CustomisationError(EumdacError):
    """Error related to customisations."""


class AlreadyDeletedCustomisationError(CustomisationError):
    """Error when dealing with customisations that are already deleted."""


class UnableToGetCustomisationError(CustomisationError):
    """Error when retrieval of customisation fails."""
