# coding=utf-8
from __future__ import print_function
import os
import argparse
import base64
import json
import logging
import requests
import shutil
import tempfile
import subprocess
from collections import OrderedDict
from ibm_apidocs_cli.version import __version__
from lxml import html
from outdated import warn_if_outdated

logger = logging.getLogger(__name__)


def get_argument_parser():
    """
    generate a CLI arguments parser
    Returns:
       argument parser
    """
    parser = argparse.ArgumentParser(description='Generate the apidocs files.')
    # required parameters
    requiredArgs = parser.add_argument_group('Required arguments')
    requiredArgs.add_argument(
        '-i', '--openapi', metavar='input-oas', help='Input OpenAPI specification file path or url', required=True)
    # Optional parameters
    optionalArgs = parser._action_groups.pop()
    parser.add_argument('-c', '--config', metavar='config-file', help='Name of front matter config file')
    parser.add_argument('--mapfile', metavar='map-file', help='Path to mapping file')
    parser.add_argument('--apidocs', metavar='apidocs-dir', help='Path to apidocs repository containing config files. Default is current directory.')
    parser.add_argument('--templates', metavar='templates-dir', help='Path to directory containing custom front matter templates.')
    parser.add_argument('--output_folder', metavar='output-folder', help='Generated apidocs output folder')
    parser.add_argument('--frontmatter', metavar='frontmatter-cli', help='Frontmatter repository or local CLI')
    parser.add_argument('--sdk_generator', metavar='sdkgen-jar', help='Path to location of SDK generator JAR file')
    parser.add_argument('--sdkgen_release', metavar='sdkgen-release', help='Release of SDK generator to download')
    parser.add_argument('--keep_sdk', action='store_true', help='Keep generated SDK files')
    parser.add_argument('--keep_temp', action='store_true', help='Keep temp directory')
    parser.add_argument('--no_update', action='store_true', help='Do not update SDK versions in front matter config')
    parser.add_argument('--verbose', action='store_true', help='verbose flag')
    parser.add_argument('--version', action='version',
                        version='%(prog)s {version}'.format(version=__version__))

    parser._action_groups.append(optionalArgs)
    return parser


def process_args(parser):
    """
    Parses the parser
    Returns:
        dict -- dictionary with the arguments and values
    """
    args = parser.parse_args()

    # validate environment
    if 'throw' in args:
        logger.error(args.throw)
        exit(1)

    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)

    return args


def main():
    parser = get_argument_parser()
    args = process_args(parser)

    temp_dir = tempfile.mkdtemp()

    if not args.config:
        mapping = get_mapping(args.mapfile)
    else:
        mapping = None

    openapi_abs = os.path.abspath(args.openapi)
    openapi_file = get_basename(args.openapi)
    frontmatter_cli = get_frontmatter_cli(args.frontmatter, temp_dir)
    sdk_generator_cli = get_sdk_generator_cli(args.sdk_generator, args.sdkgen_release, temp_dir)
    supported_languages = get_supported_languages(openapi_abs)
    templates_dir = get_frontmatter_templates(frontmatter_cli, args.templates)

    if args.output_folder:
        output_folder_abs = os.path.abspath(args.output_folder)
    else:
        output_folder_abs = os.getcwd()

    if args.apidocs:
        apidocs_dir = os.path.abspath(args.apidocs)
    else:
        apidocs_dir = os.getcwd()

    input_frontmatter_config = get_config_file(args.config, openapi_file, apidocs_dir, mapping)

    output_frontmatter_config = os.path.join(output_folder_abs, os.path.basename(input_frontmatter_config))

    frontmatter_md_file = os.path.join(output_folder_abs, os.path.splitext(openapi_file)[0])+'.md'

    logger.info('openapi_file: %s' % openapi_file)
    logger.info('config_file: %s' % input_frontmatter_config)
    logger.info('frontmatter_cli: %s' % frontmatter_cli)
    logger.info('sdk_generator_cli: %s' % sdk_generator_cli)
    logger.info('supported_languages: %s' % supported_languages)
    logger.info('templates_dir: %s' % templates_dir)

    if not os.path.exists(output_folder_abs):
        logger.info('Creating output directory %s' % output_folder_abs)
        os.makedirs(output_folder_abs)

    logger.info('Creating the frontmatter configuration')
    create_frontmatter_config_file(openapi_file=openapi_abs, input_config_file=input_frontmatter_config,
                                   supported_languages=supported_languages, output_config_file=output_frontmatter_config,
                                   no_update=args.no_update)

    logger.info('Calling frontmatter to generate the md file')
    logger.info('With %s' % templates_dir)
    create_frontmatter_md_file(frontmatter_cli=frontmatter_cli, openapi_file=openapi_abs,
                               frontmatter_config_file=output_frontmatter_config, output_file=frontmatter_md_file,
                               templates_dir=templates_dir)

    logger.info('Creating language specific middle panel')
    create_language_specific_files(sdk_generator_cli=sdk_generator_cli, supported_languages=supported_languages,
                                   openapi_file=openapi_abs, apidocs_folder=apidocs_dir, output_folder=output_folder_abs,
                                   keep_sdk=args.keep_sdk)

    if not args.keep_temp:
        logger.info('Removing temporary directory %s' % temp_dir)
        shutil.rmtree(temp_dir)

