"""
TODO:
 - quote mode lol
  - general 'modes' dc
"""
import typing as ta

from ... import check
from ... import collections as col
from ... import dataclasses as dc
from ... import dispatch
from ... import lang
from ...text import parts as tp
from ..params import ParamKey
from ..params import ParamsPreparer
from ..params import ParamStyle
from ..params import make_params_preparer
from ..params import substitute_params
from .base import Node
from .binary import Binary
from .binary import BinaryOp
from .binary import BinaryOps
from .deletes import Delete
from .exprs import Literal
from .exprs import NameExpr
from .exprs import ParamExpr
from .funcs import Func
from .idents import Ident
from .inserts import Insert
from .inserts import Values
from .keywords import LiteralKeyword
from .keywords import Star
from .multi import Multi
from .multi import MultiKind
from .names import Name
from .params import Param
from .relations import Join
from .relations import JoinKind
from .relations import Table
from .selects import AllSelectItem
from .selects import ExprSelectItem
from .selects import Select
from .selects import SelectExpr
from .selects import SelectRelation
from .unary import Unary
from .unary import UnaryOp
from .unary import UnaryOps
from .unions import Union
from .updates import Field
from .updates import Fields
from .updates import Update


##


RenderedQueryParams: ta.TypeAlias = ta.Sequence[Param] | ta.Mapping[str, Param]


@dc.dataclass(frozen=True)
class RenderedQueryParts(lang.Final):
    p: tp.Part
    params: RenderedQueryParams | None
    literals: ta.Mapping[Param, ta.Any] | None = None


@dc.dataclass(frozen=True)
class RenderedQuery(lang.Final):
    s: str
    params: RenderedQueryParams | None
    literals: ta.Mapping[Param, ta.Any] | None = None


class Renderer(lang.Abstract):
    def __init__(
            self,
            *,
            param_style: ParamStyle | None = None,
            literal_style: ta.Literal['param_all', 'safe_only'] = 'safe_only',
    ) -> None:
        super().__init__()

        self._param_style = param_style
        self._literal_style = literal_style

        self._seen_params: dict[Param, int] = {}
        self._literal_params: dict[Param, ta.Any] = {}

    #

    _params_preparer_: ParamsPreparer

    def _params_preparer(self) -> ParamsPreparer:
        try:
            return self._params_preparer_
        except AttributeError:
            pass

        if (ps := self._param_style) is None:
            raise ValueError('param_style is not set')

        self._params_preparer_ = pp = make_params_preparer(ps)
        return pp

    def _see_param(self, p: Param) -> None:
        self._seen_params.setdefault(p, len(self._seen_params))

    def _param_key(self, p: Param) -> ParamKey:
        return p.n if p.n is not None else id(p)

    def rendered_params(self) -> RenderedQueryParams | None:
        if self._param_style is None:
            check.empty(self._seen_params)
            return None

        prep_params = self._params_preparer().prepare()
        subst = col.make_map_by(self._param_key, self._seen_params, strict=True)
        return substitute_params(prep_params, subst, strict=True)

    def literal_params(self) -> ta.Mapping[Param, ta.Any] | None:
        return self._literal_params or None

    def _make_literal_param(self, o: ta.Any) -> Param:
        p = Param()
        self._literal_params[p] = o
        return p

    #

    @dispatch.method(
        instance_cache=True,
        strong_dispatch_cache=True,
        uncached_dispatch_miss=True,
    )
    def render(self, o: ta.Any) -> tp.Part:
        raise TypeError(o)

    @classmethod
    def render_query_parts(cls, o: ta.Any, *args: ta.Any, **kwargs: ta.Any) -> RenderedQueryParts:
        r = cls(*args, **kwargs)
        return RenderedQueryParts(
            r.render(o),
            r.rendered_params(),
            r.literal_params(),
        )

    @classmethod
    def render_query(cls, o: ta.Any, *args: ta.Any, **kwargs: ta.Any) -> RenderedQuery:
        rqp = cls.render_query_parts(o, *args, **kwargs)
        return RenderedQuery(
            tp.render(rqp.p),
            rqp.params,
            rqp.literals,
        )


