"""
Query processing.

You probably won't need to use this module
if you're just using :py:meth:`coolqlite.Connection.query`,
but it may be handy if you want to debug your queries or conversions.

Query proccessing happens in two steps,
arbitrarily (but alphabetically!) named "analyze" and "build".

1. Analyze takes a :class:`string.templatelib.Template` (t-string)
   and turns it into an :class:`AnalyzedQuery`,
   which holds the sql query string and named parameters.
   The values of those parameters haven't been converted yet though.
2. Build takes that :class:`AnalyzedQuery` and turns it into a
   :class:`BuiltQuery`, which has run all conversions.
   This step can fail, but it will collect all errors into
   a :class:`CoolqliteExceptionGroup` so you can see each one.

.. warning:: These names will likely change,
   especially if we eventually get an actual query builder.

   These mostly exist for you to debug your queries and conversions;
   it should not be a stable API to depend upon for now.
"""

from string.templatelib import Interpolation, Template
from coolqlite.to_sql import ToSqlFnRegistry
from coolqlite._common import (
    MissingNamedToSqlFnError,
    MissingTypedToSqlFnError,
)
from coolqlite.query._common import (
    AnalyzedQuery,
    BuiltQuery,
    ConvertedParam,
    Param,
    ParamMissingToSqlFnError,
    QueryBuilderErrorGroup,
    ToSqlFnFailure,
)


def analyze_query(query: Template) -> AnalyzedQuery:
    "Anlyze a template."
    return _QueryAnalyzer().analyze(query)


def _build_query(analyzed: AnalyzedQuery, conv_reg: ToSqlFnRegistry) -> BuiltQuery:
    "Build an analyzed query using the given converter registry."
    errors: list[ParamMissingToSqlFnError | ToSqlFnFailure] = []
    converted_params: dict[str, ConvertedParam] = {}

    for param in analyzed.params.values():
        try:
            if param.converter_name:
                converter = conv_reg._get_named(param.converter_name)
            else:
                converter = conv_reg._get_typed(type(param.original_value))
        except (MissingTypedToSqlFnError, MissingNamedToSqlFnError) as e:
            missing = ParamMissingToSqlFnError(param, e)
            missing.__cause__ = e

            errors.append(missing)
            continue

        try:
            value = converter(param.original_value)
        except (MissingTypedToSqlFnError, MissingNamedToSqlFnError) as e:
            # type dispatch happens later
            # so we need to get it here.
            missing = ParamMissingToSqlFnError(param, e)
            missing.__cause__ = e

            errors.append(missing)
            continue
        except Exception as e:
            adapter_err = ToSqlFnFailure(param)
            adapter_err.__cause__ = e

            errors.append(adapter_err)
            continue

        converted_params[param.param_name] = ConvertedParam(
            param_name=param.param_name,
            original_param_expr=param.original_param_expr,
            converter_name=param.converter_name,
            original_value=param.original_value,
            converted_value=value,
        )

    if errors:
        raise QueryBuilderErrorGroup(
            "Errors during query building", errors, analyzed, converted_params
        )
    return BuiltQuery(analyzed.query_str, converted_params)


class _QueryAnalyzer:
    def __init__(self):
        self.query_str = ""
        self._generated_name_counter = 0
        self.params: dict[str, Param] = {}

    def _gen_name(self) -> str:
        name = f"_CQL_{self._generated_name_counter}"
        self._generated_name_counter += 1
        return name

    def _make_name(self, expr: str, conv: str) -> str:
        match (expr.isidentifier(), conv):
            case (True, ""):
                return expr
            case (True, conv) if conv.isidentifier():
                return f"_CQL_{expr}_{conv}"
            case _:
                return self._gen_name()

    # TODO: double-escape _CQL if they used that
    # TODO: two interps next to each other has to be a bug, right?
    # it won't look like that in python which makes this even more pernicious, hmm
    def _add_part(self, part: str | Interpolation):
        if isinstance(part, str):
            # I believe this is as well-optimized as "".join(list[str]) ?
            self.query_str += part
            return
        # TODO: nicer error
        assert part.conversion is None, "conversions not yet supported, do it manually"
        if part.format_spec == "DANGER":
            self.query_str += str(part.value)
            return

        name = self._make_name(part.expression, part.format_spec)
        self.query_str += f":{name}"
        if name not in self.params:
            self.params[name] = Param(
                param_name=name,
                original_param_expr=part.expression,
                converter_name=part.format_spec,
                original_value=part.value,
            )

    @classmethod
    def analyze(cls, template: Template) -> AnalyzedQuery:
        self = cls()
        for part in template:
            self._add_part(part)
        return AnalyzedQuery(self.query_str, self.params)


__all__ = [
    "analyze_query",
    "AnalyzedQuery",
    "Param",
    "ConvertedParam",
    "BuiltQuery",
    "ParamMissingToSqlFnError",
    "ToSqlFnFailure",
    "QueryBuilderErrorGroup",
]
