""" Execute Kotlin linter and show results to CodeSonar. """

import argparse
import os
import platform
from shlex import split as cmdline2list
from subprocess import Popen, list2cmdline
import sys
from tempfile import mkstemp

from gtr import gthome
from gtr.globmatch import glob, globescape

import import_sarif


BAT_EXT = '.bat'
DETEKT_WARNINGS_RETURNCODE = 2
KOTLIN_SOURCE_EXTS = ('.kt', '.kts')


class RespFileArgumentParser(argparse.ArgumentParser):
    """ Extend basic ArgumentParser to parse arguments from response files. """
    def convert_arg_line_to_args(self, arg_line):
        return cmdline2list(arg_line)


def main(argv, stdio):
    """ High-level script to invoke Kotlin linter subprocess and CodeSonar analysis import commands. """
    # flags for import_sarif.py:
    INCLUDE_FLAG = '-include-sources'
    EXCLUDE_FLAG = '-exclude-sources'
    EXCLUDE_SIGIL = '!'
    ANALYZER_ARG_FLAG = '-X'
    linesep = '\n'
    stderr = stdio[2]
    cwd = os.getcwd()
    returncode = 0
    parser = RespFileArgumentParser(
        fromfile_prefix_chars='@',
        description='Scan Kotlin source files for CodeSonar analysis.',
        )
    parser.add_argument(
        'source_inputs',
        metavar='SOURCE',
        nargs='*',
        help='Base directory containing source files to analyze.')
    parser.add_argument(
        '-C', '-directory',
        metavar='DIR',
        dest='base_dir',
        help='Change to directory DIR before analyzing.')
    parser.add_argument(
        '-sarif-output',
        dest='sarif_output',
        help='Name of file to save analysis results in SARIF format.  Can be used for diagnostics.')
    parser.add_argument(
        '-include-sources',
        metavar='PATTERN',
        dest='source_args',
        action='append',
        type=(lambda s: (INCLUDE_FLAG, s)),
        help="File pattern for source files to include when importing.")
    parser.add_argument(
        '-exclude-sources',
        metavar='PATTERN',
        dest='source_args',
        action='append',
        type=(lambda s: (EXCLUDE_FLAG, s)),
        help="File pattern for source files to exclude when importing.")
    parser.add_argument(
        '-source-max-bytes',
        dest='source_max_size',
        type=int,
        help="The maximum size in bytes for any included source file.")
    parser.add_argument(
        '-detekt-cli',
        dest='detekt_cli_location',
        help='Path to alternate Kotlin detekt-cli home directory.')
    parser.add_argument(
        '-use-external-jdk',
        dest='use_external_jdk',
        action='store_true',
        help='Use JDK from environment instead of default JDK.')
    parser.add_argument(
        ANALYZER_ARG_FLAG,
        dest='analyzer_args',
        action='append',
        help='Arguments to pass to detekt-cli.  Arguments should be prefixed and separated by "+".  E.g. "+--parallel+-c+config1.yml+config2.yml"')
    arg_obj, unknown_args = parser.parse_known_args(argv[1:])

    for arg in unknown_args:
        if arg.startswith(ANALYZER_ARG_FLAG):
            arg_obj.analyzer_args.append(arg[len(ANALYZER_ARG_FLAG):])
        else:
            stderr.write(f'Error: unrecognized argument: {arg}\n')
            return 2
    analyzer_args = []
    for arg in arg_obj.analyzer_args or ():
        if not arg:
            continue
        sep = arg[0]
        arg = arg[len(sep):]
        args = arg.split(sep)
        analyzer_args.extend(args)

    source_args = arg_obj.source_args or []
    analyzer_inputs = arg_obj.source_inputs
    analyzer_source_args = ()
    source_max_size = arg_obj.source_max_size
    sarif_file = arg_obj.sarif_output
    base_dir = arg_obj.base_dir
    source_exts = KOTLIN_SOURCE_EXTS
    detekt_cli_location = arg_obj.detekt_cli_location

    if base_dir:
        base_dir = os.path.normpath(base_dir)
    if not base_dir or base_dir == os.path.curdir:
        base_dir = None
    # If there are any analyzer_inputs, then pass them to detekt -i,
    #  otherwise, for consistency with cs-java-scan,
    #  use -include-sources to decide what to pass to detekt -i.
    if not source_args and not analyzer_inputs:
        parser.print_usage(stderr)
        stderr.write("Error: An analysis target is required.  Please provide a SOURCE argument or use -include-sources to specify a pattern.\n")
        return 2
    if not analyzer_inputs:
        # If no input items, use -include-sources, etc.
        #  to decide what to pass to the analyzer.
        source_patterns = [
            pat if flag == INCLUDE_FLAG else EXCLUDE_SIGIL + pat
            for (flag, pat) in source_args]
        analyzer_inputs = glob(source_patterns or (), filesonly=True, cwd=base_dir)
    else:
        if source_args:
            # Both inputs and include/exclude patterns:
            #   Pass include patterns to detekt as --includes,--excludes filter options.
            analyzer_source_args = source_args
        # If there are analyzer_inputs then use inputs to construct
        #  initial include patterns for import_sarif.py.
        source_args2 = []
        for path in analyzer_inputs:
            # source_args must be relative to cwd (i.e. joined with base_dir)
            path2 = os.path.join(base_dir or '', path)
            if os.path.isdir(path2):
                # Use normpath to remove initial './' if it exists.
                #  This can help to ensure compatibility with exclusion patterns.
                source_args2.extend((
                    (
                        INCLUDE_FLAG,
                        os.path.normpath(os.path.join(
                            globescape(path2), '**', '*' + ext)),
                    )
                    for ext in source_exts))
            else:
                source_args2.append(
                    (
                        INCLUDE_FLAG,
                        globescape(path2),
                    ))
        for flag, source_pattern in source_args:
            if base_dir and base_dir != os.path.curdir:
                source_pattern = os.path.join(base_dir, source_pattern)
            source_args2.append((flag, source_pattern))
        source_args = source_args2

    csonar_dir = gthome()
    if not detekt_cli_location:
        detekt_cli_location = os.path.join(csonar_dir, 'win32-fetcher', 'detekt-cli')
    else:
        # Ensure detekt location is an absolute path
        #  so we don't need to make it relative to base_dir
        detekt_cli_location = os.path.join(cwd, detekt_cli_location)
    # Detekt has a shell script front-end which is implemented as a .bat file on Windows.
    #  We call the JAR directly here instead of using the shell script
    #  since using cmd (.bat) can easily lead to a "command line is too long" error.
    detekt_cli_jar = None
    if detekt_cli_location.lower().endswith('.jar') and os.path.isfile(detekt_cli_location):
        detekt_cli_jar = detekt_cli_location
    elif os.path.isdir(detekt_cli_location):
        detekt_cli_home = detekt_cli_location
        detekt_cli_jars = glob(os.path.join(detekt_cli_home, 'lib', 'detekt-cli-*.jar'))
        if detekt_cli_jars:
            # This sort isn't really trying to choose the latest version,
            #  rather it is trying to choose a consistent version:
            detekt_cli_jars.sort(reverse=True)
            detekt_cli_jar = detekt_cli_jars[0]
    if not detekt_cli_jar:
        stderr.write("Error: Could not find detekt-cli JAR file\n")
        return 1
    java_home = None
    jdk_platform_name = None
    if arg_obj.use_external_jdk:
        pass
    elif sys.platform == 'linux':
        jdk_platform_name = 'linux-gnu'
    elif sys.platform == 'win32':
        jdk_platform_name = 'cygwin'
    #else: default to environment for jdk
    if jdk_platform_name:
        jdk_arch = platform.machine()
        jdk_arch = 'x86_64' if jdk_arch == 'AMD64' else jdk_arch
        jdk_name = f'jdk-21-{jdk_arch}-pc-{jdk_platform_name}'
        java_home = os.path.join(csonar_dir, 'win32-fetcher', jdk_name)
        if not os.path.isdir(java_home):
            # CodeSonar does not ship a JDK for 32-bit Windows, for example.
            java_home = None
    if java_home is None:
        java_home = os.environ.get('JAVA_HOME')
    java_cmd = 'java'
    if java_home is not None:
        java_cmd = os.path.join(java_home, 'bin', 'java')

    temp_sarif_file = None
    if not sarif_file:
        fd, temp_sarif_file = mkstemp(prefix='codesonar.kotlin_scan.', suffix='.sarif', dir='.')
        os.close(fd)
        sarif_file = temp_sarif_file
    a_sep = ','  # comma separator for detekt
    detekt_java_argv = [
        java_cmd,
        '-jar', detekt_cli_jar,
        '-r', f'sarif:{sarif_file}',
        '-i', a_sep.join(analyzer_inputs),
        ]
    detekt_includes = []
    detekt_excludes = []
    for flag, pattern in analyzer_source_args:
        if flag == INCLUDE_FLAG:
            detekt_includes.append(pattern)
        elif flag == EXCLUDE_FLAG:
            detekt_excludes.append(pattern)
    # Note that since detekt only accepts one --excludes option,
    #  the filtering done by detekt may differ from that done by import_sarif
    #  if the filtering uses an alternating -include-sources and -exclude-sources combination.
    #  This could lead to a difference between the files analyzed by detekt
    #   and the files added to the CodeSonar analysis.
    if detekt_includes:
        detekt_java_argv.append('--includes')
        detekt_java_argv.append(a_sep.join(detekt_includes))
    if detekt_excludes:
        detekt_java_argv.append('--excludes')
        detekt_java_argv.append(a_sep.join(detekt_excludes))
    detekt_java_argv.extend(analyzer_args)
    sarif_argv = ['import_sarif.py', sarif_file]
    if source_max_size:
        sarif_argv.extend(('-source-max-bytes', str(source_max_size)))
    for flag, pattern in source_args:
        sarif_argv.append(flag)
        sarif_argv.append(pattern)

    try:
        stderr.write(list2cmdline(detekt_java_argv))
        stderr.write('\n')
        stderr.flush()
        detekt = Popen(detekt_java_argv, cwd=base_dir or None)
        detekt_returncode = detekt.wait()
        # detekt will return 2 if there are warnings,
        #  but we don't want to report it
        if detekt_returncode and detekt_returncode != DETEKT_WARNINGS_RETURNCODE:
            returncode = detekt_returncode
        if not returncode:
            stderr.write(list2cmdline(sarif_argv))
            stderr.write(linesep)
            stderr.flush()
            returncode = import_sarif.main(sarif_argv)
    finally:
        if temp_sarif_file:
            os.remove(temp_sarif_file)
    return returncode


if __name__ == '__main__':
    sys.exit(main(sys.argv, (sys.stdin, sys.stdout, sys.stderr)))
