# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2016 Continuum Analytics, Inc.
#
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
# -----------------------------------------------------------------------------
"""CLI Parser for `ciocheck`."""

# Standard library imports
from collections import OrderedDict
import argparse
import os
import shutil
import sys

# Local imports
from ciocheck.config import ALL_FILES, load_config
from ciocheck.files import FileManager
from ciocheck.formatters import FORMATTERS, MULTI_FORMATTERS, MultiFormatter
from ciocheck.linters import LINTERS
from ciocheck.tools import TOOLS


class Runner(object):
    """Main tool runner."""

    def __init__(self, cmd_root, cli_args, folders=None, files=None):
        """Main tool runner."""
        # Run options
        self.cmd_root = cmd_root  # Folder on which the command was executed
        self.config = load_config(cmd_root, cli_args)
        self.file_manager = FileManager(folders=folders, files=files)
        self.folders = folders
        self.files = files
        self.all_results = OrderedDict()
        self.all_tools = {}
        self.test_results = None
        self.failed_checks = set()

        self.check = self.config.get_value('check')
        self.enforce = self.config.get_value('enforce')
        self.diff_mode = self.config.get_value('diff_mode')
        self.file_mode = self.config.get_value('file_mode')
        self.branch = self.config.get_value('branch')
        self.disable_formatters = cli_args.disable_formatters
        self.disable_linters = cli_args.disable_linters
        self.disable_tests = cli_args.disable_tests

    def run(self):
        """Run tools."""
        msg = 'Running ciocheck'
        print('')
        print('=' * len(msg))
        print(msg)
        print('=' * len(msg))
        print('')
        self.clean()

        check_linters = [l for l in LINTERS if l.name in self.check]
        check_formatters = [f for f in FORMATTERS if f.name in self.check]
        check_testers = [t for t in TOOLS if t.name in self.check]
        run_multi = any(f for f in MULTI_FORMATTERS if f.name in self.check)

        # Format before lint, linters may complain about bad formatting

        # Formatters
        if not self.disable_formatters:
            for formatter in check_formatters:
                print('Running "{}" ...'.format(formatter.name))
                tool = formatter(self.cmd_root)
                files = self.file_manager.get_files(
                    branch=self.branch,
                    diff_mode=self.diff_mode,
                    file_mode=self.file_mode,
                    extensions=tool.extensions)
                tool.create_config(self.config)
                self.all_tools[tool.name] = tool
                results = tool.run(files)
                # Pyformat might include files in results that are not in files
                # like when an init is created
                if results:
                    self.all_results[tool.name] = {
                        'files': files,
                        'results': results,
                    }

            # The result of the the multi formatter is special!
            if run_multi:
                print('Running "Multi formatter"')
                tool = MultiFormatter(self.cmd_root, self.check)
                files = self.file_manager.get_files(
                    branch=self.branch,
                    diff_mode=self.diff_mode,
                    file_mode=self.file_mode,
                    extensions=tool.extensions)
                multi_results = tool.run(files)
                for key, values in multi_results.items():
                    self.all_results[key] = {
                        'files': files,
                        'results': values,
                    }

        # Linters
        if not self.disable_linters:
            for linter in check_linters:
                print('Running "{}" ...'.format(linter.name))
                tool = linter(self.cmd_root)
                files = self.file_manager.get_files(
                    branch=self.branch,
                    diff_mode=self.diff_mode,
                    file_mode=self.file_mode,
                    extensions=tool.extensions)
                self.all_tools[tool.name] = tool
                tool.create_config(self.config)
                self.all_results[tool.name] = {
                    'files': files,
                    'results': tool.run(files),
                }

        # Tests
        if not self.disable_tests:
            for tester in check_testers:
                print('Running "{}" ...'.format(tester.name))
                tool = tester(self.cmd_root)
                tool.create_config(self.config)
                self.all_tools[tool.name] = tool

                if tool.name == 'pytest':
                    tool.setup_pytest_coverage_args(self.folders)

                files = self.file_manager.get_files(
                    branch=self.branch,
                    diff_mode=self.diff_mode,
                    file_mode=ALL_FILES,
                    extensions=tool.extensions)
                results = tool.run(files)
                if results:
                    results['files'] = files
                    self.test_results = results

        for tool in LINTERS + FORMATTERS + TOOLS:
            tool.remove_config(self.cmd_root)
        self.clean()

        self.process_results(self.all_results)
        if self.enforce_checks():
            msg = 'Ciocheck successful run'
            print('\n\n' + '=' * len(msg))
            print(msg)
            print('=' * len(msg))
            print('')

    def process_results(self, all_results):
        """Group all results by file path."""
        all_changed_paths = []
        for tool_name, data in all_results.items():
            if data:
                files, results = data['files'], data['results']
                all_changed_paths += [result['path'] for result in results]

        all_changed_paths = list(sorted(set(all_changed_paths)))

        if self.test_results:
            test_files = self.test_results.get('files')
            test_coverage = self.test_results.get('coverage')
        else:
            test_files = []
            test_coverage = []

        for path in all_changed_paths:
            short_path = path.replace(self.cmd_root, '...')
            print('')
            print(short_path)
            print('-' * len(short_path))
            for tool_name, data in all_results.items():
                if data:
                    files, results = data['files'], data['results']
                    lines = [[-1], range(100000)]

                    if isinstance(files, dict):
                        added_lines = files.get(path, lines)[-1]
                    else:
                        added_lines = lines[-1]

                    messages = []
                    for result in results:
                        res_path = result['path']
                        if path == res_path:
                            # LINTERS
                            line = int(result.get('line', -1))
                            created = result.get('created')
                            added_copy = result.get('added-copy')
                            added_header = result.get('added-header')
                            diff = result.get('diff')
                            if line and line in list(added_lines):
                                spaces = (8 - len(str(line))) * ' '
                                args = result.copy()
                                args['spaces'] = spaces
                                msg = ('    {line}:{spaces}'
                                       '{type}: {message}').format(**args)
                                messages.append(msg)

                            # Formatters
                            if created:
                                msg = '    __init__ file created.'
                                messages.append(msg)
                            if added_copy:
                                msg = '    added copyright.'
                                messages.append(msg)
                            if added_header:
                                msg = '    added header.'
                                messages.append(msg)
                            if diff:
                                msg = self.format_diff(diff)
                                messages.append(msg)

                            # TESTERS / COVERAGE

                    test = [r['path'] for r in results if path == r['path']]
                    if test and messages:
                        print('\n  ' + tool_name)
                        print('  ' + '-' * len(tool_name))
                        self.failed_checks.add(tool_name)
                        for message in messages:
                            print(message)

            if isinstance(test_files, dict) and test_files:
                # Asked for lines changed
                if test_coverage:
                    lines_changed_not_covered = []
                    lines = test_files.get(path)
                    lines_added = lines[-1] if lines else []
                    lines_covered = test_coverage.get(path)
                    for line in lines_added:
                        if line not in lines_covered:
                            lines_changed_not_covered.append(str(line))

                    if lines_changed_not_covered:
                        uncov_perc = ((1.0 * len(lines_changed_not_covered)) /
                                      (1.0 * len(lines_added)))
                        cov_perc = (1 - uncov_perc) * 100
                        tool_name = 'coverage'
                        print('\n  ' + tool_name)
                        print('  ' + '-' * len(tool_name))
                        print('    The following lines changed and are not '
                              'covered by tests ({0}%):'.format(cov_perc))
                        print('    ' + ', '.join(lines_changed_not_covered))

        print('')
        pytest_tool = self.all_tools.get('pytest')
        if pytest_tool:
            if pytest_tool.coverage_fail:
                self.failed_checks.add('coverage')

    def enforce_checks(self):
        """Check that enforced checks did not generate reports."""
        if self.test_results:
            if 'pytest' in self.test_results:
                test_summary = self.test_results['pytest']['report']['summary']
                if test_summary.get('failed'):
                    self.failed_checks.add('pytest')
            else:
                self.failed_checks.add('pytest')

        for enforce_tool in self.enforce:
            if enforce_tool in self.failed_checks:
                msg = "Ciocheck failures in: {0}".format(
                    repr(self.failed_checks))
                print('\n\n' + '=' * len(msg))
                print(msg)
                print('=' * len(msg))
                print('')
                sys.exit(1)
                break

        return True

    def format_diff(self, diff, indent='    '):
        """Format diff to include an indentation for console printing."""
        lines = diff.split('\n')
        new_lines = []
        for line in lines:
            new_lines.append(indent + line)
        return '\n'.join(new_lines)

    def clean(self):
        """Remove build directories and temporal config files."""
        # Clean up leftover trash as best we can
        build_tmp = os.path.join(self.cmd_root, 'build', 'tmp')
        if os.path.isdir(build_tmp):
            try:
                shutil.rmtree(build_tmp, ignore_errors=True)
            except Exception:
                pass


