import logging
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Union
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 Connexion

logger = logging.getLogger("picsellia")


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

    @property
    @abstractmethod
    def connexion(self) -> Connexion:
        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: Union[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: Union[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
            )
