"""Base effect configuration classes.

`BaseConfig` works with `argutils.ArgSpec` field defaults to define effect
configuration options and populate an `argparse.ArgumentParser`.

`BaseConfig._build_config` constructs config instances either from a parsed
`argparse.Namespace` or from the default values stored on each field's
`ArgSpec`.
"""

from __future__ import annotations

import argparse
import typing
from dataclasses import dataclass, fields

from terminaltexteffects.utils import argutils
from terminaltexteffects.utils.graphics import Color, Gradient


@dataclass(frozen=True)
class FinalGradientDirectionArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient direction."""

    name: str = "--final-gradient-direction"
    type: typing.Callable[[str], Gradient.Direction] = argutils.GradientDirection.type_parser
    default: Gradient.Direction = Gradient.Direction.VERTICAL
    metavar: str = argutils.GradientDirection.METAVAR
    help: str = "Direction of the final gradient across the text."


@dataclass(frozen=True)
class FinalGradientStopsArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient stops."""

    name: str = "--final-gradient-stops"
    type: typing.Callable[[str], Color] = argutils.ColorArg.type_parser
    nargs: str = "+"
    action: type[argutils.TupleAction] = argutils.TupleAction
    default: tuple[Color, ...] = (Color("#8A008A"), Color("#00D1FF"), Color("#FFFFFF"))
    metavar: str = argutils.ColorArg.METAVAR
    help: str = (
        "Space separated, unquoted, list of colors for the character gradient (applied across the canvas). "
        "If only one color is provided, the characters will be displayed in that color."
    )


@dataclass(frozen=True)
class FinalGradientStepsArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient steps."""

    name: str = "--final-gradient-steps"
    type: typing.Callable[[str], int] = argutils.PositiveInt.type_parser
    nargs: str = "+"
    action: type[argutils.TupleAction] = argutils.TupleAction
    default: tuple[int, ...] | int = 12
    metavar: str = argutils.PositiveInt.METAVAR
    help: str = (
        "Space separated, unquoted, list of the number of gradient steps to use. "
        "More steps will create a smoother and longer gradient animation."
    )


@dataclass(frozen=True)
class FinalGradientFramesArg(argutils.ArgSpec):
    """Argument specification for selecting the final text gradient frames."""

    name: str = "--final-gradient-frames"
    type: typing.Callable[[str], int] = argutils.PositiveInt.type_parser
    default: int = 5
    metavar: str = argutils.PositiveInt.METAVAR
    help: str = "Number of frames to display each gradient step. Increase to slow down the gradient animation."


@dataclass
class BaseConfig:
    """Base configuration class for effects.

    This class serves as a base for all effect configurations, providing a common
    interface for argument parser population and configuration building. Effect config
    classes are created via `_build_config`, which reads values from a parsed
    `argparse.Namespace` and falls back to `argutils.ArgSpec` defaults for fields
    defined with `ArgSpec` instances. Any config class intended to be used to populate
    a subparser must define a `parser_spec` attribute with type `argutils.ParserSpec`.
    """

    @classmethod
    def _populate_parser(cls, parser: argparse.ArgumentParser | argparse._SubParsersAction) -> None:
        """Populate the argument parser with the config class's argument specs.

        If `parser` is a subparser collection, `cls.parser_spec` is used to create
        the subparser before adding field argument specs. Config classes used in that mode
        must define `parser_spec`.

        Raises:
            AttributeError: If `parser` is a subparser collection and `cls.parser_spec`
                is not defined.
        """
        if isinstance(parser, argparse._SubParsersAction):
            parser = parser.add_parser(**vars(cls.parser_spec))  # pyright: ignore[reportAttributeAccessIssue]
            parser.formatter_class = argutils.CustomFormatter  # pyright: ignore[reportAttributeAccessIssue]

        assert isinstance(parser, argparse.ArgumentParser)
        for field in fields(cls):
            if field.name == "parser_spec":
                continue
            spec = field.default
            assert isinstance(spec, argutils.ArgSpec)
            add_args_sig = {k: v for k, v in vars(spec).items() if v is not argutils._MISSING}
            parser.add_argument(add_args_sig.pop("name"), **add_args_sig)

    @classmethod
    def _build_config(cls: type[CONFIG], parsed_args: argparse.Namespace | None = None) -> CONFIG:
        """Build a config instance from parsed arguments or `ArgSpec` defaults.

        When `parsed_args` is provided, matching namespace attributes are used first and
        missing fields fall back to each field's `ArgSpec.default` when available. When
        `parsed_args` is `None`, the config is built entirely from `ArgSpec.default`
        values.

        Args:
            parsed_args (argparse.Namespace | None): Parsed CLI arguments, or None to use
                `ArgSpec` defaults only.

        Raises:
            AttributeError: If a required config field is missing from `parsed_args` and
                does not define an `ArgSpec` default.

        Returns:
            CONFIG: A populated config instance.
        """
        if parsed_args is not None:
            config_args: dict[str, typing.Any] = {}
            for field in fields(cls):
                if field.name == "parser_spec":
                    continue
                if hasattr(parsed_args, field.name):
                    config_args[field.name] = getattr(parsed_args, field.name)
                elif isinstance(field.default, argutils.ArgSpec):
                    config_args[field.name] = field.default.default
                else:
                    msg = f"Missing required config field '{field.name}' for {cls.__name__} in parsed arguments."
                    raise AttributeError(msg)
            return cls(**config_args)
        return cls(
            **{
                field.name: field.default.default
                for field in fields(cls)
                if isinstance(field.default, argutils.ArgSpec)
            },
        )


CONFIG = typing.TypeVar("CONFIG", bound=BaseConfig)