def get_mapping(mapfile=None):
    '''If required, loads the API definition mapping file from the specified location, or from GitHub.'''

    if mapfile:
        with open(mapfile) as f:
            mapping = json.load(f)
        logger.info('Loaded map file from %s' % mapfile)
    else:
        github_token = os.environ.get('GITHUB_TOKEN')
        if not github_token:
            raise ValueError('Cannot download map file without GitHub token. Either set GITHUB_TOKEN or specify local map file.')
        mapfile_url = 'https://api.github.ibm.com/repos/Watson/developer-cloud--api-definitions/contents/generate-apidocs.json'
        r = requests.get(mapfile_url,
                         headers={'Authorization': 'token '+github_token})
        r.raise_for_status()

        mapping = json.loads(base64.b64decode(r.json()['content']).decode())

        logger.info('Loaded map file from %s' % mapfile_url)

    return mapping

def get_config_file(config_file=None, openapi_file=None, apidocs_dir=None, mapping=None):
    '''Get config file name either from command-line argument or from mapping file.'''

    if not config_file:
        config_filename = next(item for item in mapping['public']+mapping['private'] if item['openapi']==openapi_file)['config']
        config_file = os.path.join(apidocs_dir, config_filename)
        logger.info('Looked up config file: %s' % config_file)
    
    if os.path.dirname(config_file) == '':
        config_file = os.path.join(apidocs_dir, config_file)

    return config_file

def get_basename(filepath):
    '''Returns the basename of the given filepath without the file extension'''

    if not filepath:
        raise ValueError('openapi file path cannot be null or empty')
    basename = os.path.basename(filepath)
    return basename

def get_frontmatter_cli(frontmatter, temp_dir):
    '''Returns the absolute path to the frontmatter app.js file

    Arguments:
      sdk_generator {String} -- The path to the app.js file or the folder where the app.js file is
      temp_dir {String} -- The path to the temp directory, used to clone the repo if necessary

    Raises:
      ValueError -- if the given path doesn't exist

    Returns:
      String -- The absolute file path to the frontmatter app.js file
    '''

    if not frontmatter:
        github_token = os.environ.get('GITHUB_TOKEN')
        if not github_token:
            raise ValueError('Cannot download front-matter generator without GitHub token. Either set GITHUB_TOKEN or specify front-matter generator location.')
        frontmatter = os.path.join(temp_dir, 'frontmatter')
        logger.info('Cloning front-matter generator to %s' % frontmatter)
        subprocess.call(['git',
                         'clone',
                         'https://'+github_token+'@github.ibm.com/cloud-doc-build/frontmatter-generator.git',
                         frontmatter])
        subprocess.call(['npm',
                         'install',
                         '--prefix',
                         frontmatter])

    cli = os.path.join(os.path.abspath(frontmatter), 'app.js')
    logger.info('Using front-matter CLI at %s' % cli)

    if not os.path.exists(cli):
        raise ValueError('Front-matter generator not found at %s.' % cli)

    return cli

def get_frontmatter_templates(frontmatter_cli, templates):
    '''Returns the absolute path to the frontmatter templates to use.

    Arguments:
      frontmatter {String} -- The path to the frontmatter generator being used
      custom_templates {String} -- The path to the custom templates, if specified
    '''
    if not templates:
        templates = 'templates'
    if os.path.isabs(templates):
        return templates
    else:
        return os.path.join(os.path.dirname(frontmatter_cli), templates)

