import json
import copy
from pathlib import Path
import asyncio
import subprocess as sub
import sys
import traceback
import yaml


from fastapi import Request

import pytest
from sophios.wic_types import Json, List
import sophios.post_compile as pc


from sophios.api.http import restapi

try:
    import cwltool.main
    import toil.cwl.cwltoil  # transitively imports cwltool
except ImportError as exc:
    print('Could not import cwltool.main and/or toil.cwl.cwltoil')
    # (pwd is imported transitively in cwltool.provenance)
    print(exc)
    if exc.msg == "No module named 'pwd'":
        print('Windows does not have a pwd module')
        print('If you want to run on windows, you need to install')
        print('Windows Subsystem for Linux')
        print('See https://pypi.org/project/cwltool/#ms-windows-users')
    else:
        raise exc


def run_cwl_local(workflow_name: str, cwl_runner: str, docker_cmd: str, use_subprocess: bool) -> int:
    """A helper function to run the compiled cwl output"""
    quiet = ['--quiet']
    skip_schemas = ['--skip-schemas']
    provenance = ['--provenance', f'provenance/{workflow_name}']
    docker_cmd_: List[str] = []
    if docker_cmd == 'docker':
        docker_cmd_ = []
    elif docker_cmd == 'singularity':
        docker_cmd_ = ['--singularity']
    else:
        docker_cmd_ = ['--user-space-docker-cmd', docker_cmd]
    write_summary = ['--write-summary', f'output_{workflow_name}.json']
    path_check = ['--relax-path-checks']
    # See https://github.com/common-workflow-language/cwltool/blob/5a645dfd4b00e0a704b928cc0bae135b0591cc1a/cwltool/command_line_tool.py#L94
    # NOTE: Using --leave-outputs to disable --outdir
    # See https://github.com/dnanexus/dx-cwl/issues/20
    # --outdir has one or more bugs which will cause workflows to fail!!!
    docker_pull = ['--disable-pull']  # Use cwl-docker-extract to pull images
    script = 'cwltool_filterlog' if cwl_runner == 'cwltool' else cwl_runner
    cmd = [script] + docker_pull + quiet + provenance + \
        docker_cmd_ + write_summary + skip_schemas + path_check
    if cwl_runner == 'cwltool':
        cmd += ['--leave-outputs',
                f'autogenerated/{workflow_name}.cwl', f'autogenerated/{workflow_name}_inputs.yml']
    elif cwl_runner == 'toil-cwl-runner':
        cmd += ['--outdir', 'outdir_toil',
                '--jobStore', f'file:./jobStore_{workflow_name}',  # NOTE: This is the equivalent of --cachedir
                '--clean', 'always',  # This effectively disables caching, but is reproducible
                f'autogenerated/{workflow_name}.cwl', f'autogenerated/{workflow_name}_inputs.yml']
    else:
        pass
    cmdline = ' '.join(cmd)

    retval = 1  # overwrite on success
    print('Running ' + cmdline)
    if use_subprocess:
        # To run in parallel (i.e. pytest ... --workers 8 ...), we need to
        # use separate processes. Otherwise:
        # "signal only works in main thread or with __pypy__.thread.enable_signals()"
        proc = sub.run(cmd, check=False)
        retval = proc.returncode
    else:
        print('via cwltool.main.main python API')
        try:
            if cwl_runner == 'cwltool':
                retval = cwltool.main.main(cmd[1:])
            elif cwl_runner == 'toil-cwl-runner':
                retval = toil.cwl.cwltoil.main(cmd[1:])
            else:
                raise Exception("Invalid cwl_runner!")

            print(f'Final output json metadata blob is in output_{workflow_name}.json')
        except Exception as e:
            print('Failed to execute', workflow_name)
            print(f'See error_{workflow_name}.txt for detailed technical information.')
            # Do not display a nasty stack trace to the user; hide it in a file.
            with open(f'error_{workflow_name}.txt', mode='w', encoding='utf-8') as f:
                # https://mypy.readthedocs.io/en/stable/common_issues.html#python-version-and-system-platform-checks
                if sys.version_info >= (3, 10):
                    traceback.print_exception(type(e), value=e, tb=None, file=f)
            print(e)  # we are always running this on CI
    return retval


def write_out_to_disk(res: Json, workflow_name: str) -> None:
    "write compiled output to before running through cwl_runner entrypoints"
    res_cwl = copy.deepcopy(res)
    res_cwl.pop('retval', None)
    res_cwl.pop('cwlJobInputs', None)
    res_cwl.pop('name', None)
    compiled_cwl = workflow_name + '.cwl'
    inputs_yml = workflow_name + '_inputs.yml'
    # write compiled .cwl file
    with open(Path.cwd() / 'autogenerated' / compiled_cwl, 'w', encoding='utf-8') as f:
        yaml.dump(res_cwl, f)
    # write _input.yml file
    with open(Path.cwd() / 'autogenerated' / inputs_yml, 'w', encoding='utf-8') as f:
        yaml.dump(res['cwlJobInputs'], f)


