import logging
import os
import sys
import random
import subprocess
import threading
import time
import pathlib
from io import BytesIO
from typing import BinaryIO

import requests.exceptions

from biolib import utils
from biolib.biolib_api_client import BiolibApiClient
from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
from biolib.biolib_binary_format import UnencryptedModuleOutput, InMemoryIndexableBuffer
from biolib.biolib_errors import BioLibError
from biolib.biolib_logging import logger
from biolib.compute_node.job_worker.executors import RemoteExecutor, RemoteExecuteOptions
from biolib.utils import stream_process_output
from biolib.typing_utils import Optional


def run_job(
        job,
        module_input_serialized: bytes,
        force_local: bool = False,
        result_name_prefix: Optional[str] = None,
) -> UnencryptedModuleOutput:
    job_id = job['public_id']
    logger.info(f'Job "{job_id}" is starting...')

    if not force_local:
        return RemoteExecutor.execute_job(
            RemoteExecuteOptions(
                biolib_base_url=utils.BIOLIB_BASE_URL,
                job=job,
                result_name_prefix=result_name_prefix,
                root_job_id=job_id,
            ),
            module_input_serialized,
        )

    if not job['app_version'].get('modules'):
        BioLibError('Unable to run the application locally')

    # Run locally
    host = '127.0.0.1'
    port = str(random.choice(range(5000, 65000)))
    node_url = f'http://{host}:{port}'
    logger.debug(f'Starting local compute node at {node_url}')
    start_cli_path = pathlib.Path(__file__).parent.parent.resolve() / 'start_cli.py'
    python_executable_path = sys.executable
    output_destination: Optional[int]
    if utils.IS_RUNNING_IN_NOTEBOOK:
        utils.STREAM_STDOUT = True
        output_destination = subprocess.PIPE
    elif utils.STREAM_STDOUT:
        # If not running in notebook but streaming stdout is enabled (probably running as CLI)
        # set output destination to None to have it write to current process output (this does not work in notebook)
        output_destination = None
    else:
        output_destination = subprocess.DEVNULL

    compute_node_process = subprocess.Popen(
        args=[python_executable_path, start_cli_path, 'start', '--host', host, '--port', port],
        env=dict(
            os.environ,
            BIOLIB_LOG=logging.getLevelName(logger.level),
            BIOLIB_BASE_URL=BiolibApiClient.get().base_url
        ),
        stdout=output_destination,
        stderr=output_destination,
    )

    if utils.IS_RUNNING_IN_NOTEBOOK:
        logger.debug('Running in Notebook, so starting output streaming thread...')
        threading.Thread(target=stream_process_output, args=(compute_node_process,)).start()

    try:
        for retry in range(20):
            time.sleep(1)
            try:
                BiolibJobApi.save_compute_node_job(
                    job=job,
                    module_name='main',
                    access_token=BiolibApiClient.get().access_token,
                    node_url=node_url
                )
                break

            except requests.exceptions.ConnectionError:
                if retry == 19:
                    raise BioLibError('Could not connect to local compute node') from None
                logger.debug('Could not connect to local compute node retrying...')

        BiolibJobApi.start_local_job(job_id, module_input_serialized, node_url)
        BiolibJobApi.await_local_compute_node_status(
            retry_interval_seconds=1.5,
            retry_limit_minutes=43800,  # Let users run an app locally for a month (43800 minutes)
            status_to_await='Result Ready',
            compute_type='Compute Node',
            node_url=node_url,
            job=job,
        )

        result = BiolibJobApi.get_cloud_result(job_id, node_url)
        unencrypted_module_output: BinaryIO = BytesIO()
        legacy_module_output: BinaryIO = BytesIO(result)
        UnencryptedModuleOutput.write_to_file_from_legacy_module_output_file(
            input_file=legacy_module_output,
            output_file=unencrypted_module_output
        )
        unencrypted_module_output.seek(0)
        return UnencryptedModuleOutput(InMemoryIndexableBuffer(unencrypted_module_output.read()))

    finally:
        compute_node_process.terminate()
