from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from coolqlite._common import (
    CoolqliteError,
    CoolqliteExceptionGroup,
    MissingToSqlFnError,
)


@dataclass(kw_only=True)
class Param:
    "An analyzed input parameter."

    param_name: str
    """The name assigned to the parameter.

    May be mangled if a filter was applied
    or it wasn't a valid identifier.
    """

    original_param_expr: str
    "The original expression used in the t-string."

    converter_name: str
    """The name of the converter to use.

    (Has not been validated to be present,
    that happens during building.)
    """
    original_value: Any
    "The value of the original expression in the t-string."


@dataclass(kw_only=True)
class ConvertedParam(Param):
    "A param that has been converted."

    converted_value: Any
    "The converted value."


@dataclass(eq=False)
class AnalyzedQuery:
    """A template that has been analyzed
    into the query string and
    (possibly name-mangled) parameters,
    ready to be converted.
    """

    query_str: str
    params: dict[str, Param]
    """Maps (possibly mangled) param name to the param."""

    def __str__(self):  # pragma: no cover
        return self.query_str


# while AnalyzedQuery and BuiltQuery could just be the
# same class but generic,
# I want to reduce the chance of them getting mixed up.
@dataclass(eq=False)
class BuiltQuery:
    """A query that has been "built"
    from an :class:`AnalyzedQuery`,
    with its parameters converted.
    """

    query_str: str
    params: dict[str, ConvertedParam]
    """Maps (possibly mangled) param name to the param."""

    def _converted_dict(self) -> dict[str, Any]:
        return {key: value.converted_value for key, value in self.params.items()}

    def __str__(self):  # pragma: no cover
        return self.query_str


@dataclass
class ParamMissingToSqlFnError(CoolqliteError):
    """A converter fn was missing.
    See error message for details.
    """

    param: Param
    "The parameter whose converter couldn't be found."

    cause: MissingToSqlFnError
    """
    This *should* be the same as ``__cause__`` but
    this helps guarantee it's there.
    """


@dataclass
class ToSqlFnFailure(CoolqliteError):
    """A converter itself threw an exception."""

    param: Param
    "The parameter being processed when this error occurred."


class QueryBuilderErrorGroup(
    CoolqliteExceptionGroup[ParamMissingToSqlFnError | ToSqlFnFailure]
):
    """Errors that occurred during query building."""

    def __new__(cls, message, exceptions, *_args, **kwargs):
        return super().__new__(cls, message, exceptions)

    def __init__(
        self,
        message: str,
        exceptions: Sequence[ParamMissingToSqlFnError | ToSqlFnFailure],
        input_: AnalyzedQuery,
        converted: dict[str, ConvertedParam],
    ):
        super().__init__(message, exceptions)
        self.input_ = input_
        self.converted = converted