def prepare_call_rest_api(inp_path: Path) -> Json:
    """prepare payload and call rest api"""
    with open(inp_path, 'r', encoding='utf-8') as f:
        inp = json.load(f)
    print('----------- from rest api ----------- \n\n')
    scope = {}
    scope['type'] = 'http'

    async def receive() -> Json:
        inp_byte = json.dumps(inp).encode('utf-8')
        return {"type": "http.request", "body": inp_byte}

    # create a request object and pack it with our json payload
    req: Request = Request(scope)
    req._receive = receive
    res: Json = asyncio.run(restapi.compile_wf(req))  # call to rest api
    return res


@pytest.mark.skip_pypi_ci
@pytest.mark.fast
def test_rest_core_single_node() -> None:
    """A simple single node sophios/restapi test"""
    basepath = 'autogenerated'
    inp_file = "single_node.json"
    inp_path = Path(__file__).parent / 'rest_wfb_objects' / inp_file
    workflow_name = inp_file.split('.', maxsplit=1)[0]
    # write compiled_cwl and inputs_yml
    res = prepare_call_rest_api(inp_path)
    output_dirs = pc.find_output_dirs(res)
    pc.create_output_dirs(output_dirs, basepath)
    write_out_to_disk(res, workflow_name)
    retval = run_cwl_local(workflow_name, 'cwltool', 'docker', False)
    assert retval == 0


@pytest.mark.skip_pypi_ci
def test_rest_core_single_node_bbbc() -> None:
    """A simple single node sophios/restapi test"""
    basepath = 'autogenerated'
    inp_file = "single_node_bbbc_download.json"
    inp_path = Path(__file__).parent / 'rest_wfb_objects' / inp_file
    workflow_name = inp_file.split('.', maxsplit=1)[0]
    # write compiled_cwl and inputs_yml
    res = prepare_call_rest_api(inp_path)
    output_dirs = pc.find_output_dirs(res)
    pc.create_output_dirs(output_dirs, basepath)
    write_out_to_disk(res, workflow_name)
    retval = run_cwl_local(workflow_name, 'cwltool', 'docker', False)
    assert retval == 0


@pytest.mark.skip_pypi_ci
def test_rest_core_bbbc_download_wfb() -> None:
    """A simple multi node (inline cwl) sophios/restapi test"""
    basepath = 'autogenerated'
    inp_file = "bbbc_download_wfb.json"
    inp_path = Path(__file__).parent / 'rest_wfb_objects' / inp_file
    workflow_name = inp_file.split('.', maxsplit=1)[0]
    # write compiled_cwl and inputs_yml
    res = prepare_call_rest_api(inp_path)
    output_dirs = pc.find_output_dirs(res)
    pc.create_output_dirs(output_dirs, basepath)
    write_out_to_disk(res, workflow_name)
    retval = run_cwl_local(workflow_name, 'cwltool', 'docker', False)
    assert retval == 0


@pytest.mark.fast
@pytest.mark.skip_pypi_ci
def test_rest_core_multi_node_file() -> None:
    """A simple multi node sophios/restapi test"""
    basepath = 'autogenerated'
    inp_file = "multi_node.json"
    inp_path = Path(__file__).parent / 'rest_wfb_objects' / inp_file
    workflow_name = inp_file.split('.', maxsplit=1)[0]
    # write compiled_cwl and inputs_yml
    res = prepare_call_rest_api(inp_path)
    output_dirs = pc.find_output_dirs(res)
    pc.create_output_dirs(output_dirs, basepath)
    write_out_to_disk(res, workflow_name)
    retval = run_cwl_local(workflow_name, 'cwltool', 'docker', False)
    assert retval == 0


@pytest.mark.fast
@pytest.mark.skip_pypi_ci
def test_rest_core_multi_node_inline_cwl() -> None:
    """A simple multi node (inline cwl) sophios/restapi test"""
    basepath = 'autogenerated'
    inp_file = "multi_node_inline_cwl.json"
    inp_path = Path(__file__).parent / 'rest_wfb_objects' / inp_file
    workflow_name = inp_file.split('.', maxsplit=1)[0]
    # write compiled_cwl and inputs_yml
    res = prepare_call_rest_api(inp_path)
    output_dirs = pc.find_output_dirs(res)
    pc.create_output_dirs(output_dirs, basepath)
    write_out_to_disk(res, workflow_name)
    retval = run_cwl_local(workflow_name, 'cwltool', 'docker', False)
    assert retval == 0