def get_sdk_generator_cli(sdk_generator=None, sdkgen_release=None, temp_dir=None):
    '''Returns the absolute path to the Jar file where the SDK Generator is

    Arguments:
      sdk_generator {String} -- The path to the jar file or the folder where the jar file is
      sdkgen_release {String} -- The release of the SDK generator to download
      temp_dir {String} -- The path to the temp directory, used to download the jar file if necessary

    Raises:
      ValueError -- if the given path doesn't exist

    Returns:
      String -- The absolute file path to the SDK Generator jar file
    '''

    if not sdk_generator:
        github_token = os.environ.get('GITHUB_TOKEN')
        if not github_token:
            raise ValueError('Cannot download SDK generator without GitHub token. Either set GITHUB_TOKEN or specify SDK generator JAR file location.')
        if sdkgen_release:
            sdkgen_url='https://api.github.ibm.com/repos/CloudEngineering/openapi-sdkgen/releases/tags/'+sdkgen_release
        else:
            sdkgen_url='https://api.github.ibm.com/repos/CloudEngineering/openapi-sdkgen/releases/latest'

        r = requests.get(sdkgen_url,
                         headers={'Authorization': 'token '+github_token})
        r.raise_for_status()
        sdkgen_release = json.loads(r.text)
        
        sdkgen_jar = 'openapi-sdkgen-'+sdkgen_release['tag_name']+'.jar'
        logger.info('Downloading SDK generator JAR file %s' % sdkgen_jar)

        r = requests.get(sdkgen_release['assets_url'],
                         headers={'Authorization': 'token '+github_token})
        sdkgen_assets = json.loads(r.text)

        jar_asset = next((i for i in sdkgen_assets if i['name'] == sdkgen_jar), None)

        r = requests.get(jar_asset['url'],
                         headers={'Authorization': 'token '+github_token,
                                  'Accept': 'application/octet-stream'}
                        )
        with open(os.path.join(temp_dir, 'openapi-sdkgen.jar'), 'wb') as f:
            f.write(r.content)

        sdk_generator = temp_dir

    if os.path.isdir(sdk_generator):
        cli = os.path.abspath(os.path.join(sdk_generator, 'openapi-sdkgen.jar'))
    else:
        cli = os.path.abspath(sdk_generator)

    if os.path.isdir(sdk_generator):
        cli = os.path.abspath(os.path.join(sdk_generator, 'openapi-sdkgen.jar'))
    else:
        cli = os.path.abspath(sdk_generator)

    if not os.path.isfile(cli):
        raise ValueError('Could not find SDK generator JAR at %s' % sdk_generator)

    return cli

def get_supported_languages(openapi_file=None):
    '''
    Generates the list of supported languages to be used by the SDK generator.
    The list is derived from the x-sdk-supported-languages in the OAS API definition file

    Arguments:
      openapi_file {String} -- The name of the OAS API definition file

    Raises:
      ValueError -- if a required file cannot be found or parsed

    Returns:
      String -- The array of supported languages
    '''

    with open(openapi_file) as f:
        openapi = json.load(f)

    if 'x-sdk-supported-languages' in openapi['info']:
        supported_languages = openapi['info']['x-sdk-supported-languages']
    else:
        supported_languages = []

    return supported_languages


def get_latest_sdk_version(language):
    '''
    Performs an HTTP GET to retrieve the GitHub release page for the latest
    SDK release for the specified language, and parses the returned HTML to
    find the release number.

    Arguments:
      language {String} -- The name of the SDK language (for example, "java").

    Raises:
      RuntimeError -- If the version number cannot be identified from the page

    Returns:
      String -- The version number (for example, "2.1").
    '''
    url = 'https://github.com/watson-developer-cloud/' + \
        language + '-sdk/releases/latest'
    page = html.fromstring(requests.get(url).content)

    matches = page.xpath(r"//div[@class='release-header'][1]//a[re:match(text(), '^[a-zA-Z\-]*[\d.]+$')]",
                         namespaces={"re": "http://exslt.org/regular-expressions"})

    if matches:
        if matches[0].text[0] == 'v':
            sdk_ver = matches[0].text[1:]
        elif matches[0].text.startswith('java-sdk-'):
            sdk_ver = matches[0].text[9:]
        else:
            sdk_ver = matches[0].text
    else:
        raise RuntimeError(
            'Latest SDK version for %s not found in expected format' % language)

    return sdk_ver


def create_frontmatter_config_file(openapi_file=None, input_config_file=None, supported_languages=None, output_config_file=None, no_update=False):
    '''
    Generates the config file that will be used for front matter generation.
    Any value specified in the input YAML config will be passed through
    as-is. Other values (if required) are derived from the OAS file or
    queried from GitHub.

    Arguments:
      openapi_file {String} -- The name of the OAS API definition file
      config_file {String} -- The name of the input config YAML file
      supported_languages {String} -- The supported languages from the OAS API definition file
      output_file {String} -- The name of the generated front matter config file
      no_update {boolean} -- Uses the input config as-is without updating SDK versions

    Raises:
      ValueError -- if a required file cannot be found or parsed

    '''

    with open(input_config_file) as f:
        input_fm_config = json.load(f, object_pairs_hook=OrderedDict)
    with open(openapi_file) as f:
        openapi = json.load(f)

    output_fm_config = OrderedDict(input_fm_config.items())
    if 'serviceMajorVersion' not in output_fm_config:
        output_fm_config['serviceMajorVersion'] = openapi['info']['version'].split('.')[
            0]

    if not no_update:
        for l in supported_languages:
            output_fm_config[l+'SdkVersion'] = get_latest_sdk_version(l)

    with open(output_config_file, 'w') as f:
        json.dump(output_fm_config, f, indent=2, separators=(',', ': '))


