"""
Delegate for handling click errors.

Copyright (c) 2026 Proton AG

This file is part of Proton VPN.

Proton VPN 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.

Proton VPN 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 ProtonVPN.  If not, see <https://www.gnu.org/licenses/>.
"""
import re
from typing import Optional

import click
from click.exceptions import \
    BadParameter, \
    ClickException, \
    MissingParameter, \
    UsageError

from proton.vpn.cli._cli_constants import HELP_OPTION
from proton.vpn.cli.commands.feature_setting_definitions import ALL_FEATURES


_BAD_CMD_RE = re.compile(r"No such command '([^']+)'")
_BAD_CHOICE_RE = re.compile(r": '([^']+)' is not ")


class ClickExceptionHandler:  # pylint: disable=too-few-public-methods
    """Handles exceptions generated by click when parsing and executing commands"""

    @staticmethod
    def handle_error(exception: Exception) -> bool:  # pylint: disable=too-many-return-statements
        """
        Handles the provided error and returns True.
        Returns False otherwise.
        """
        # MissingParameter and BadParameter are subclasses of UsageError,
        # so they must be checked before the generic UsageError handler.
        if isinstance(exception, MissingParameter):
            return ClickExceptionHandler._handle_missing_parameter(exception)

        if isinstance(exception, BadParameter):
            match = _BAD_CHOICE_RE.search(exception.format_message())
            bad_value = match.group(1) if match else "?"
            return ClickExceptionHandler._handle_bad_parameter(exception, bad_value)

        if isinstance(exception, UsageError):
            if exception.ctx and isinstance(exception.ctx.command, click.Group):
                match = _BAD_CMD_RE.search(exception.format_message())
                bad_cmd = match.group(1) if match else None
                is_root_cmd = exception.ctx.parent is None

                if is_root_cmd and not bad_cmd:
                    # Root command with no input given
                    # show the quickstart screen.
                    exception.show()
                    return True

                return ClickExceptionHandler._handle_no_such_command(exception, bad_cmd)

            lines = [
                f"Error: {exception.format_message()}\n",
            ]
            if exception.ctx:
                lines.append(
                    f"Try '{exception.ctx.command_path} {HELP_OPTION}' for more information."
                )
            click.echo("\n".join(lines), err=True)
            return True

        if isinstance(exception, ClickException):
            error_message = f"Error: {exception.format_message()}\n"
            click.echo(error_message, err=True)
            return True

        return False

    @staticmethod
    def _handle_no_such_command(exc: UsageError, bad_cmd: Optional[str]) -> bool:
        ctx = exc.ctx
        lines = []
        parsed_command_name = ctx.info_name
        if bad_cmd:
            lines.append(
                f"Error: Unknown action '{bad_cmd}' for '{parsed_command_name}' command.\n"
            )
        else:
            lines.append(f"Error: Missing action for '{parsed_command_name}' command.\n")

        subcommands = ClickExceptionHandler._list_subcommands(ctx)
        if subcommands:
            lines.append("Available actions:")
            col_width = max(len(name) for name, _ in subcommands) + 2
            for name, short_help in subcommands:
                lines.append(f"  {name:<{col_width}} {short_help}")

        lines.append(f"\nTry '{ctx.command_path} {HELP_OPTION}' for more information.")
        click.echo("\n".join(lines), err=True)
        return True

    @staticmethod
    def _handle_missing_parameter(exc: MissingParameter) -> bool:
        ctx = exc.ctx
        param = exc.param

        if param is None or ctx is None:
            return False

        if isinstance(param.type, click.Choice):
            parsed_command_name = ctx.info_name
            lines = [
                f"Error: Missing value for '{parsed_command_name}' setting.\n",
                *ClickExceptionHandler._format_valid_values(ctx.command.name, param.type.choices),
                f"\nTry '{ctx.command_path} {HELP_OPTION}' for examples.",
            ]
        else:
            param_label = param.human_readable_name or param.name or "argument"
            lines = [
                f"Error: Missing {param_label.lower()} argument.\n",
                f"Usage: {ctx.command_path} <{param_label.upper()}>\n",
                f"Try '{ctx.command_path} {HELP_OPTION}' for examples.",
            ]

        click.echo("\n".join(lines), err=True)
        return True

    @staticmethod
    def _handle_bad_parameter(exc: BadParameter, bad_value: str) -> bool:
        ctx = exc.ctx
        param = exc.param

        if param is None or ctx is None:
            return False

        if isinstance(param.type, click.Choice):
            parsed_command_name = ctx.info_name
            lines = [
                f"Error: Invalid value '{bad_value}' for '{parsed_command_name}'.\n",
                *ClickExceptionHandler._format_valid_values(ctx.command.name, param.type.choices),
                f"\nTry '{ctx.command_path} {HELP_OPTION}' for examples.",
            ]
        else:
            lines = [
                f"Error: {exc.format_message()}\n",
                f"Try '{ctx.command_path} {HELP_OPTION}' for more information.",
            ]

        click.echo("\n".join(lines), err=True)
        return True

    @staticmethod
    def _format_valid_values(command_name: str, choices: tuple[str, ...]) -> list[str]:
        """
        Returns lines for the 'Valid values' block.

        When the command matches a known feature setting, returns a multi-line
        block with value/description pairs sourced from the feature definition.
        Falls back to a single comma-separated line for unrecognised commands.
        """
        # maps a feature's command name to its list of value,help pairs
        feature_to_value_help = {
            feature.command: feature.value_to_help
            for feature in ALL_FEATURES
            if feature.value_to_help
        }
        value_help = feature_to_value_help.get(command_name)
        if value_help:
            return [
                "Valid values:",
                *[
                    f"  {value}  {description}"
                    for value, description in value_help.items()
                ],
            ]
        return [f"Valid values: {', '.join(choices)}"]

    @staticmethod
    def _list_subcommands(ctx: click.Context) -> list[tuple[str, str]]:
        """Returns (name, short_help) pairs for all visible subcommands."""
        result = []
        cmd = ctx.command
        if isinstance(cmd, click.Group):
            for name in cmd.list_commands(ctx):
                subcmd = cmd.get_command(ctx, name)
                if subcmd and not subcmd.hidden:
                    short_help = subcmd.get_short_help_str(limit=100)
                    result.append((name, short_help))
        return result
