import json
import os
import os.path
import shutil
import subprocess
import time
import uuid
import warnings
from itertools import repeat
from pathlib import Path

from pharmpy.model.external.nonmem import convert_model
from pharmpy.modeling import write_csv, write_model
from pharmpy.tools.external.nonmem import conf, parse_modelfit_results
from pharmpy.workflows import ModelEntry

PARENT_DIR = f'..{os.path.sep}'


def execute_model(model_entry, db):
    assert isinstance(model_entry, ModelEntry)
    model = model_entry.model

    database = db.model_database
    parent_model = model.parent_model
    model = convert_model(model)
    model = model.replace(parent_model=parent_model)
    path = Path.cwd() / f'NONMEM_run_{model.name}-{uuid.uuid1()}'

    # NOTE: This deduplicates the dataset before running NONMEM, so we know which
    # filename to give to this dataset.
    database.store_model(model)
    # NOTE: We cannot reuse model_with_correct_datapath as the model object
    # later because it might have lost some of the ETA names mapping due to the
    # current incomplete implementation of serialization of Pharmpy Model
    # objects through the NONMEM plugin. Hopefully we can get rid of this
    # hack later.
    model_with_correct_datapath = database.retrieve_model(model.name)
    stream = model_with_correct_datapath.internals.control_stream
    data_record = stream.get_records('DATA')[0]
    relative_dataset_path = data_record.filename

    # NOTE: We set up a directory tree that replicates the structure generated by
    # the database so that NONMEM writes down the correct relative paths in
    # generated files such as results.lst.
    # NOTE: It is important that we do this in a DB-agnostic way so that we do
    # not depent on its implementation.
    depth = relative_dataset_path.count(PARENT_DIR)
    # NOTE: This creates an FS tree branch x/x/x/x/...
    model_path = path.joinpath(*repeat('x', depth))
    meta = model_path / '.pharmpy'
    meta.mkdir(parents=True, exist_ok=True)
    # NOTE: This removes the leading ../
    relative_dataset_path_suffix = relative_dataset_path[len(PARENT_DIR) * depth :]
    # NOTE: We do not support non-leading ../, e.g. a/b/../c
    assert PARENT_DIR not in relative_dataset_path_suffix
    dataset_path = path / Path(relative_dataset_path_suffix)
    datasets_path = dataset_path.parent
    datasets_path.mkdir(parents=True, exist_ok=True)

    # NOTE: Write dataset and model files so they can be used by NONMEM.
    model = write_csv(model, path=dataset_path, force=True)
    model = write_model(model, path=model_path, force=True)

    args = nmfe(
        model.name + model.filename_extension,
        'results.lst',
    )

    stdout = model_path / 'stdout'
    stderr = model_path / 'stderr'

    with open(stdout, "wb") as out, open(stderr, "wb") as err:
        result = subprocess.run(
            args, stdin=subprocess.DEVNULL, stderr=err, stdout=out, cwd=str(model_path)
        )

    basename = Path(model.name)

    results_path = model_path / 'results.lst'
    start = time.time()
    timeout = 5

    while True:
        try:
            results_path.rename((model_path / basename).with_suffix('.lst'))
            break
        except FileNotFoundError:
            elapsed_time = time.time() - start
            if elapsed_time >= timeout:
                warnings.warn(f'UNEXPECTED Could not find .lst-file after waiting {elapsed_time}s')
                break
            else:
                time.sleep(1)

    metadata = {
        'plugin': 'nonmem',
        'path': str(model_path),
    }

    plugin = {
        'commands': [
            {
                'args': args,
                'returncode': result.returncode,
                'stdout': 'stdout',
                'stderr': 'stderr',
            }
        ]
    }

    modelfit_results = parse_modelfit_results(model, model_path / basename)

    log = modelfit_results.log if modelfit_results else None
    model_entry = model_entry.attach_results(modelfit_results=modelfit_results, log=log)

    with database.transaction(model_entry) as txn:
        if (
            not (model_path / basename).with_suffix('.lst').is_file()
            or not (model_path / basename).with_suffix('.ext').is_file()
        ):
            warnings.warn(f'Expected result files do not exist, copying everything: {basename}')
            for file in path.glob('*'):
                txn.store_local_file(file)
        else:
            for suffix in ['.lst', '.ext', '.phi', '.cov', '.cor', '.coi', '.grd']:
                file_path = (model_path / basename).with_suffix(suffix)
                txn.store_local_file(file_path)

            for rec in model.internals.control_stream.get_records('TABLE'):
                txn.store_local_file(model_path / rec.path)

        txn.store_local_file(stdout)
        txn.store_local_file(stderr)

        plugin_path = model_path / 'nonmem.json'
        with open(plugin_path, 'w') as f:
            json.dump(plugin, f, indent=2)

        txn.store_local_file(plugin_path)

        txn.store_metadata(metadata)

        txn.store_model_entry()

    return model_entry


def nmfe_path():
    if os.name == 'nt':
        nmfe_candidates = ['nmfe75.bat', 'nmfe74.bat', 'nmfe73.bat']
    else:
        nmfe_candidates = ['nmfe75', 'nmfe74', 'nmfe73']
    path = conf.default_nonmem_path
    if path != Path(''):
        path /= 'run'
        for nmfe in nmfe_candidates:
            candidate_path = path / nmfe
            if candidate_path.is_file():
                path = candidate_path
                break
        else:
            raise FileNotFoundError(f'Cannot find nmfe script for NONMEM ({path})')
    else:
        # Not in configuration file
        for nmfe in nmfe_candidates:
            candidate_path = shutil.which(nmfe)
            if candidate_path is not None:
                path = candidate_path
                break
        else:
            raise FileNotFoundError(
                'No path to NONMEM configured in pharmpy.conf and nmfe not in PATH'
            )
    return str(path)


def nmfe(*args):
    conf_args = []
    if conf.licfile is not None:
        conf_args.append(f'-licfile={str(conf.licfile)}')

    return [
        nmfe_path(),
        *args,
        *conf_args,
    ]
