"""Config subcommand implementation."""

from __future__ import annotations

import curses
import re
import shlex
import shutil

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

from ansible_navigator.action_base import ActionBase
from ansible_navigator.action_defs import RunStdoutReturn
from ansible_navigator.runner import AnsibleConfig
from ansible_navigator.runner import Command
from ansible_navigator.steps import Step
from ansible_navigator.ui_framework import CursesLine
from ansible_navigator.ui_framework import CursesLinePart
from ansible_navigator.ui_framework import CursesLines
from ansible_navigator.ui_framework import Interaction
from ansible_navigator.ui_framework import nonblocking_notification
from ansible_navigator.ui_framework import warning_notification
from ansible_navigator.utils.serialize import Loader
from ansible_navigator.utils.serialize import yaml

from . import _actions as actions
from . import run_action


if TYPE_CHECKING:
    from ansible_navigator.app_public import AppPublic
    from ansible_navigator.configuration_subsystem.definitions import ApplicationConfiguration


def color_menu(colno: int, colname: str, entry: dict[str, Any]) -> tuple[int, int]:
    """Provide a color for a collections menu entry in one column.

    Args:
        colno: The column number
        colname: The column name
        entry: The menu entry

    Returns:
        The color and decoration
    """
    if entry["default"] is False:
        return 3, 0
    return 2, 0


def content_heading(obj: Any, screen_w: int) -> CursesLines | None:
    """Create a heading for config content.

    Args:
        obj: The content going to be shown
        screen_w: The current screen width

    Returns:
        The heading
    """
    string = obj["name"]
    if obj["default"] is False:
        string += f" (current: {obj['current_value']})  (default: {obj['default_value']})"
        color = 3
    else:
        string += f" (current/default: {obj['current_value']})"
        color = 2

    string = string + (" " * (screen_w - len(string) + 1))

    line_part = CursesLinePart(
        column=0,
        string=string,
        color=color,
        decoration=curses.A_UNDERLINE,
    )
    return CursesLines((CursesLine((line_part,)),))


def filter_content_keys(obj: dict[Any, Any]) -> dict[Any, Any]:
    """Filter out some keys when showing collection content.

    Args:
        obj: The object from which keys should be removed

    Returns:
        The object with keys removed
    """
    return {k: v for k, v in obj.items() if not k.startswith("__")}


