#!/usr/bin/env python3

import argparse
import dotenv
import json
import os
import requests
import requests.exceptions

config = {
    **dotenv.dotenv_values("secret/secret.env"),
    **dotenv.dotenv_values("config/config.env"),
    **dotenv.dotenv_values("shared/shared.env"),
}

EXAMPLE_SIMULATIONS_FILENAME = os.path.join(os.path.dirname(__file__), 'example-projects.json')

EXAMPLE_SIMULATIONS_RUNS_FILENAME = os.path.join(os.path.dirname(__file__),
                                                 '..', 'apps', 'dispatch', 'src', 'app', 'components',
                                                 'simulations', 'browse', 'example-simulations.{}.json')

BIOSIMULATIONS_API_AUTH_ENDPOINT = config.get('AUTH0_DOMAIN') + 'oauth/token'
BIOSIMULATIONS_API_AUDIENCE = config.get('RUNBIOSIMULATIONS_EXAMPLES_API_AUDIENCE')
BIOSIMULATIONS_API_CLIENT_ID = config.get('RUNBIOSIMULATIONS_EXAMPLES_CLIENT_ID', None)
BIOSIMULATIONS_API_CLIENT_SECRET = config.get('RUNBIOSIMULATIONS_EXAMPLES_CLIENT_SECRET', None)

if not BIOSIMULATIONS_API_CLIENT_ID:
    raise ValueError('A BioSimulations API client id (`RUNBIOSIMULATIONS_EXAMPLES_CLIENT_ID`) must be provided')
if not BIOSIMULATIONS_API_CLIENT_SECRET:
    raise ValueError('A BioSimulations API client secret (`RUNBIOSIMULATIONS_EXAMPLES_CLIENT_SECRET`) must be provided')


def get_status_endpoint(biosimulations_deployment):
    """ Get the endpoint for getting the status of runs

    Args:
        biosimulations_deployment (:obj:`str`): which deployment of the BioSimulations deployment to use (``dev``, ``prod``, or ``local``)

    Returns:
        :obj:`str`: endpoint for getting the status of runs
    """
    if biosimulations_deployment == 'local':
        return 'http://localhost:3333/runs'
    elif biosimulations_deployment == 'dev':
        return 'https://api.biosimulations.dev/runs'
    else:
        return 'https://api.biosimulations.org/runs'


def get_project_endpoint(biosimulations_deployment, project_id):
    """ Get the endpoint for getting, publishing, and updating projects

    Args:
        biosimulations_deployment (:obj:`str`): which deployment of the BioSimulations deployment to use (``dev``, ``prod``, or ``local``)
        project_id (:obj:`str`): project id

    Returns:
        :obj:`str`: endpoint for getting, publishing, and updating projects
    """
    if biosimulations_deployment == 'local':
        return 'http://localhost:3333/projects/' + project_id
    elif biosimulations_deployment == 'dev':
        return 'https://api.biosimulations.dev/projects/' + project_id
    else:
        return 'https://api.biosimulations.org/projects/' + project_id


def did_run_succeed(biosimulations_deployment, id):
    """ Check if a simulation run succeeded

    Args:
        biosimulations_deployment (:obj:`str`): which deployment of the BioSimulations deployment to use (``dev``, ``prod``, or ``local``)
        id (:obj:`str`): id of simulation run

    Returns:
        :obj:`bool`: whether the simulation run succeeded
    """
    response = requests.get(get_status_endpoint(biosimulations_deployment) + '/' + id)
    response.raise_for_status()
    return response.json()['status'] == 'SUCCEEDED'


def get_auth_headers_for_biosimulations_deployment():
    """ Get authorization headers for using the BioSimulations REST API.

    Returns:
        :obj:`dict`: authorization headers
    """
    response = requests.post(BIOSIMULATIONS_API_AUTH_ENDPOINT, json={
        'client_id': BIOSIMULATIONS_API_CLIENT_ID,
        'client_secret': BIOSIMULATIONS_API_CLIENT_SECRET,
        'audience': BIOSIMULATIONS_API_AUDIENCE,
        "grant_type": "client_credentials",
    })
    response.raise_for_status()
    response_data = response.json()
    return {'Authorization': response_data['token_type'] + ' ' + response_data['access_token']}


