# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import copy
import argparse
from os.path import expanduser
from unittest.mock import patch

from docutils import nodes
from docutils.statemachine import ViewList
from docutils.parsers.rst import directives

from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.util.compat import Directive
from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util.docfields import Field

from azure.cli.core import MainCommandsLoader, AzCli  # pylint: disable=import-error
from azure.cli.core.commands import AzCliCommandInvoker  # pylint: disable=import-error
from azure.cli.core.parser import AzCliCommandParser  # pylint: disable=import-error
from azure.cli.core._help import AzCliHelp, CliCommandHelpFile, ArgumentGroupRegistry  # pylint: disable=import-error

_USER_HOME = expanduser('~')

_CLI_FIELD_TYPES = [
    Field('summary', label='Summary', has_arg=False,
          names=('summary', 'shortdesc')),
    Field('description', label='Description', has_arg=False,
          names=('description', 'desc', 'longdesc'))
]


class CliBaseDirective(ObjectDescription):
    def handle_signature(self, sig, signode):
        signode += addnodes.desc_addname(sig, sig)
        return sig

    def needs_arglist(self):
        return False

    def add_target_and_index(self, name, sig, signode):
        signode['ids'].append(name)

    def get_index_text(self, modname, name):  # pylint: disable=unused-argument
        return name


class CliGroupDirective(CliBaseDirective):
    doc_field_types = copy.copy(_CLI_FIELD_TYPES)
    doc_field_types.extend([
        Field('docsource', label='Doc Source', has_arg=False,
              names=('docsource', 'documentsource')),
        Field('deprecated', label='Deprecated', has_arg=False,
              names=('deprecated',))
    ])


class CliCommandDirective(CliBaseDirective):
    doc_field_types = copy.copy(_CLI_FIELD_TYPES)
    doc_field_types.extend([
        Field('docsource', label='Doc Source', has_arg=False,
              names=('docsource', 'documentsource')),
        Field('deprecated', label='Deprecated', has_arg=False,
              names=('deprecated',))
    ])


class CliArgumentDirective(CliBaseDirective):
    doc_field_types = copy.copy(_CLI_FIELD_TYPES)
    doc_field_types.extend([
        Field('required', label='Required', has_arg=False,
              names=('required',)),
        Field('values', label='Allowed values', has_arg=False,
              names=('values', 'choices', 'options')),
        Field('default', label='Default value', has_arg=False,
              names=('default',)),
        Field('source', label='Values from', has_arg=False,
              names=('source', 'sources')),
        Field('deprecated', label='Deprecated', has_arg=False,
              names=('deprecated',))
    ])


class CliExampleDirective(CliBaseDirective):
    pass