@actions.register
class Action(ActionBase):
    """Config subcommand implementation."""

    KEGEX = r"^config(\s(?P<params>.*))?$"

    def __init__(self, args: ApplicationConfiguration) -> None:
        """Initialize the ``:config`` action.

        Args:
            args: The current settings for the application
        """
        super().__init__(args=args, logger_name=__name__, name="config")

        self._config: list[Any] | None = None
        self._runner: AnsibleConfig | Command

    def run(self, interaction: Interaction, app: AppPublic) -> Interaction | None:
        """Execute the ``config`` request for mode interactive.

        Args:
            interaction: The interaction from the user
            app: The app instance

        Returns:
            The pending
            :class:`~ansible_navigator.ui_framework.ui.Interaction` or
            :data:`None`
        """
        self._logger.debug("config requested in interactive mode")
        self._prepare_to_run(app, interaction)

        notification = nonblocking_notification(
            messages=[
                "Collecting the ansible configuration, this may take a minute...",
            ],
        )
        interaction.ui.show_form(notification)

        args_updated = self._update_args(
            [self._name] + shlex.split(self._interaction.action.match.groupdict()["params"] or ""),
        )
        if not args_updated:
            self._prepare_to_exit(interaction)
            return None

        self._run_runner()
        if self._config is None:
            self._prepare_to_exit(interaction)
            return None

        self.steps.append(self._build_main_menu())

        while True:
            self._calling_app.update()
            self._take_step()

            if not self.steps:
                break

            if self.steps.current.name == "quit":
                return self.steps.current

        self._prepare_to_exit(interaction)
        return None

    def run_stdout(self) -> RunStdoutReturn:
        """Execute the ``config`` request for mode stdout.

        Returns:
            The return code or 1. If the response from the runner
            invocation is None, indicates there is no console output to
            display, so assume an issue and return 1 along with a
            message to review the logs.
        """
        self._logger.debug("config requested in stdout mode")
        response = self._run_runner()
        if (
            response is None
            or response[0] is None
            or response[1] is None
            or not isinstance(response[2], int)
        ):
            self._logger.error("Unexpected response: %s", response)
            return RunStdoutReturn(message="Please review the log for errors.", return_code=1)
        _out, error, return_code = response
        return RunStdoutReturn(message=error, return_code=return_code)

    def _take_step(self) -> None:
        """Take a step based on the current step or step back."""
        result = None
        if isinstance(self.steps.current, Interaction):
            result = run_action(self.steps.current.name, self.app, self.steps.current)
        elif isinstance(self.steps.current, Step):
            if self.steps.current.show_func:
                current_index = self.steps.current.index
                self.steps.current.show_func()
                self.steps.current.index = current_index

            if self.steps.current.type == "menu":
                result = self._interaction.ui.show(
                    obj=self.steps.current.value,
                    columns=self.steps.current.columns,
                    color_menu_item=color_menu,
                )
            elif self.steps.current.type == "content":
                result = self._interaction.ui.show(
                    obj=self.steps.current.value,
                    index=self.steps.current.index,
                    content_heading=content_heading,
                    filter_content_keys=filter_content_keys,
                )

        if result is None:
            self.steps.back_one()
        else:
            self.steps.append(result)

    def _build_main_menu(self) -> Step:
        """Build the menu of configuration options.

        Returns:
            The config menu definition
        """
        return Step(
            name="all_options",
            columns=["name", "default", "source", "__current"],
            select_func=self._build_option_content,
            step_type="menu",
            value=self._config,
        )

    def _build_option_content(self) -> Step:
        """Build the content for one configuration option.

        Returns:
            The option's content
        """
        return Step(
            name="option_content",
            step_type="content",
            value=self._config,
            index=self.steps.current.index,
        )

    def _run_runner(self) -> tuple[str, str, int] | None:
        """Use the runner subsystem to retrieve the configuration.

        Returns:
            For mode interactive nothing. For mode stdout the output,
            errors and return code from runner.
        """
        if isinstance(self._args.set_environment_variable, dict):
            set_env_vars = {**self._args.set_environment_variable}
        else:
            set_env_vars = {}

        if self._args.display_color is False:
            set_env_vars["ANSIBLE_NOCOLOR"] = "1"

        kwargs = {
            "container_engine": self._args.container_engine,
            "host_cwd": str(Path.cwd()),
            "execution_environment_image": self._args.execution_environment_image,
            "execution_environment": self._args.execution_environment,
            "navigator_mode": self._args.mode,
            "pass_environment_variable": self._args.pass_environment_variable,
            "set_environment_variable": set_env_vars,
            "private_data_dir": self._args.ansible_runner_artifact_dir,
            "rotate_artifacts": self._args.ansible_runner_rotate_artifacts_count,
            "timeout": self._args.ansible_runner_timeout,
        }

        if isinstance(self._args.execution_environment_volume_mounts, list):
            kwargs.update(
                {"container_volume_mounts": self._args.execution_environment_volume_mounts},
            )

        if isinstance(self._args.container_options, list):
            kwargs.update({"container_options": self._args.container_options})

        if self._args.mode == "interactive":
            self._run_runner_interactive(kwargs)
            return None
        return self._run_runner_stdout(kwargs)

    def _run_runner_interactive(
        self,
        kwargs: dict[str, Any],
    ) -> None:
        """Run the ansible-config runner in interactive mode.

        Fetches both the list and dump output from ansible-config,
        logs any errors, and parses/merges the results.

        Args:
            kwargs: The common runner keyword arguments.
        """
        self._runner = AnsibleConfig(**kwargs)
        config_file = self._args.config if isinstance(self._args.config, str) else None
        list_output, list_output_err = self._runner.fetch_ansible_config(
            "list",
            config_file=config_file,
        )
        dump_output, dump_output_err = self._runner.fetch_ansible_config(
            "dump",
            config_file=config_file,
        )
        if list_output_err:
            msg = f"Error occurred while fetching ansible config (list): '{list_output_err}'"
            self._logger.error(msg)
        if dump_output_err:
            msg = f"Error occurred while fetching ansible config (dump): '{dump_output_err}'"
            self._logger.error(msg)

        err_msg = "\n".join(e for e in (list_output_err, dump_output_err) if e)
        if "ERROR!" in err_msg or not list_output or not dump_output:
            warn_msg = ["Errors were encountered while gathering the configuration:"]
            if err_msg:
                warn_msg += err_msg.splitlines()
            if not list_output or not dump_output:
                warn_msg.append("The configuration could not be gathered.")
            warning = warning_notification(warn_msg)
            self._interaction.ui.show_form(warning)
        else:
            self._parse_and_merge(list_output, dump_output)

    def _run_runner_stdout(
        self,
        kwargs: dict[str, Any],
    ) -> tuple[str, str, int] | None:
        """Run the ansible-config runner in stdout mode.

        Resolves the ansible-config executable path, builds the
        pass-through arguments, and runs the command.

        Args:
            kwargs: The common runner keyword arguments.

        Raises:
            RuntimeError: When the ansible-config command cannot be
                found with execution environment support disabled.

        Returns:
            The output, errors and return code from runner.
        """
        if self._args.execution_environment:
            ansible_config_path = "ansible-config"
        else:
            exec_path = shutil.which("ansible-config")
            if exec_path is None:
                msg = "'ansible-config' executable not found"
                self._logger.error(msg)
                raise RuntimeError(msg)
            ansible_config_path = exec_path

        pass_through_arg = self._args.cmdline.copy() if isinstance(self._args.cmdline, list) else []

        if self._args.help_config is True:
            pass_through_arg.append("--help")

        if isinstance(self._args.config, str):
            pass_through_arg.extend(["--config", self._args.config])

        kwargs.update({"cmdline": pass_through_arg})

        self._runner = Command(executable_cmd=ansible_config_path, **kwargs)
        stdout_return = self._runner.run()
        return stdout_return

    def _parse_and_merge(self, list_output: str, dump_output: str) -> None:
        """Parse the list and dump output. Merge dump into list.

        Args:
            list_output: The output from config list
            dump_output: The output from config dump

        Returns:
            Nothing
        """
        try:
            parsed = yaml.load(list_output, Loader=Loader)
            self._logger.debug("yaml loading list output succeeded")
        except yaml.YAMLError as exc:
            self._logger.debug("error yaml loading list output: '%s'", str(exc))
            return

        parsed.pop("GALAXY_SERVERS", None)

        regex = re.compile(r"^(?P<variable>\S+)\((?P<source>.*)\)\s=\s(?P<current>.*)$")
        for line in dump_output.splitlines():
            if not line:
                break
            if not self._parse_dump_line(regex, line, parsed):
                return

        self._finalize_parsed_config(parsed)
        self._config = list(parsed.values())
        self._logger.debug("parsed and merged list and dump successfully")
        return

    def _parse_dump_line(
        self,
        regex: re.Pattern[str],
        line: str,
        parsed: dict[str, Any],
    ) -> bool:
        """Parse a single line from the ansible-config dump output.

        Extracts the variable name, source, and current value from
        the line and merges them into the parsed config dictionary.

        Args:
            regex: The compiled regex pattern for matching dump lines.
            line: A single line from the dump output.
            parsed: The parsed config dictionary to update in place.

        Returns:
            True if the line was parsed successfully, False on error.
        """
        extracted = regex.match(line)
        if not extracted:
            self._logger.error("Unparsable dump entry: %s", line)
            return False

        variable = extracted.groupdict()["variable"]
        try:
            source = yaml.load(extracted.groupdict()["source"], Loader=Loader)
        except yaml.YAMLError:
            source = extracted.groupdict()["source"]
        try:
            current = yaml.load(extracted.groupdict()["current"], Loader=Loader)
        except yaml.YAMLError:
            current = extracted.groupdict()["current"]
        try:
            if isinstance(source, dict):
                parsed[variable]["source"] = next(iter(source.keys()))
                parsed[variable]["via"] = next(iter(source.values()))
            else:
                parsed[variable]["source"] = source
                parsed[variable]["via"] = source
            current_as_str = str(current)

            target_screen_w = int(100 / 2)  # half a wide screen
            if len(current_as_str) > target_screen_w:
                more_indicator = "..."
                text_width = target_screen_w - len(more_indicator)
                current_as_str = f"{current_as_str[0:text_width]}{more_indicator}"
            parsed[variable]["__current"] = current_as_str
            parsed[variable]["current_value"] = current
        except KeyError:
            self._logger.exception("variable '%s' not found in list output", variable)
            return False
        return True

    def _finalize_parsed_config(self, parsed: dict[str, Any]) -> None:
        """Post-process the parsed config entries with display metadata.

        Adds option name, default indicators, and config file path
        to each parsed config entry.

        Args:
            parsed: The parsed config dictionary to update in place.
        """
        for key, value in parsed.items():
            value["option"] = key
            value["name"] = key.replace("_", " ").capitalize()
            value["default_value"] = value.get("default", "None")
            if value["source"] == "default":
                value["default"] = True
            else:
                value["default"] = False
            value["current_config_file"] = str(self._args.internals.ansible_configuration.path)

        self._config = list(parsed.values())
        self._logger.debug("parsed and merged list and dump successfully")
