import os
from biolib.compute_node.job_worker.job_storage import JobStorage
from biolib.experiments.experiment import Experiment
from biolib.jobs import Job
from biolib.typing_utils import List, Optional, cast
from biolib.app.utils import run_job
from biolib.biolib_api_client import JobState, BiolibApiClient, CreatedJobDict
from biolib.jobs.types import JobDict
from biolib.biolib_api_client.app_types import App, AppVersion
from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
from biolib.biolib_api_client.biolib_app_api import BiolibAppApi
from biolib.biolib_binary_format import ModuleInput
from biolib.biolib_errors import BioLibError
from biolib.biolib_logging import logger


class BioLibApp:

    def __init__(self, uri: str):
        app_response = BiolibAppApi.get_by_uri(uri)
        self._app: App = app_response['app']
        self._app_uri = app_response['app_uri']
        self._app_version: AppVersion = app_response['app_version']

        logger.info(f'Loaded project {self._app_uri}')

    def __str__(self) -> str:
        return self._app_uri

    @property
    def uuid(self) -> str:
        return self._app['public_id']

    @property
    def version(self) -> AppVersion:
        return self._app_version

    def cli(
            self,
            args=None,
            stdin=None,
            files=None,
            override_command=False,
            machine='',
            blocking: bool = True,
            experiment_id: Optional[str] = None,
            result_prefix: Optional[str] = None,
    ):

        if not experiment_id:
            experiment = Experiment.get_experiment_in_context()
            experiment_id = experiment.uuid if experiment else None

        if not blocking:
            return self._cli_non_blocking(
                args,
                stdin,
                files,
                override_command,
                machine,
                experiment_id,
                result_prefix,
            )

        module_input_serialized = self._get_serialized_module_input(args, stdin, files)

        job: CreatedJobDict = BiolibJobApi.create(
            app_version_id=self._app_version['public_id'],
            override_command=override_command,
            experiment_uuid=experiment_id,
            machine=machine if machine != 'local' else '',
        )

        BiolibJobApi.update_state(job['public_id'], JobState.IN_PROGRESS.value)

        try:
            module_output = run_job(
                job,
                module_input_serialized,
                force_local=(machine == 'local'),
                result_name_prefix=result_prefix,
            )
            try:
                BiolibJobApi.update_state(job_id=job['public_id'], state=JobState.COMPLETED.value)
            except Exception as error:  # pylint: disable=broad-except
                logger.debug(f'Could not update job state to completed:\n{error}')

            # TODO: Create new JobDict that has shared relevant fields
            job_dict = cast(JobDict, job)
            job_object = Job(job_dict=job_dict)
            job_object._set_result_module_output(module_output)  # pylint: disable=protected-access
            return job_object

        except BioLibError as exception:
            logger.error(f'Compute failed with: {exception.message}')
            try:
                BiolibApiClient.refresh_auth_token()
                BiolibJobApi.update_state(job_id=job['public_id'], state=JobState.FAILED.value)
            except Exception as error:  # pylint: disable=broad-except
                logger.debug(f'Could not update job state to failed:\n{error}')

            raise exception

    def exec(self, args=None, stdin=None, files=None, machine=''):
        return self.cli(args, stdin, files, override_command=True, machine=machine)

    def __call__(self, *args, **kwargs):
        if not args and not kwargs:
            self.cli()

        else:
            raise BioLibError('''
Calling an app directly with app() is currently being reworked.
To use the previous functionality, please call app.cli() instead. 
Example: "app.cli('--help')"
''')

    @staticmethod
    def _get_serialized_module_input(args=None, stdin=None, files=None) -> bytes:
        if args is None:
            args = []

        if stdin is None:
            stdin = b''

        if isinstance(args, str):
            args = list(filter(lambda p: p != '', args.split(' ')))

        if not isinstance(args, list):
            raise Exception('The given input arguments must be list or str')

        if isinstance(stdin, str):
            stdin = stdin.encode('utf-8')

        if files is None:
            files = []
            for idx, arg in enumerate(args):
                if os.path.isfile(arg):
                    files.append(arg)
                    args[idx] = arg.split('/')[-1]

        cwd = os.getcwd()
        files_dict = {}
        if isinstance(files, list):
            for file in files:
                path = file
                if not file.startswith('/'):
                    # make path absolute
                    path = cwd + '/' + file

                arg_split = path.split('/')
                file = open(path, 'rb')
                path = '/' + arg_split[-1]

                files_dict[path] = file.read()
                file.close()
        elif isinstance(files, dict):
            files_dict = files
        else:
            raise Exception('The given files input must be list or dict or None')

        module_input_serialized: bytes = ModuleInput().serialize(
            stdin=stdin,
            arguments=args,
            files=files_dict,
        )
        return module_input_serialized

    def _cli_non_blocking(
            self,
            args: Optional[List[str]] = None,
            stdin: Optional[bytes] = None,
            files: Optional[List[str]] = None,
            override_command: bool = False,
            machine: Optional[str] = None,
            experiment_id: Optional[str] = None,
            result_prefix: Optional[str] = None,
    ) -> Job:
        module_input_serialized = self._get_serialized_module_input(args, stdin, files)

        job_dict: CreatedJobDict = BiolibJobApi.create(
            app_version_id=self._app_version['public_id'],
            override_command=override_command,
            machine=machine,
            experiment_uuid=experiment_id
        )
        JobStorage.upload_module_input(job=job_dict, module_input_serialized=module_input_serialized)
        cloud_job = BiolibJobApi.create_cloud_job(job_id=job_dict['public_id'], result_name_prefix=result_prefix)
        logger.debug(f"Cloud: Job created with id {cloud_job['public_id']}")
        return Job(cast(JobDict, job_dict))