class StdRenderer(Renderer):
    # parens

    NEEDS_PAREN_TYPES: ta.AbstractSet[type[Node]] = {
        Binary,
        # IsNull,
        # SelectExpr,
    }

    def needs_paren(self, node: Node) -> bool:
        return type(node) in self.NEEDS_PAREN_TYPES

    def paren(self, node: Node) -> tp.Part:
        return tp.Wrap(self.render(node)) if self.needs_paren(node) else self.render(node)

    # binary

    BINARY_OP_TO_STR: ta.ClassVar[ta.Mapping[BinaryOp, str]] = {
        BinaryOps.EQ: '=',
        BinaryOps.NE: '!=',
        BinaryOps.LT: '<',
        BinaryOps.LE: '<=',
        BinaryOps.GT: '>',
        BinaryOps.GE: '>=',

        BinaryOps.ADD: '+',
        BinaryOps.SUB: '-',
        BinaryOps.MUL: '*',
        BinaryOps.DIV: '/',
        BinaryOps.MOD: '%',

        BinaryOps.CONCAT: '||',
    }

    @Renderer.render.register
    def render_binary(self, o: Binary) -> tp.Part:
        return [
            self.paren(o.l),
            self.BINARY_OP_TO_STR[o.op],
            self.paren(o.r),
        ]

    # deletes

    @Renderer.render.register
    def render_delete(self, o: Delete) -> tp.Part:
        return [
            'delete from',
            self.render(o.from_),
            *(['where', self.render(o.where)] if o.where is not None else []),
        ]

    # exprs

    SAFE_LITERAL_TYPES: ta.ClassVar[tuple[type, ...]] = (
        int,
    )

    @Renderer.render.register
    def render_literal(self, o: Literal) -> tp.Part:
        if self._literal_style == 'param_all':
            pass

        elif self._literal_style == 'safe_only':
            if isinstance(o.v, self.SAFE_LITERAL_TYPES):
                return str(o.v)

        else:
            raise ValueError(self._literal_style)

        return self.render(self._make_literal_param(o.v))

    @Renderer.render.register
    def render_name_expr(self, o: NameExpr) -> tp.Part:
        return self.render(o.n)

    @Renderer.render.register
    def render_param_expr(self, o: ParamExpr) -> tp.Part:
        return self.render(o.p)

    # funcs

    @Renderer.render.register
    def render_func(self, o: Func) -> tp.Part:
        args: list[tp.Part]
        if len(o.args) == 1 and isinstance(a0 := o.args[0], SelectExpr):
            args = [self.render(a0.s)]
        else:
            args = [self.render(a) for a in o.args]

        return tp.Concat([
            self.render(o.name),
            tp.Wrap(tp.List(args)),
        ])

    # idents

    @Renderer.render.register
    def render_ident(self, o: Ident) -> tp.Part:
        return f'"{o.s}"'

    # inserts

    @Renderer.render.register
    def render_values(self, o: Values) -> tp.Part:
        return [
            'values',
            tp.Wrap(tp.List([self.render(v) for v in o.vs])),
        ]

    @Renderer.render.register
    def render_insert(self, o: Insert) -> tp.Part:
        return [
            'insert into',
            self.render(o.into),
            tp.Wrap(tp.List([self.render(c) for c in o.columns])),
            self.render(o.data),
        ]

    # keywords

    @Renderer.render.register
    def render_literal_keyword(self, o: LiteralKeyword) -> tp.Part:
        return o.s

    @Renderer.render.register
    def render_star(self, o: Star) -> tp.Part:
        return '*'

    # multis

    MULTI_KIND_TO_STR: ta.ClassVar[ta.Mapping[MultiKind, str]] = {
        MultiKind.AND: 'and',
        MultiKind.OR: 'or',
    }

    @Renderer.render.register
    def render_multi(self, o: Multi) -> tp.Part:
        return tp.Wrap(tp.List(
            [self.render(e) for e in o.es],
            delimiter=' ' + self.MULTI_KIND_TO_STR[o.k],  # FIXME: Part
        ))

    # names

    @Renderer.render.register
    def render_name(self, o: Name) -> tp.Part:
        out: list[tp.Part] = []
        for n, i in enumerate(o.ps):
            if n:
                out.append('.')
            out.append(self.render(i))
        return tp.Concat(out)

    # params

    @Renderer.render.register
    def render_param(self, o: Param) -> tp.Part:
        self._see_param(o)
        return self._params_preparer().add(self._param_key(o))

    # relations

    @Renderer.render.register
    def render_table(self, o: Table) -> tp.Part:
        return [
            self.render(o.n),
            *(['as', self.render(o.a)] if o.a is not None else []),
        ]

    JOIN_KIND_TO_STR: ta.ClassVar[ta.Mapping[JoinKind, str]] = {
        JoinKind.DEFAULT: 'join',
        JoinKind.INNER: 'inner join',
        JoinKind.LEFT: 'left join',
        JoinKind.RIGHT: 'right join',
        JoinKind.FULL: 'full join',
        JoinKind.CROSS: 'cross join',
        JoinKind.NATURAL: 'natural join',
    }

    @Renderer.render.register
    def render_join(self, o: Join) -> tp.Part:
        return [
            self.render(o.l),
            self.JOIN_KIND_TO_STR[o.k],
            self.render(o.r),
            *(['on', self.render(o.c)] if o.c is not None else []),
        ]

    # selects

    @Renderer.render.register
    def render_all_select_item(self, o: AllSelectItem) -> tp.Part:
        return '*'

    @Renderer.render.register
    def render_expr_select_item(self, o: ExprSelectItem) -> tp.Part:
        return [
            self.render(o.v),
            *(['as', self.render(o.a)] if o.a is not None else []),
        ]

    @Renderer.render.register
    def render_select(self, o: Select) -> tp.Part:
        return [
            'select',
            tp.List([self.render(i) for i in o.items]),
            *(['from', self.render(o.from_)] if o.from_ is not None else []),
            *(['where', self.render(o.where)] if o.where is not None else []),
        ]

    @Renderer.render.register
    def render_select_expr(self, o: SelectExpr) -> tp.Part:
        return tp.Wrap(self.render(o.s))

    @Renderer.render.register
    def render_select_relation(self, o: SelectRelation) -> tp.Part:
        return tp.Wrap(self.render(o.s))

    # unary

    UNARY_OP_TO_STR: ta.ClassVar[ta.Mapping[UnaryOp, tuple[str, str]]] = {
        UnaryOps.NOT: ('not ', ''),
        UnaryOps.IS_NULL: ('', ' is null'),
        UnaryOps.IS_NOT_NULL: ('', ' is not null'),

        UnaryOps.POS: ('+', ''),
        UnaryOps.NEG: ('-', ''),
    }

    @Renderer.render.register
    def render_unary(self, o: Unary) -> tp.Part:
        pfx, sfx = self.UNARY_OP_TO_STR[o.op]
        return tp.Concat([
            pfx,
            self.render(o.v),
            sfx,
        ])

    # unions

    @Renderer.render.register
    def render_union(self, o: Union) -> tp.Part:
        return list(lang.interleave(
            'union all' if o.all else 'union',
            (self.render(s) for s in o.selects),
        ))

    # updates

    @Renderer.render.register
    def render_field(self, o: Field) -> tp.Part:
        return [
            self.render(o.c),
            '=',
            self.render(o.v),
        ]

    @Renderer.render.register
    def render_fields(self, o: Fields) -> tp.Part:
        return tp.List([self.render(f) for f in o.fields])

    @Renderer.render.register
    def render_update(self, o: Update) -> tp.Part:
        return [
            'update',
            self.render(o.into),
            'set',
            self.render(o.fields),
            *(['where', self.render(o.where)] if o.where is not None else []),
        ]


def render_parts(n: Node, **kwargs: ta.Any) -> RenderedQueryParts:
    return StdRenderer.render_query_parts(n, **kwargs)


def render(n: Node, **kwargs: ta.Any) -> RenderedQuery:
    return StdRenderer.render_query(n, **kwargs)
