import enum
import sys
import tomllib
import traceback
from collections.abc import Callable
from functools import reduce, wraps
from typing import Any, Protocol

import click
from click import Context

from . import __version__, config


class EnumChoice[Enum: enum.Enum](click.Choice[Enum]):
    values: type[Enum]

    def __init__(self, values: type[Enum]) -> None:
        super().__init__(
            (values[value] for value in values.__members__),
            case_sensitive=False,
        )
        self.values = values

    def normalize_choice(self, choice: Enum, ctx: Context | None) -> str:
        if not isinstance(choice, self.values):
            return ""
        return choice.name.lower().replace("_", "-")

    def convert(
        self, value: Any, param: click.Parameter | None, ctx: click.Context | None
    ) -> Enum:
        if isinstance(value, self.values):
            return value

        for choice in self.values:
            if self.normalize_choice(choice, ctx) == value:
                return choice

        return super().convert(value, param, ctx)


class AcceptingVerbose[T, **P](Protocol):
    def __call__(self, verbose: bool, *args: P.args, **kwargs: P.kwargs) -> T: ...


def error_reporting[T, **P](fun: Callable[P, T], /) -> AcceptingVerbose[T, P]:
    @click.option(
        "--verbose/--no-verbose",
        "-v/-V",
        default=False,
        help="Run command in verbose mode",
    )
    @wraps(fun)
    @click.pass_context
    def wrapper(
        ctx: click.Context,
        /,
        verbose: bool = False,
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> T:
        try:
            return ctx.invoke(
                fun,
                *args,
                **kwargs,
            )
        except Exception as e:
            if verbose:
                click.secho(f"An error occurred: `{e!r}`", fg="red")
                for line in traceback.format_exception(e):
                    click.secho(f"{line}", nl=False, fg="red")
            else:
                click.secho(
                    f"An error occurred: `{e}` (re-run with -v for stacktrace)",
                    fg="red",
                )
            sys.exit(-1)

    return wrapper


class AcceptingConfig[**P](Protocol):
    def __call__(
        self, defines: tuple[str, ...], *args: P.args, **kwargs: P.kwargs
    ) -> None: ...


def deep_merge(val1: Any, val2: Any) -> Any:
    if isinstance(val1, dict) and isinstance(val2, dict):
        for k, v in val2.items():
            if k not in val1:
                val1[k] = v
            else:
                v1 = val1[k]
                val1[k] = deep_merge(v1, v)

        return val1
    else:
        return val2


def configurable[**P](fun: Callable[P, None], /) -> AcceptingConfig[P]:
    @click.option(
        "--define",
        "-D",
        "defines",
        type=str,
        multiple=True,
        help="Define configuration values, using TOML syntax: "
        "-D feature.enabled=true."
        "Use -D - to see list of available configurations.",
    )
    @wraps(fun)
    @click.pass_context
    def wrapper(
        ctx: click.Context,
        /,
        defines: tuple[str, ...],
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> None:
        for define in defines:
            if define.strip() == "-":
                click.secho("Available configurations (with defaults):", underline=True)
                for key, value in config.items():
                    click.echo(f"{key} = {value}")
                return

        parsed_defines: dict[str, Any] = reduce(
            deep_merge, map(tomllib.loads, defines), {}
        )
        with config.override(parsed_defines):
            ctx.invoke(
                fun,
                *args,
                **kwargs,
            )

    return wrapper


versioned = click.version_option(__version__)