def create_frontmatter_md_file(frontmatter_cli=None, openapi_file=None, frontmatter_config_file=None, output_file=None, templates_dir=None):
    '''
    Generates the Markdown source file (md) file from the front matter config file generated
    in the previous step.  Assumes the frontmatter-generator repository has been downloaded and
    is accessible to enable the call to app.js to execute properly.

    Arguments:
      frontmatter_cli {String} -- The name of the frontmatter app.js file
      openapi_file {String} -- The name of the OAS API definition file
      frontmatter_config_file {String} -- The name of the generated front matter config file
      output_file {String} -- The name of the generated md file

    Raises:
      ValueError -- if a required file cannot be found or parsed
    '''

    logger.info('\n')
    logger.info('frontmatter_cli: %s' % frontmatter_cli)
    logger.info('openapi_file: %s' % openapi_file)
    logger.info('frontmatter_config_file: %s' % frontmatter_config_file)
    logger.info('output_file: %s' % output_file)

    fg_command = "node %s -i %s -c %s -t %s -o %s" % (
        frontmatter_cli, openapi_file, frontmatter_config_file, templates_dir, output_file)

    working_directory = os.path.dirname(frontmatter_cli)
    logger.info('frontmatter generator command: %s' % fg_command)
    logger.info('frontmatter generator location: %s' % working_directory)
    logger.info('custom frontmatter templates location: %s' % templates_dir)

    subprocess.call(fg_command, shell=True, cwd=working_directory)


def create_language_specific_files(sdk_generator_cli=None, supported_languages=None, openapi_file=None, apidocs_folder=None, output_folder=None, keep_sdk=False):
    '''
    Generates the middle panel for all supported languages.  Assumes the SDK generator (openapi-sdkgen) release directory has been
    downloaded and is accessible to enable the call to openapi-sdkgen.sh to execute properly.

    Arguments:
      sdk_generator_cli {String} -- The name of the sdk generator shell script
      supported_languages {String} -- The supported languages from the OAS API definition file
      openapi_file {String} -- The name of the OAS API definition file
      output_folder {String} -- The name of the folder where the middle column for each language will be written
      keep_sdk {boolean} -- Whether to keep the generated SDK files for debugging purposes

    Raises:
      ValueError -- if a required file cannot be found or parsed
    '''

    logger.info('\n')
    logger.info('sdk_generator_cli: %s' % sdk_generator_cli)
    logger.info('supported_languages: %s' % supported_languages)
    logger.info('openapi_file: %s' % openapi_file)
    logger.info('apidocs_folder: %s' % apidocs_folder)
    logger.info('output_folder: %s' % output_folder)

    working_directory = os.path.dirname(sdk_generator_cli)
    logger.info('SDK generator location: %s' % working_directory)

    index_file = os.path.join(apidocs_folder, 'apiref-index.json')
    if os.path.isfile(index_file):
        with open(index_file) as f:
            apiref_index = json.load(f)
    else:
        logger.warning('apiref-index.json not found.')
        apiref_index = {}

    for sdk in supported_languages:
        watson_sdk = 'watson-' + sdk
        oas_filename = os.path.basename(openapi_file)
        if oas_filename in apiref_index:
            if sdk in apiref_index[oas_filename]:
                target_filename = apiref_index[oas_filename][sdk]
            else:
                target_filename = sdk + '-apiref.json'
                logger.warning('Language '+sdk+' not found in apiref-index.json. Using default file name '+target_filename+'.')
        else:
            target_filename = sdk + '-apiref.json'
            logger.warning(oas_filename+' not found in apiref-index.json. Using default file name '+target_filename+'.')
        sdk_temp = os.path.join(output_folder, '_sdktemp')
        sdk_command = "java -jar %s generate -i %s -g %s -o %s --apiref %s" % (
            sdk_generator_cli, openapi_file, watson_sdk, sdk_temp, os.path.join(output_folder, target_filename))
        logger.info('SDK generator command: %s' % sdk_command)
        subprocess.call(sdk_command, shell=True, cwd=working_directory)
        if not keep_sdk:
            logger.info('Deleting SDK artifacts directory %s' % sdk_temp)
            shutil.rmtree(sdk_temp)
