#  Licensed under the Apache License, Version 2.0 (the "License"); you may
#  not use this file except in compliance with the License. You may obtain
#  a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#  License for the specific language governing permissions and limitations
#  under the License.

"""Application base class for displaying data."""

import abc
import argparse
from collections.abc import Iterable, Iterator, Sequence
from itertools import compress
from typing import Any, Generic, TypeVar

import stevedore

from cliff import app
from cliff import command
from cliff.formatters import base as base_formatters

_T = TypeVar("_T")


class DisplayCommandBase(
    command.Command,
    Generic[base_formatters.FormatterT],
    metaclass=abc.ABCMeta,
):
    """Command base class for displaying data about a single object."""

    _formatter_plugins: stevedore.ExtensionManager[base_formatters.FormatterT]
    formatter: base_formatters.FormatterT

    def __init__(
        self,
        app: app.App,
        app_args: argparse.Namespace | None,
        cmd_name: str | None = None,
    ) -> None:
        super().__init__(app, app_args, cmd_name=cmd_name)
        self._formatter_plugins = self._load_formatter_plugins()

    @property
    @abc.abstractmethod
    def formatter_namespace(self) -> str:
        """Namespace to use for loading formatter plugins."""

    @property
    @abc.abstractmethod
    def formatter_default(self) -> str:
        """String specifying the name of the default formatter."""

    def _load_formatter_plugins(
        self,
    ) -> stevedore.ExtensionManager[base_formatters.FormatterT]:
        # Here so tests can override
        return stevedore.ExtensionManager(
            self.formatter_namespace,
            invoke_on_load=True,
        )

    def get_parser(self, prog_name: str) -> argparse.ArgumentParser:
        parser = super().get_parser(prog_name)
        formatter_group = parser.add_argument_group(
            title='output formatters',
            description='output formatter options',
        )
        self._formatter_group = formatter_group
        formatter_choices = sorted(self._formatter_plugins.names())
        formatter_default = self.formatter_default
        if formatter_default not in formatter_choices:
            formatter_default = formatter_choices[0]
        formatter_group.add_argument(
            '-f',
            '--format',
            dest='formatter',
            action='store',
            choices=formatter_choices,
            default=formatter_default,
            help=f'the output format, defaults to {formatter_default}',
        )
        formatter_group.add_argument(
            '-c',
            '--column',
            action='append',
            default=[],
            dest='columns',
            metavar='COLUMN',
            help=(
                'specify the column(s) to include, can be '
                'repeated to show multiple columns'
            ),
        )
        for formatter in self._formatter_plugins:
            assert formatter.obj is not None  # noqa
            formatter.obj.add_argument_group(parser)
        return parser

    @abc.abstractmethod
    def produce_output(
        self,
        parsed_args: argparse.Namespace,
        column_names: Sequence[str],
        data: Any,
    ) -> int:
        """Use the formatter to generate the output.

        :param parsed_args: argparse.Namespace instance with argument values
        :param column_names: sequence of strings containing names
                             of output columns
        :param data: iterable with values matching the column names
        :returns: a status code
        """

    def _generate_columns_and_selector(
        self,
        parsed_args: argparse.Namespace,
        column_names: Sequence[str],
    ) -> tuple[list[str], list[bool] | None]:
        """Generate included columns and selector according to parsed args.

        We normalize the column names so that someone can do e.g. '-c
        server_name' when the output field is actually called 'Server Name'.

        :param parsed_args: argparse.Namespace instance with argument values
        :param column_names: sequence of strings containing names
                             of output columns
        """
        if not parsed_args.columns:
            return list(column_names), None

        def normalize_column(column_name: str) -> str:
            return column_name.lower().strip().replace(' ', '_')

        requested_columns = [normalize_column(c) for c in parsed_args.columns]
        columns_to_include = [
            c for c in column_names if normalize_column(c) in requested_columns
        ]
        if not columns_to_include:
            raise ValueError(
                f'No recognized column names in {str(parsed_args.columns)}. '
                f'Recognized columns are {str(column_names)}.'
            )

        # Set up argument to compress()
        selector = [(c in columns_to_include) for c in column_names]
        return columns_to_include, selector

    def run(self, parsed_args: argparse.Namespace) -> int:
        parsed_args = self._run_before_hooks(parsed_args)
        formatter = self._formatter_plugins[parsed_args.formatter].obj
        assert formatter is not None  # noqa
        self.formatter = formatter
        column_names, data = self.take_action(parsed_args)
        column_names, data = self._run_after_hooks(
            parsed_args, (column_names, data)
        )
        self.produce_output(parsed_args, column_names, data)
        return 0

    def _run_after_hooks(  # type: ignore[override]
        self,
        parsed_args: argparse.Namespace,
        data: tuple[Sequence[str], Iterable[Any]],
    ) -> tuple[Sequence[str], Iterable[Any]]:
        """Calls after() method of the hooks.

        This method is intended to be called from the run() method after
        take_action() is called.

        This method should only be overridden by developers creating new
        command base classes and only if it is necessary to have different
        hook processing behavior.
        """
        for hook in self._hooks:
            # we need to ignore the types since CommandHook states that it
            # should return an integer, but they'll actually return a tuple of
            # column names and data when used with DisplayCommandBase
            # subclasses
            ret = hook.obj.after(parsed_args, data)  # type: ignore
            # If the return is None do not change return_code, otherwise
            # set up to pass it to the next hook
            if ret is not None:
                data = ret  # type: ignore
        return data

    @staticmethod
    def _compress_iterable(
        iterable: Iterable[_T],
        selectors: Iterable[Any],
    ) -> Iterator[_T]:
        return compress(iterable, selectors)