def main():
    """CLI `Parser for ciocheck`."""
    description = 'Run Continuum IO test suite.'
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument(
        'folders', help='Folders to analyze. Use from repo root.', nargs='+')
    parser.add_argument(
        '--disable-formatters',
        '-df',
        action='store_true',
        default=False,
        help=('Skip all configured formatters'))
    parser.add_argument(
        '--disable-linters',
        '-dl',
        action='store_true',
        default=False,
        help=('Skip all configured linters'))
    parser.add_argument(
        '--disable-tests',
        '-dt',
        action='store_true',
        default=False,
        help=('Skip running tests'))
    parser.add_argument(
        '--file-mode',
        '-fm',
        dest='file_mode',
        choices=['lines', 'files', 'all'],
        default=None,
        help=('Define if the tool should run on modified '
              'lines of files (default), modified files or '
              'all files'))
    parser.add_argument(
        '--diff-mode',
        '-dm',
        dest='diff_mode',
        choices=['commited', 'staged', 'unstaged'],
        default=None,
        help='Define diff mode. Default mode is commited.')
    parser.add_argument(
        '--branch',
        '-b',
        dest='branch',
        default=None,
        help=('Define branch to compare to. Default branch is '
              '"origin/master"'))
    parser.add_argument(
        '--check',
        '-c',
        dest='check',
        nargs='+',
        choices=[
            'pep8', 'pydocstyle', 'flake8', 'pylint', 'pyformat', 'isort',
            'yapf', 'autopep8', 'coverage', 'pytest'
        ],
        default=None,
        help='Select tools to run. Default is "pep8"')
    parser.add_argument(
        '--enforce',
        '-e',
        dest='enforce',
        choices=[
            'pep8', 'pydocstyle', 'flake8', 'pylint', 'pyformat', 'isort',
            'yapf', 'autopep8', 'coverage', 'pytest'
        ],
        default=None,
        nargs='+',
        help=('Select tools to enforce. Enforced tools will '
              'fail if a result is obtained. Default is '
              'none.'))
    parser.add_argument(
        '--config',
        '-cf',
        dest='config_file',
        default=None,
        help=('Select a config file to use. Default is none.'))

    cli_args = parser.parse_args()
    root = os.getcwd()
    folders = []
    files = []
    for folder_or_file in cli_args.folders:
        folder_or_file = os.path.abspath(folder_or_file)
        if os.path.isfile(folder_or_file):
            files.append(folder_or_file)
        elif os.path.isdir(folder_or_file):
            folders.append(folder_or_file)

    if folders or files:
        test = Runner(root, cli_args, folders=folders, files=files)
        test.run()
    elif not folders and not files:
        print('Invalid folders or files!')


if __name__ == '__main__':
    main()