# Copyright (C) 2016 Adrien Vergé
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import locale
import os
import platform
import sys

from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION, linter
from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.linter import PROBLEM_LEVELS


def find_files_recursively(items, conf):
    for item in items:
        if os.path.isdir(item):
            for root, _dirnames, filenames in os.walk(item):
                for f in filenames:
                    filepath = os.path.join(root, f)
                    if (conf.is_yaml_file(filepath) and
                            not conf.is_file_ignored(filepath)):
                        yield filepath
        else:
            yield item


def supports_color():
    supported_platform = not (platform.system() == 'Windows' and not
                              ('ANSICON' in os.environ or
                               ('TERM' in os.environ and
                                os.environ['TERM'] == 'ANSI')))
    return (supported_platform and
            hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())


class Format:
    @staticmethod
    def parsable(problem, filename):
        return (f'{filename}:{problem.line}:{problem.column}: '
                f'[{problem.level}] {problem.message}')

    @staticmethod
    def standard(problem, filename):
        line = f'  {problem.line}:{problem.column}'
        line += max(12 - len(line), 0) * ' '
        line += problem.level
        line += max(21 - len(line), 0) * ' '
        line += problem.desc
        if problem.rule:
            line += f'  ({problem.rule})'
        return line

    @staticmethod
    def standard_color(problem, filename):
        line = f'  \033[2m{problem.line}:{problem.column}\033[0m'
        line += max(20 - len(line), 0) * ' '
        if problem.level == 'warning':
            line += f'\033[33m{problem.level}\033[0m'
        else:
            line += f'\033[31m{problem.level}\033[0m'
        line += max(38 - len(line), 0) * ' '
        line += problem.desc
        if problem.rule:
            line += f'  \033[2m({problem.rule})\033[0m'
        return line

    @staticmethod
    def github(problem, filename):
        line = f'::{problem.level} file={filename},' \
               f'line={problem.line},col={problem.column}' \
               f'::{problem.line}:{problem.column} '
        if problem.rule:
            line += f'[{problem.rule}] '
        line += problem.desc
        return line


def show_problems(problems, file, args_format, no_warn):
    max_level = 0
    first = True

    if args_format == 'auto':
        if ('GITHUB_ACTIONS' in os.environ and
                'GITHUB_WORKFLOW' in os.environ):
            args_format = 'github'
        elif supports_color():
            args_format = 'colored'

    for problem in problems:
        max_level = max(max_level, PROBLEM_LEVELS[problem.level])
        if no_warn and (problem.level != 'error'):
            continue
        if args_format == 'parsable':
            print(Format.parsable(problem, file))
        elif args_format == 'github':
            if first:
                print(f'::group::{file}')
                first = False
            print(Format.github(problem, file))
        elif args_format == 'colored':
            if first:
                print(f'\033[4m{file}\033[0m')
                first = False
            print(Format.standard_color(problem, file))
        else:
            if first:
                print(file)
                first = False
            print(Format.standard(problem, file))

    if not first and args_format == 'github':
        print('::endgroup::')

    if not first and args_format != 'parsable':
        print('')

    return max_level


def find_project_config_filepath(path='.'):
    for filename in ('.yamllint', '.yamllint.yaml', '.yamllint.yml'):
        filepath = os.path.join(path, filename)
        if os.path.isfile(filepath):
            return filepath

    if os.path.abspath(path) == os.path.abspath(os.path.expanduser('~')):
        return None
    if os.path.abspath(path) == os.path.abspath(os.path.join(path, '..')):
        return None
    return find_project_config_filepath(path=os.path.join(path, '..'))


def run(argv=None):
    parser = argparse.ArgumentParser(prog=APP_NAME,
                                     description=APP_DESCRIPTION)
    files_group = parser.add_mutually_exclusive_group(required=True)
    files_group.add_argument('files', metavar='FILE_OR_DIR', nargs='*',
                             default=(),
                             help='files to check')
    files_group.add_argument('-', action='store_true', dest='stdin',
                             help='read from standard input')
    config_group = parser.add_mutually_exclusive_group()
    config_group.add_argument('-c', '--config-file', dest='config_file',
                              action='store',
                              help='path to a custom configuration')
    config_group.add_argument('-d', '--config-data', dest='config_data',
                              action='store',
                              help='custom configuration (as YAML source)')
    parser.add_argument('--list-files', action='store_true', dest='list_files',
                        help='list files to lint and exit')
    parser.add_argument('-f', '--format',
                        choices=('parsable', 'standard', 'colored', 'github',
                                 'auto'),
                        default='auto', help='format for parsing output')
    parser.add_argument('-s', '--strict',
                        action='store_true',
                        help='return non-zero exit code on warnings '
                             'as well as errors')
    parser.add_argument('--no-warnings',
                        action='store_true',
                        help='output only error level problems')
    parser.add_argument('-v', '--version', action='version',
                        version=f'{APP_NAME} {APP_VERSION}')

    args = parser.parse_args(argv)

    if 'YAMLLINT_CONFIG_FILE' in os.environ:
        user_global_config = os.path.expanduser(
            os.environ['YAMLLINT_CONFIG_FILE'])
    # User-global config is supposed to be in ~/.config/yamllint/config
    elif 'XDG_CONFIG_HOME' in os.environ:
        user_global_config = os.path.join(
            os.environ['XDG_CONFIG_HOME'], 'yamllint', 'config')
    else:
        user_global_config = os.path.expanduser('~/.config/yamllint/config')

    project_config_filepath = find_project_config_filepath()
    try:
        if args.config_data is not None:
            if args.config_data != '' and ':' not in args.config_data:
                args.config_data = f'extends: {args.config_data}'
            conf = YamlLintConfig(content=args.config_data)
        elif args.config_file is not None:
            conf = YamlLintConfig(file=args.config_file)
        elif project_config_filepath:
            conf = YamlLintConfig(file=project_config_filepath)
        elif os.path.isfile(user_global_config):
            conf = YamlLintConfig(file=user_global_config)
        else:
            conf = YamlLintConfig('extends: default')
    except YamlLintConfigError as e:
        print(e, file=sys.stderr)
        sys.exit(-1)

    if conf.locale is not None:
        locale.setlocale(locale.LC_ALL, conf.locale)

    if args.list_files:
        for file in find_files_recursively(args.files, conf):
            if not conf.is_file_ignored(file):
                print(file)
        sys.exit(0)

    max_level = 0

    for file in find_files_recursively(args.files, conf):
        filepath = file.removeprefix('./')
        try:
            with open(file, mode='rb') as f:
                problems = linter.run(f, conf, filepath)
        except OSError as e:
            print(e, file=sys.stderr)
            sys.exit(-1)
        prob_level = show_problems(problems, file, args_format=args.format,
                                   no_warn=args.no_warnings)
        max_level = max(max_level, prob_level)

    # read yaml from stdin
    if args.stdin:
        try:
            # The .buffer part makes sure that we get the raw bytes. We need to
            # get the raw bytes so that we can autodetect the character
            # encoding.
            problems = linter.run(sys.stdin.buffer, conf, '')
        except OSError as e:
            print(e, file=sys.stderr)
            sys.exit(-1)
        prob_level = show_problems(problems, 'stdin', args_format=args.format,
                                   no_warn=args.no_warnings)
        max_level = max(max_level, prob_level)

    if max_level == PROBLEM_LEVELS['error']:
        return_code = 1
    elif max_level == PROBLEM_LEVELS['warning']:
        return_code = 2 if args.strict else 0
    else:
        return_code = 0

    sys.exit(return_code)
