from pathlib import Path
from fnmatch import fnmatch
import time
import requests

from biolib import api
from biolib.biolib_binary_format import UnencryptedModuleOutput
from biolib.biolib_binary_format.utils import RemoteIndexableBuffer, LazyLoadedFile
from biolib.biolib_errors import BioLibError
from biolib.biolib_logging import logger_no_user_data, logger
from biolib.typing_utils import Optional, Dict, List, cast, Union, Callable


class JobResultError(BioLibError):
    pass


class JobResultNotFound(JobResultError):
    pass


class JobResultPermissionError(JobResultError):
    pass


PathFilter = Union[str, Callable[[LazyLoadedFile], bool]]


class JobResult:

    def __init__(
            self,
            job_uuid: str,
            job_auth_token: Optional[str],
            module_output: Optional[UnencryptedModuleOutput] = None,
    ):
        self._job_uuid: str = job_uuid
        self._job_auth_token: Optional[str] = job_auth_token

        self._module_output: Optional[UnencryptedModuleOutput] = module_output

    def get_stdout(self) -> bytes:
        return self._get_module_output().get_stdout()

    def get_stderr(self) -> bytes:
        return self._get_module_output().get_stderr()

    def get_exit_code(self) -> int:
        return self._get_module_output().get_exit_code()

    def save_files(self, output_dir: str, path_filter: Optional[PathFilter] = None) -> None:
        module_output = self._get_module_output()
        output_files = module_output.get_files()

        if path_filter:
            output_files = self._get_filtered_files(output_files, path_filter)

        logger.info(f'Saving {len(output_files)} files to {output_dir}...')

        for file in output_files:
            # Remove leading slash of file_path
            destination_file_path = Path(output_dir) / Path(file.path.lstrip('/'))
            if destination_file_path.exists():
                destination_file_path.rename(f'{destination_file_path}.biolib-renamed.{time.strftime("%Y%m%d%H%M%S")}')

            dir_path = destination_file_path.parent
            if dir_path:
                dir_path.mkdir(parents=True, exist_ok=True)

            with open(destination_file_path, mode='wb') as file_handler:
                for chunk_number, chunk in enumerate(file.get_data_as_iterable()):
                    logger_no_user_data.debug(f'Processing chunk {chunk_number}...')
                    file_handler.write(chunk)

            logger.info(f'  - {destination_file_path}')

    def get_output_file(self, filename) -> LazyLoadedFile:
        files = self._get_module_output().get_files()
        filtered_files = self._get_filtered_files(files, path_filter=filename)
        if not filtered_files:
            raise BioLibError(f"File {filename} not found in results.")

        if len(filtered_files) != 1:
            raise BioLibError(f"Found multiple results for filename {filename}.")

        return filtered_files[0]

    def list_output_files(self, path_filter: Optional[PathFilter] = None) -> List[LazyLoadedFile]:
        files = self._get_module_output().get_files()
        if not path_filter:
            return files

        return self._get_filtered_files(files, path_filter)

    @staticmethod
    def _get_filtered_files(files: List[LazyLoadedFile], path_filter: PathFilter) -> List[LazyLoadedFile]:
        if not (isinstance(path_filter, str) or callable(path_filter)):
            raise Exception('Expected path_filter to be a string or a function')

        if callable(path_filter):
            return list(filter(path_filter, files))

        glob_filter = cast(str, path_filter)

        def _filter_function(file: LazyLoadedFile) -> bool:
            return fnmatch(file.path, glob_filter)

        return list(filter(_filter_function, files))

    def _get_presigned_download_url(self) -> str:
        try:
            response_dict: Dict[str, str] = api.client.get(
                url=f'/jobs/{self._job_uuid}/storage/results/download/',
                headers={'Job-Auth-Token': self._job_auth_token} if self._job_auth_token else None,
            ).json()
            return response_dict['presigned_download_url']
        except requests.exceptions.HTTPError as error:
            status_code = error.response.status_code

            if status_code == 401:
                raise JobResultPermissionError('You must be signed in to get result of the job') from None
            elif status_code == 403:
                raise JobResultPermissionError(
                    'Cannot get result of job. Maybe the job was created without being signed in?'
                ) from None
            elif status_code == 404:
                raise JobResultNotFound('Job result not found') from None
            else:
                raise JobResultError('Failed to get result of job') from error

        except Exception as error:
            raise JobResultError('Failed to get result of job') from error

    def _get_module_output(self) -> UnencryptedModuleOutput:
        if self._module_output is None:
            buffer = RemoteIndexableBuffer(url=self._get_presigned_download_url())
            self._module_output = UnencryptedModuleOutput(buffer)

        return self._module_output