class AbstractHelpGenDirective(Directive):
    """ Generic Sphinx Directive for generating azure cli documentation.
        Should be overridden for core CLI or CLI extensions documentation
    """

    _INDENT = ' ' * 3
    _DOUBLE_INDENT = _INDENT * 2

    def make_rst(self):  # pylint: disable=too-many-statements, too-many-nested-blocks

        az_cli = AzCli(cli_name='az',
                       commands_loader_cls=MainCommandsLoader,
                       invocation_cls=AzCliCommandInvoker,
                       parser_cls=AzCliCommandParser,
                       help_cls=AzCliHelp)

        with patch('getpass.getuser', return_value='your_system_user_login_name'):
            help_files = self._get_help_files(az_cli)

        doc_source_map = self._load_doc_source_map()
        group_registry = None

        for help_file in help_files:  # pylint: disable=too-many-nested-blocks
            is_command = isinstance(help_file, CliCommandHelpFile)

            # it is top level group az if command is empty
            yield '.. cli{}:: {}'.format('command' if is_command else 'group',
                                         help_file.command if help_file.command else 'az')
            yield ''
            yield '{}:summary: {}'.format(self._INDENT, help_file.short_summary)
            yield '{}:description: {}'.format(self._INDENT, help_file.long_summary)
            if help_file.deprecate_info:
                yield '{}:deprecated: {}'.format(self._INDENT,
                                                 help_file.deprecate_info._get_message(help_file.deprecate_info))  # pylint: disable=protected-access
            doc_source_content = self._get_doc_source_content(doc_source_map, help_file)
            if doc_source_content:
                yield doc_source_content
            yield ''

            if is_command and help_file.parameters:
                group_registry = ArgumentGroupRegistry(
                    [p.group_name for p in help_file.parameters if p.group_name])

                for arg in sorted(help_file.parameters, key=lambda p: group_registry.get_group_priority(p.group_name) + str(not p.required) + p.name):  # pylint: disable=line-too-long, cell-var-from-loop
                    yield '{}.. cliarg:: {}'.format(self._INDENT, arg.name)
                    yield ''
                    yield '{}:required: {}'.format(self._DOUBLE_INDENT, arg.required)
                    if arg.deprecate_info:
                        yield '{}:deprecated: {}'.format(self._DOUBLE_INDENT,
                                                         arg.deprecate_info._get_message(  # pylint: disable=protected-access
                                                             arg.deprecate_info))
                    short_summary = arg.short_summary or ''
                    possible_values_index = short_summary.find(' Possible values include')
                    short_summary_end_idx = possible_values_index if possible_values_index >= 0 else len(short_summary)
                    short_summary = short_summary[0:short_summary_end_idx]
                    short_summary = short_summary.strip()
                    yield '{}:summary: {}'.format(self._DOUBLE_INDENT, short_summary)
                    yield '{}:description: {}'.format(self._DOUBLE_INDENT, arg.long_summary)
                    if arg.choices:
                        yield '{}:values: {}'.format(self._DOUBLE_INDENT,
                                                     ', '.join(sorted([str(x) for x in arg.choices])))
                    if arg.default and arg.default != argparse.SUPPRESS:
                        try:
                            if arg.default.startswith(_USER_HOME):
                                arg.default = arg.default.replace(_USER_HOME, '~').replace('\\', '/')
                        except Exception:  # pylint: disable=broad-except
                            pass
                        try:
                            arg.default = arg.default.replace("\\", "\\\\")
                        except Exception:  # pylint: disable=broad-except
                            pass
                        yield '{}:default: {}'.format(self._DOUBLE_INDENT, arg.default)
                    if arg.value_sources:
                        yield '{}:source: {}'.format(self._DOUBLE_INDENT, ', '.join(self._get_param_value_sources(arg)))
                    yield ''
            yield ''
            if help_file.examples:
                for e in help_file.examples:
                    yield '{}.. cliexample:: {}'.format(self._INDENT, e.short_summary)
                    yield ''
                    yield self._DOUBLE_INDENT + e.command.replace("\\", "\\\\")
                    yield ''

    def run(self):
        node = nodes.section()
        node.document = self.state.document
        result = ViewList()
        for line in self.make_rst():
            result.append(line, '<azhelpgen>')

        nested_parse_with_titles(self.state, result, node)
        return node.children

    def _get_help_files(self, az_cli):
        raise NotImplementedError()

    def _load_doc_source_map(self):
        raise NotImplementedError()

    def _get_doc_source_content(self, doc_source_map, help_file):
        raise NotImplementedError()

    @staticmethod
    def _get_param_value_sources(param):
        commands = []
        for value_source in param.value_sources:
            try:
                commands.append(value_source["link"]["command"])
            except KeyError:
                continue
        return commands


def setup_common_directives(app):
    """ Add common directives to the sphinx app. This should be called by setup(app) which sphinx searches for when
        generating documentation from sphinx extensions.

    :param app: The sphinx app
    :return:
    """

    _add_directive(app, 'cligroup', CliGroupDirective)
    _add_directive(app, 'clicommand', CliCommandDirective)
    _add_directive(app, 'cliarg', CliArgumentDirective)
    _add_directive(app, 'cliexample', CliExampleDirective)


def _add_directive(app, name, cls):
    # check based on similar check in Sphinx().add_directive
    if name not in directives._directives:  # pylint: disable=protected-access
        app.add_directive(name, cls)
