# -----------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -----------------------------------------------------------------------------

import shlex

import re
from unittest import mock

from knack.log import get_logger

from ..rule_decorators import HelpFileEntryRule
from ..linter import RuleError, LinterSeverity
from ..util import LinterError

# pylint: disable=anomalous-backslash-in-string
# pylint: disable=no-value-for-parameter


# 'az' space then repeating runs of quoted tokens or non quoted characters
_az_pattern = r'az\s*' + '(([^\"\'])*|' + r'((\"[^\"]*\"\s*)|(\'[^\']*\'\s*))' + ')'
# match the two types of command substitutions
_CMD_SUB_1 = re.compile(r"\$\(\s*" + "(" + _az_pattern + ")" + r"\)")
_CMD_SUB_2 = re.compile(r"`\s*" + "(" + _az_pattern + ")" + "`")

logger = get_logger(__name__)


@HelpFileEntryRule(LinterSeverity.HIGH)
def unrecognized_help_entry_rule(linter, help_entry):
    if help_entry not in linter.commands and help_entry not in linter.command_groups:
        raise RuleError('Not a recognized command or command-group')


@HelpFileEntryRule(LinterSeverity.HIGH)
def faulty_help_type_rule(linter, help_entry):
    if linter.get_help_entry_type(help_entry) != 'group' and help_entry in linter.command_groups:
        raise RuleError('Command-group should be of help-type `group`')
    if linter.get_help_entry_type(help_entry) != 'command' and help_entry in linter.commands:
        raise RuleError('Command should be of help-type `command`')


@HelpFileEntryRule(LinterSeverity.HIGH)
def unrecognized_help_parameter_rule(linter, help_entry):
    if help_entry not in linter.commands:
        return

    param_help_names = linter.get_help_entry_parameter_names(help_entry)
    violations = []
    for param_help_name in param_help_names:
        if not linter.is_valid_parameter_help_name(help_entry, param_help_name):
            violations.append(param_help_name)
    if violations:
        raise RuleError('The following parameter help names are invalid: {}'.format(' | '.join(violations)))


@HelpFileEntryRule(LinterSeverity.HIGH)
def faulty_help_example_rule(linter, help_entry):
    violations = []
    for index, example in enumerate(linter.get_help_entry_examples(help_entry)):
        if 'az ' + help_entry not in example.get('text', ''):
            violations.append(str(index))

    if violations:
        raise RuleError('The following example entry indices do not include the command: {}'.format(
            ' | '.join(violations)))


@HelpFileEntryRule(LinterSeverity.HIGH)
def faulty_help_example_parameters_rule(linter, help_entry):
    # print(linter, help_entry)
    parser = linter.command_parser
    violations = []

    for example in linter.get_help_entry_examples(help_entry):
        supported_profiles = example.get('supported-profiles')
        if supported_profiles and 'latest' not in supported_profiles:
            logger.warning("\n\tSKIPPING example: %s\n\tas 'latest' is not in its supported profiles."
                           "\n\t\tsupported-profiles: %s.", example['text'], example['supported-profiles'])
            continue

        unsupported_profiles = example.get('unsupported-profiles')
        if unsupported_profiles and 'latest' in unsupported_profiles:
            logger.warning("\n\tSKIPPING example: %s\n\tas 'latest' is in its unsupported profiles."
                           "\n\t\tunsupported-profiles: %s.", example['text'], example['unsupported-profiles'])
            continue

        example_text = example.get('text', '')
        commands = _extract_commands_from_example(example_text)
        while commands:
            command = commands.pop()
            violation, nested_commands = _lint_example_command(command, parser)

            commands.extend(nested_commands)  # append commands that are the source of any arguments
            if violation:
                violations.append(violation)

    if violations:
        num_err = len(violations)
        violation_str = "\n\n".join(violations)
        violation_msg = "\n\tThere is a violation:\n{}.".format(violation_str) if num_err == 1 else \
            "\n\tThere are {} violations:\n{}".format(num_err, violation_str)
        raise RuleError(violation_msg + "\n\n")


# Faulty help example parameters rule helpers

@mock.patch("azure.cli.core.parser.AzCliCommandParser._check_value")
@mock.patch("argparse.ArgumentParser._get_value")
@mock.patch("azure.cli.core.parser.AzCliCommandParser.error")
def _lint_example_command(command, parser, mocked_error_method, mocked_get_value, mocked_check_value):  # pylint: disable=unused-argument
    def get_value_side_effect(action, arg_string):  # pylint: disable=unused-argument
        return arg_string
    mocked_error_method.side_effect = LinterError  # mock call of parser.error so usage won't be printed.
    mocked_get_value.side_effect = get_value_side_effect

    violation = None
    nested_commands = []

    try:
        command_args = shlex.split(command, comments=True)[1:]  # split commands into command args, ignore comments.
        command_args, nested_commands = _process_command_args(command_args)
        parser.parse_args(command_args)
    except ValueError as e:  # handle exception thrown by shlex.
        if str(e) == "No closing quotation":
            violation = '\t"{}"\n\thas no closing quotation. Tip: to continue an example ' \
                        'command on the next line, use a "\\" followed by a newline.\n\t' \
                        'If needed, you can escape the "\\", like so "\\\\"'.format(command)
        else:
            raise e
    except LinterError:  # handle parsing failure due to invalid option
        violation = '\t"{}" is not a valid command'.format(command)
        if mocked_error_method.called:
            call_args = mocked_error_method.call_args
            violation = "{}.\n\t{}".format(violation, call_args[0][0])

    return violation, nested_commands


# return list of commands in the example text
def _extract_commands_from_example(example_text):

    # fold commands spanning multiple lines into one line. Split commands that use pipes
    # handle single and double quotes properly
    lines = example_text.splitlines()
    example_text = ""
    quote = None
    for line in lines:
        for ch in line:
            if quote is None:
                if ch in ('"', "'"):
                    quote = ch
            elif ch == quote:
                quote = None
        if quote is None and line.endswith("\\"):
            # attach this line with removed '\' and no '\n' (space at the end to keep consistent with initial algorithm)
            example_text += line[0:-1] + " "
        elif quote is not None:
            # attach this line without '\n'
            example_text += line
        else:
            # attach this line with '\n' as no quote and no continuation
            example_text += line + "\n"
    # this is also for consistency with original algorithm
    example_text = example_text.replace("\\ ", " ")

    commands = example_text.splitlines()
    processed_commands = []
    for command in commands:  # filter out commands
        command.strip()
        if command.startswith("az"):  # if this is a single az command add it.
            processed_commands.append(command)

        for re_prog in [_CMD_SUB_1, _CMD_SUB_2]:
            start = 0
            match = re_prog.search(command, start)
            while match:  # while there is a nested az command of type 1 $( az ...)
                processed_commands.append(match.group(1).strip())  # add it
                start = match.end(1)  # get index of rest of string
                match = re_prog.search(command, start)  # attempt to get next match

    return processed_commands


def _process_command_args(command_args):
    result_args = []
    new_commands = []
    operators = ["&&", "||", "|"]

    for arg in command_args:  # strip unnecessary punctuation, otherwise arg validation could fail.
        if arg in operators:  # handle cases where multiple commands are connected by control operators or pipe.
            idx = command_args.index(arg)
            maybe_new_command = " ".join(command_args[idx:])

            idx = maybe_new_command.find("az ")
            if idx != -1:
                new_commands.append(maybe_new_command[idx:])  # remaining command is in fact a new command / commands.
            break

        result_args.append(arg)

    return result_args, new_commands