def main(biosimulations_deployment, example_names, dry_run=False):
    """ Publish example project to BioSimulations

    Args:
        biosimulations_deployment (:obj:`str`): which deployment of the BioSimulations deployment to use (``dev``, ``prod``, or ``local``)
        example_names (:obj:`list` of :obj:`str`): names of examples to publish. Default: publish all examples.
        dry_run (:obj:`bool`, optional): If :obj:`True`, do not submit simulations to runBioSimulations
    """

    # read examples
    with open(EXAMPLE_SIMULATIONS_FILENAME, 'r') as file:
        examples = json.load(file)
    examples.sort(key=lambda example: example['name'])
    num_total_examples = len(examples)

    # filter out examples that shouldn't be published
    examples_not_publish = list(filter(
        lambda example:
        not example['publishToBioSimulations'],
        examples))
    examples = list(filter(
        lambda example:
        example['publishToBioSimulations'],
        examples))

    # filter to selected examples
    if example_names:
        examples = list(
            filter(lambda example: example['name'] in example_names, examples))

        missing_example_names = set(example_names).difference(
            set(example['name'] for example in examples))
        if missing_example_names:
            raise ValueError('No examples have the following names:\n  - {}'.format(
                '\n  - '.join(sorted(missing_example_names))))

    # get runs
    example_runs_filename = EXAMPLE_SIMULATIONS_RUNS_FILENAME.format(
        'dev' if biosimulations_deployment == 'local' else biosimulations_deployment
    )
    with open(example_runs_filename, 'r') as file:
        runs = {run['name']: run for run in json.load(file)}

    # filter out examples that don't have runs
    examples_no_runs = list(filter(
        lambda example:
        example['name'] not in runs,
        examples))
    examples = list(filter(
        lambda example:
        example['name'] in runs,
        examples))

    # filter out examples whose runs didn't succeed
    examples_failed_runs = list(filter(
        lambda example:
        not did_run_succeed(biosimulations_deployment, runs[example['name']]['id']),
        examples))
    examples = list(filter(
        lambda example:
        did_run_succeed(biosimulations_deployment, runs[example['name']]['id']),
        examples))

    # publish examples
    auth_headers = get_auth_headers_for_biosimulations_deployment()
    created = []
    updated = []
    failures = []
    for example in examples:
        run = runs[example['name']]

        if not example['bioSimulationsId']:
            raise ValueError('{} must provide a BioSimulations id'.format(example['name']))

        endpoint = get_project_endpoint(biosimulations_deployment, example['bioSimulationsId'])
        response = requests.get(endpoint)
        try:
            response.raise_for_status()
            if run['id'] == response.json()['simulationRun']:
                continue

            method = requests.put
        except requests.exceptions.RequestException:
            method = requests.post

        if not dry_run:
            response = method(
                endpoint,
                headers=auth_headers,
                json={
                    "id": example['bioSimulationsId'],
                    "simulationRun": run['id'],
                })
            try:
                response.raise_for_status()
                if method == requests.post:
                    created.append(example['name'])
                else:
                    updated.append(example['name'])

            except requests.exceptions.HTTPError as exception:
                failures.append(example['name'] + ': ' + str(exception))

    print('{} examples were created{}'.format(
        len(created),
        ''.join('\n  ' + example for example in created)))
    print('{} examples were updated{}'.format(
        len(updated),
        ''.join('\n  ' + example for example in updated)))
    print('{} examples were already published'.format(
        len(examples) - (len(created) + len(updated))))
    print('{} examples could not be published{}'.format(
        len(failures),
        ''.join('\n  ' + example for example in failures)))
    print('{} examples had were skipped because they are not marked for publication{}'.format(
        len(examples_not_publish),
        ''.join('\n  ' + example['name'] for example in examples_not_publish)))
    print('{} examples had no runs{}'.format(
        len(examples_no_runs),
        ''.join('\n  ' + example['name'] for example in examples_no_runs)))
    print('{} examples had failed runs{}'.format(
        len(examples_failed_runs),
        ''.join('\n  ' + example['name'] for example in examples_failed_runs)))
    print('{} examples were skipped'.format(
        num_total_examples
        - len(examples)
        - len(examples_not_publish)
        - len(examples_no_runs)
        - len(examples_failed_runs)
    ))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Publish the example simulation runs to BioSimulations.'
    )
    parser.add_argument(
        '--biosimulations-deployment', type=str, default='dev',
        help='BioSimulations deployment which projects should be published to (`dev`, `prod`, `local`). Default: `dev`.'
    )
    parser.add_argument(
        '--example', type=str, nargs='*',
        help='Names of the example projects to publish. Default: publish all projects.',
        default=None,
        dest='example_names',
    )
    parser.add_argument(
        '--dry-run',
        help='If set, do not publish simulations to BioSimulations',
        action='store_true',

    )
    args = parser.parse_args()

    main(biosimulations_deployment=args.biosimulations_deployment,
         example_names=args.example_names,
         dry_run=args.dry_run)
