import logging
import os
from abc import ABC, abstractmethod
from pathlib import Path
from uuid import UUID

import requests
from beartype import beartype

from picsellia.decorators import exception_handler
from picsellia.exceptions import NoUrlAvailable
from picsellia.sdk.connexion import Connection

logger = logging.getLogger("picsellia")


class Downloadable(ABC):
    def __init__(self):
        if not hasattr(self, "_url"):
            self._url = None

    @property
    @abstractmethod
    def connexion(self) -> Connection:
        pass

    @property
    def url(self) -> str:
        """Url
        This is generated by backend and expires after 1 hour. So this property might be out of date.
        Calling `sync()` method will retrieve a new url when expired.
        """
        if not self._url:
            self.reset_url()
        return self._url

    @property
    @abstractmethod
    def id(self) -> UUID:
        """Id"""
        pass

    @property
    @abstractmethod
    def object_name(self) -> str:
        """Object name"""
        pass

    @property
    @abstractmethod
    def filename(self) -> str:
        """Filename"""
        pass

    @property
    def id_with_extension(self) -> str:
        extension = os.path.splitext(self.filename)[1]
        return f"{self.id}{extension}"

    @property
    @abstractmethod
    def large(self) -> bool:
        """If true, this (Object) has a large size"""
        pass

    @abstractmethod
    def reset_url(self) -> str:
        """Reset url of this object"""
        pass

    @exception_handler
    @beartype
    def download(
        self,
        target_path: str | Path = "./",
        force_replace: bool = False,
        use_id: bool = False,
    ) -> None:
        """Download this object into given target_path

        Examples:
            ```python
            data = clt.get_datalake().list_data(limit=1)
            data.download('./data/')
            ```

        Arguments:
            target_path (str, optional): Target path where data will be downloaded. Defaults to './'.
            force_replace (bool, optional): Replace an existing file if exists. Defaults to False.
            use_id (bool, optional): If true, will download file with id and extension as file name. Defaults to False.
        """
        if self._do_download(target_path, force_replace, use_id=use_id):
            logger.info(f"{self.filename} downloaded successfully.")
        else:
            logger.info(f"{self.filename} was already downloaded.")

    @exception_handler
    @beartype
    def _do_download(
        self,
        target_path: str | Path,
        force_replace: bool,
        use_id: bool = False,
    ) -> bool:
        if use_id:
            path = os.path.join(target_path, self.id_with_extension)
        else:
            path = os.path.join(target_path, self.filename)
        url = self.url
        if not url:
            raise NoUrlAvailable(
                "Could not retrieve an url to download this file: you might not have rights to download it."
            )

        try:
            return self.connexion.do_download_file(
                path, url, is_large=self.large, force_replace=force_replace
            )
        except requests.exceptions.HTTPError as e:
            # Handle presigned url expired
            if (
                e.response is None
                or e.response.status_code is None
                or e.response.status_code != 403
            ):
                raise

            logger.debug("url might have expired, regenerating it")
            url = self.reset_url()
            if not url:
                raise NoUrlAvailable(
                    "Could not retrieve an url to download this file: you might not have rights to download it."
                )

            return self.connexion.do_download_file(
                path, url, is_large=self.large, force_replace=force_replace
            )
