# This file contains Python classes to parse SQL queries using sqlglot library.
# It defines dataclasses to represent various SQL components and classes
# to parse SELECT, INSERT, DELETE, and UPDATE statements into structured formats.

import re
from dataclasses import dataclass, field as dc_field
from typing import Any

import sqlglot


# ======================================================================================================================
# Dataclasses
# ======================================================================================================================

@dataclass
class Ident2:
    table: str | None = None
    schema: str | None = None


@dataclass
class Ident3:
    column: str | None = None
    table: str | None = None


    schema: str | None = None


@dataclass
class SelectedElement:
    alias: str | None = None
    expr: str | None = None
    column: Ident3 | None = None


@dataclass
class JoinTable:
    ident: Ident2 | None = None
    columns: list[str] = dc_field(default_factory=list)


@dataclass
class JoinElement:
    join_type: str | None = None
    join_side: str | None = None
    join_index: int | None = None
    joined_table_alias: str | None = None
    table1: JoinTable | None = None
    table2: JoinTable | None = None


@dataclass
class InsertValue:
    """Represents a single value in an INSERT VALUES row.

    is_quoted: True  → value was wrapped in quotes in the original SQL (string literal).
    is_quoted: False → value was a bare literal (number, NULL, expression, etc.).
    """
    value: str | None = None
    is_quoted: bool = False


@dataclass
class ConflictAction:
    action_type: str | None = None  # "UPDATE", "IGNORE", etc.
    update_columns: list | None = None


@dataclass
class SetElement:
    column: Ident3 | None = None
    value: str | None = None


@dataclass
class TableRow:
    check: bool = True
    function: str | None = None
    column: str | None = None
    alias: str | None = None
    object: str | None = None
    object_parent: str | None = None
    full_name: str | None = None
    group_index: int | None = None
    type_of_row: str | None = None  # 'field' | 'expr'


# Backwards compatible alias (external code/tests may still import/expect tableRow)
tableRow = TableRow


@dataclass
class DynamicElement:
    field: str | None = None
    table: str | None = None
    schema: str | None = None
    filters: list = dc_field(default_factory=list)
    type_of_row: str | None = None  # 'field' | 'expr'

# Backwards compatible alias
dynamicElement = DynamicElement


@dataclass
class GroupElement:
    """A group of DynamicElements connected by AND (one OR-block).

    The WHERE clause is split into GroupElements at every top-level OR operator.
    Everything between two ORs (connected by AND) lives in the same GroupElement.
    """
    elements: list = dc_field(default_factory=list)  # list[DynamicElement]


# ======================================================================================================================
# Shared module-level helpers
# ======================================================================================================================

def get_leafs(in_operator):
    res = []
    if in_operator:
        if isinstance(in_operator, str):
            res.append(in_operator)
        elif in_operator.get("is_leaf"):
            res.append(in_operator)
        else:
            if in_operator.get("lhs"):
                res.extend(get_leafs(in_operator["lhs"]))
            if in_operator.get("rhs"):
                res.extend(get_leafs(in_operator["rhs"]))
    return res


def _to_sql(obj: Any, dialect=None, pretty: bool = False) -> str:
    """Render a sqlglot node (or SQL string) back to SQL text."""
    if obj is None:
        return ""
    if isinstance(obj, str):
        return sqlglot.parse_one(obj).sql(dialect=dialect, pretty=pretty)
    return obj.sql(dialect=dialect, pretty=pretty)


def _process_operator_type(node) -> str:
    """Map a sqlglot expression node to its operator string."""
    if isinstance(node, sqlglot.exp.EQ):       return "="
    if isinstance(node, sqlglot.exp.NEQ):      return "<>"
    if isinstance(node, sqlglot.exp.GT):       return ">"
    if isinstance(node, sqlglot.exp.LT):       return "<"
    if isinstance(node, sqlglot.exp.GTE):      return ">="
    if isinstance(node, sqlglot.exp.LTE):      return "<="
    if isinstance(node, sqlglot.exp.And):      return "AND"
    if isinstance(node, sqlglot.exp.Or):       return "OR"
    if isinstance(node, sqlglot.exp.Not):      return "NOT"
    if isinstance(node, sqlglot.exp.Exists):   return "EXISTS"
    if isinstance(node, sqlglot.exp.Like):     return "LIKE"
    if isinstance(node, sqlglot.exp.ILike):    return "ILIKE"
    if isinstance(node, sqlglot.exp.Is):       return "IS"
    if isinstance(node, sqlglot.exp.In):       return "IN"
    if isinstance(node, sqlglot.exp.Between):  return "BETWEEN"
    if hasattr(sqlglot.exp, "IsNot")      and isinstance(node, sqlglot.exp.IsNot):      return "IS NOT"
    if hasattr(sqlglot.exp, "NotIn")      and isinstance(node, sqlglot.exp.NotIn):      return "NOT IN"
    if hasattr(sqlglot.exp, "NotBetween") and isinstance(node, sqlglot.exp.NotBetween): return "NOT BETWEEN"
    if hasattr(sqlglot.exp, "Any")        and isinstance(node, sqlglot.exp.Any):        return "ANY"
    if hasattr(sqlglot.exp, "All")        and isinstance(node, sqlglot.exp.All):        return "ALL"
    if hasattr(sqlglot.exp, "RegexpLike") and isinstance(node, sqlglot.exp.RegexpLike): return "REGEXP"
    if hasattr(sqlglot.exp, "Regexp")     and isinstance(node, sqlglot.exp.Regexp):     return "REGEXP"
    if hasattr(sqlglot.exp, "RLike")      and isinstance(node, sqlglot.exp.RLike):      return "RLIKE"
    if hasattr(sqlglot.exp, "SimilarTo")  and isinstance(node, sqlglot.exp.SimilarTo):  return "SIMILAR TO"
    if hasattr(sqlglot.exp, "Glob")       and isinstance(node, sqlglot.exp.Glob):       return "GLOB"
    if hasattr(sqlglot.exp, "IsNull")     and isinstance(node, sqlglot.exp.IsNull):     return "IS NULL"
    if hasattr(sqlglot.exp, "IsNotNull")  and isinstance(node, sqlglot.exp.IsNotNull):  return "IS NOT NULL"
    if hasattr(sqlglot.exp, "NullSafeEQ") and isinstance(node, sqlglot.exp.NullSafeEQ): return "<=>"
    return "unknown_operator"


def _get_table_column_alias(node) -> list:
    """Return [schema, table, column] for a Column node, or [] otherwise."""
    if not isinstance(node, sqlglot.exp.Column):
        return []
    schema = node.args.get("db").alias_or_name    if node.args.get("db")    else None
    table  = node.args.get("table").alias_or_name if node.args.get("table") else None
    column = node.alias_or_name
    return [schema or None, table or None, column]


def _process_handside(node) -> Any:
    """Convert a predicate operand node to a compact representation."""
    if isinstance(node, sqlglot.exp.Column):
        return _get_table_column_alias(node)
    if isinstance(node, sqlglot.exp.Literal):
        return node.sql()
    return _to_sql(node)


def _unwrap_paren(node):
    while isinstance(node, sqlglot.exp.Paren):
        node = node.this
    return node


def _literal_int(node) -> int | None:
    node = _unwrap_paren(node)
    if not isinstance(node, sqlglot.exp.Literal):
        return None
    try:
        return int(node.this)
    except (TypeError, ValueError):
        return None


def _null_predicate_target(node):
    node = _unwrap_paren(node)

    if isinstance(node, sqlglot.exp.Is) and isinstance(node.args.get("expression"), sqlglot.exp.Null):
        return node.args.get("this"), False

    if isinstance(node, sqlglot.exp.Not):
        inner = _unwrap_paren(node.this)
        if isinstance(inner, sqlglot.exp.Is) and isinstance(inner.args.get("expression"), sqlglot.exp.Null):
            return inner.args.get("this"), True

    return None, None


def _build_operator_tree(in_operator, parent_op_type=None, double_parent_op_type=None) -> dict | None:
    """Recursively build an operator-tree dict from a sqlglot WHERE/HAVING/ON expression.

    Returned dict shape:
      {double_parent_op, parent_op, is_leaf, lhs, mid, rhs}
    Leaf nodes additionally carry: rhs_sql.
    """
    if in_operator is None:
        return None

    if isinstance(in_operator, sqlglot.exp.Paren):
        return _build_operator_tree(in_operator.args.get("this"), parent_op_type, double_parent_op_type)

    if isinstance(in_operator, sqlglot.exp.Not):
        operand_val = _build_operator_tree(in_operator.args.get("this"), None, parent_op_type)
        return {
            "double_parent_op": parent_op_type,
            "parent_op": "NOT",
            "is_leaf": False,
            "lhs": None,
            "mid": "NOT",
            "rhs": operand_val,
        }

    if isinstance(in_operator, sqlglot.exp.Exists):
        operand_val = "(" + in_operator.args.get("this").sql() + ") "
        return {
            "double_parent_op": parent_op_type,
            "parent_op": "EXISTS",
            "is_leaf": True,
            "lhs": None,
            "mid": "EXISTS",
            "rhs": operand_val,
        }

    lhs = in_operator.args.get("this")
    rhs = in_operator.args.get("expression")
    if rhs is None and isinstance(in_operator, sqlglot.exp.In):
        exprs = in_operator.args.get("expressions")
        rhs = exprs if exprs is not None else in_operator.args.get("query")

    is_leaf = not isinstance(in_operator, (sqlglot.exp.And, sqlglot.exp.Or))
    mid_val = _process_operator_type(in_operator)

    if isinstance(lhs, (sqlglot.exp.Binary, sqlglot.exp.Not, sqlglot.exp.Paren, sqlglot.exp.Exists)) and not is_leaf:
        lhs_val = _build_operator_tree(lhs, mid_val, parent_op_type)
    else:
        lhs_val = _process_handside(lhs)

    if rhs is None:
        rhs_val = None
    elif isinstance(rhs, (sqlglot.exp.Binary, sqlglot.exp.Not, sqlglot.exp.Paren, sqlglot.exp.Exists)) and not is_leaf:
        rhs_val = _build_operator_tree(rhs, mid_val, parent_op_type)
    else:
        rhs_val = [_process_handside(x) for x in rhs if x is not None] if isinstance(rhs, list) else _process_handside(rhs)

    node_dict = {
        "double_parent_op": double_parent_op_type,
        "parent_op": parent_op_type,
        "is_leaf": is_leaf,
        "lhs": lhs_val,
        "mid": mid_val,
        "rhs": rhs_val,
    }

    if is_leaf:
        try:
            if isinstance(in_operator, sqlglot.exp.In):
                exprs = in_operator.args.get("expressions")
                if exprs is not None:
                    node_dict["rhs_sql"] = f"({', '.join(_to_sql(e) for e in exprs)})"
                else:
                    q = in_operator.args.get("query")
                    node_dict["rhs_sql"] = f"({_to_sql(q)})" if q is not None else None
            else:
                rexpr = in_operator.args.get("expression")
                node_dict["rhs_sql"] = _to_sql(rexpr) if rexpr is not None else None
        except Exception:
            node_dict["rhs_sql"] = None

    return node_dict


# ======================================================================================================================
# Boolean Algebra / DNF normalization helpers
# ======================================================================================================================

def _make_and_node(lhs: dict, rhs: dict) -> dict:
    """Create a synthetic AND operator-tree node."""
    return {"double_parent_op": None, "parent_op": None, "is_leaf": False, "lhs": lhs, "mid": "AND", "rhs": rhs}


def _make_or_node(lhs: dict, rhs: dict) -> dict:
    """Create a synthetic OR operator-tree node."""
    return {"double_parent_op": None, "parent_op": None, "is_leaf": False, "lhs": lhs, "mid": "OR", "rhs": rhs}


def _negate_leaf(leaf: dict) -> dict:
    """Return a copy of leaf with its negation flag toggled.

    double-NOT cancels out; otherwise sets parent_op='NOT'.
    """
    import copy as _copy
    new = _copy.deepcopy(leaf)
    if new.get("parent_op") == "NOT":
        new["parent_op"] = None
        new["double_parent_op"] = None
    else:
        new["double_parent_op"] = new.get("parent_op")
        new["parent_op"] = "NOT"
    return new


def _push_not_down(tree: dict) -> dict:
    """Push NOT operators down to leaf level using De Morgan's laws.

      NOT(NOT X)   => X
      NOT(A AND B) => NOT(A) OR  NOT(B)
      NOT(A OR  B) => NOT(A) AND NOT(B)
      NOT(leaf)    => leaf with parent_op='NOT'
    """
    if tree is None:
        return tree
    if tree.get("is_leaf"):
        return tree

    mid = (tree.get("mid") or "").upper()

    if mid == "NOT":
        operand = tree.get("rhs")
        if operand is None:
            return tree
        if operand.get("is_leaf"):
            return _negate_leaf(operand)
        inner_mid = (operand.get("mid") or "").upper()
        if inner_mid == "NOT":
            return _push_not_down(operand.get("rhs"))
        if inner_mid == "AND":
            new_lhs = _push_not_down({"double_parent_op": None, "parent_op": "NOT", "is_leaf": False, "lhs": None, "mid": "NOT", "rhs": operand.get("lhs")})
            new_rhs = _push_not_down({"double_parent_op": None, "parent_op": "NOT", "is_leaf": False, "lhs": None, "mid": "NOT", "rhs": operand.get("rhs")})
            return _make_or_node(new_lhs, new_rhs)
        if inner_mid == "OR":
            new_lhs = _push_not_down({"double_parent_op": None, "parent_op": "NOT", "is_leaf": False, "lhs": None, "mid": "NOT", "rhs": operand.get("lhs")})
            new_rhs = _push_not_down({"double_parent_op": None, "parent_op": "NOT", "is_leaf": False, "lhs": None, "mid": "NOT", "rhs": operand.get("rhs")})
            return _make_and_node(new_lhs, new_rhs)
        return tree

    import copy as _copy
    result = _copy.copy(tree)
    result["lhs"] = _push_not_down(tree.get("lhs"))
    result["rhs"] = _push_not_down(tree.get("rhs"))
    return result


def _distribute_and_over_or(tree: dict) -> dict:
    """Apply distributivity: A AND (B OR C) => (A AND B) OR (A AND C)."""
    if tree is None:
        return tree
    if tree.get("is_leaf"):
        return tree

    mid = (tree.get("mid") or "").upper()
    if mid == "NOT":
        return tree  # after _push_not_down, NOT only wraps leaves

    import copy as _copy
    lhs = _distribute_and_over_or(tree.get("lhs"))
    rhs = _distribute_and_over_or(tree.get("rhs"))

    if mid == "OR":
        result = _copy.copy(tree)
        result["lhs"] = lhs
        result["rhs"] = rhs
        return result

    if mid == "AND":
        lhs_mid = (lhs.get("mid") or "").upper() if lhs and not lhs.get("is_leaf") else ""
        rhs_mid = (rhs.get("mid") or "").upper() if rhs and not rhs.get("is_leaf") else ""
        if rhs_mid == "OR":
            return _make_or_node(
                _distribute_and_over_or(_make_and_node(lhs, rhs.get("lhs"))),
                _distribute_and_over_or(_make_and_node(lhs, rhs.get("rhs"))),
            )
        if lhs_mid == "OR":
            return _make_or_node(
                _distribute_and_over_or(_make_and_node(lhs.get("lhs"), rhs)),
                _distribute_and_over_or(_make_and_node(lhs.get("rhs"), rhs)),
            )

    result = _copy.copy(tree)
    result["lhs"] = lhs
    result["rhs"] = rhs
    return result


def normalize_to_dnf(tree: dict, _max_iterations: int = 64) -> dict:
    """Normalize an operator-tree to Disjunctive Normal Form (DNF).

    Applies repeatedly until stable:
      1. Push NOT down using De Morgan's laws:
         - NOT(A AND B) => NOT(A) OR  NOT(B)
         - NOT(A OR  B) => NOT(A) AND NOT(B)
         - NOT(NOT X)   => X
      2. Distribute AND over OR:
         - A AND (B OR C) => (A AND B) OR (A AND C)
         - (A OR B) AND C => (A AND C) OR (B AND C)

    Examples:
      NOT(a > 1 AND b < 2)  =>  NOT(a > 1) OR  NOT(b < 2)
      NOT(a = 1 OR  b = 2)  =>  NOT(a = 1) AND NOT(b = 2)
      a AND (b OR c)         =>  (a AND b)  OR  (a AND c)
      NOT(a AND (b OR c))    =>  NOT(a) OR  (NOT(b) AND NOT(c))
    """
    if tree is None:
        return tree

    import copy as _copy

    def _repr(t):
        if t is None:
            return "None"
        if t.get("is_leaf"):
            return f"LEAF({t.get('mid')},{t.get('parent_op')})"
        return f"({t.get('mid')}:{_repr(t.get('lhs'))}:{_repr(t.get('rhs'))})"

    current = _copy.deepcopy(tree)
    for _ in range(_max_iterations):
        step = _distribute_and_over_or(_push_not_down(current))
        if _repr(step) == _repr(current):
            return step
        current = step
    return current


# ======================================================================================================================
# MyInsert
# ======================================================================================================================
class MyInsert:
    """
    - args['tables']      tracks (schema, table, alias)
    - args['into']        is dict {alias, table: Ident2}
    - args['columns']     is list[str]
    - args['values']      is list[list[InsertValue]]  (each inner list = one VALUES row)
    - args['select']      is MySelect().args dict when INSERT … SELECT
    - args['on_conflict'] is ConflictAction | None
    """

    def __init__(self, *columns: str):
        self.columns = columns
        self.args: dict[str, Any] = {
            "tables": [],
            "into": {"alias": None, "table": None},
            "columns": [],
            "values": [],
            "select": None,
            "on_conflict": None,
        }

    def parse_glot_tree(self, parsed):
        if isinstance(parsed, sqlglot.exp.Insert):
            self.args["into"] = self.get_into(parsed)
            self.args["columns"] = self.get_columns(parsed)
            self.args["values"] = self.get_values(parsed)
            self.args["select"] = self.get_select(parsed)
            self.args["on_conflict"] = self.get_on_conflict(parsed)
        return self.args

    def process_into_table(self, node):
        if node is None:
            return [None, None]
        if isinstance(node, sqlglot.exp.Schema):
            node = node.this
        if isinstance(node, sqlglot.exp.Table):
            alias = node.alias or None
            ident2 = Ident2(table=node.name, schema=node.db or None)
            self.args["tables"].append((ident2.schema, ident2.table, alias))
            return [alias, ident2]
        return [None, None]

    def get_into(self, parsed):
        if parsed.this:
            alias, ident2 = self.process_into_table(parsed.this)
            return {"alias": alias, "table": ident2}
        return {"alias": None, "table": None}

    def get_columns(self, parsed):
        result: list[str] = []
        schema = parsed.this
        if isinstance(schema, sqlglot.exp.Schema):
            for col in (schema.args.get("expressions") or []):
                if hasattr(col, "name"):
                    result.append(col.name)
                elif hasattr(col, "alias_or_name"):
                    result.append(col.alias_or_name)
        return result

    def get_values(self, parsed):
        result: list[list[InsertValue]] = []
        if parsed.expression and isinstance(parsed.expression, sqlglot.exp.Values):
            for row in parsed.expression.expressions:
                row_values: list[InsertValue] = []
                exprs = row.expressions if isinstance(row, sqlglot.exp.Tuple) else [row]
                for value in exprs:
                    if isinstance(value, sqlglot.exp.Literal):
                        row_values.append(InsertValue(value=value.this, is_quoted=value.is_string))
                    else:
                        row_values.append(InsertValue(value=_to_sql(value), is_quoted=False))
                result.append(row_values)
        return result

    def get_select(self, parsed):
        if parsed.expression and isinstance(parsed.expression, sqlglot.exp.Select):
            return MySelect().parse_glot_tree(parsed.expression)
        return None

    def get_on_conflict(self, parsed):
        conflict = parsed.args.get("conflict")
        if conflict is None:
            return None
        ca = ConflictAction()
        action_node = conflict.args.get("action")
        if action_node is not None:
            kind = getattr(action_node, "args", {}).get("kind")
            ca.action_type = str(kind).upper() if kind else action_node.__class__.__name__.upper()
            updates = action_node.args.get("expressions") or []
            ca.update_columns = []
            for upd in updates:
                if isinstance(upd, sqlglot.exp.EQ):
                    lhs = upd.args.get("this")
                    if isinstance(lhs, sqlglot.exp.Column):
                        ca.update_columns.append(lhs.name)
        return ca


# ======================================================================================================================
# MyDelete
# ======================================================================================================================
class MyDelete:
    """
    - args['tables'] tracks (schema, table, alias)
    - args['from']   is dict {alias, table: Ident2}
    - args['where']  is operator tree (dict)
    """

    def __init__(self, *columns: str):
        self.columns = columns
        self.args: dict[str, Any] = {
            "tables": [],
            "from": {"alias": None, "table": None},
            "where": None,
        }

    def parse_glot_tree(self, parsed):
        if isinstance(parsed, sqlglot.exp.Delete):
            self.args["from"] = self.get_from(parsed)
            self.args["where"] = self.get_where(parsed)
        return self.args

    def process_from_table(self, node):
        if node is None:
            return [None, None]
        if isinstance(node, sqlglot.exp.From):
            node = node.this
        if isinstance(node, sqlglot.exp.Table):
            alias = node.alias or None
            ident2 = Ident2(table=node.name, schema=node.db or None)
            self.args["tables"].append((ident2.schema, ident2.table, alias))
            return [alias, ident2]
        return [None, None]

    def get_from(self, parsed):
        if parsed.this:
            alias, ident2 = self.process_from_table(parsed.this)
            return {"alias": alias, "table": ident2}
        return {"alias": None, "table": None}

    def get_where(self, parsed):
        where = parsed.args.get("where")
        return _build_operator_tree(where.args.get("this")) if where else None


# ======================================================================================================================
# MyUpdate
# ======================================================================================================================
class MyUpdate:
    """
    - args['tables'] tracks (schema, table, alias)
    - args['table']  is dict {alias, table: Ident2}
    - args['set']    is list[SetElement]
    - args['from']   is dict {alias, table: Ident2}
    - args['where']  is operator tree (dict)
    """

    def __init__(self, *columns: str):
        self.columns = columns
        self.args: dict[str, Any] = {
            "tables": [],
            "table": None,
            "set": [],
            "from": None,
            "where": None,
        }

    def parse_glot_tree(self, parsed):
        if isinstance(parsed, sqlglot.exp.Update):
            self.args["table"] = self.get_table(parsed)
            self.args["set"] = self.get_set(parsed)
            self.args["from"] = self.get_from(parsed)
            self.args["where"] = self.get_where(parsed)
        return self.args

    def process_from_table(self, node):
        if node is None:
            return [None, None]
        if isinstance(node, sqlglot.exp.From):
            node = node.this
        if isinstance(node, sqlglot.exp.Table):
            alias = node.alias or None
            ident2 = Ident2(table=node.name, schema=node.db or None)
            self.args["tables"].append((ident2.schema, ident2.table, alias))
            return [alias, ident2]
        return [getattr(node, "alias", None), Ident2(table=getattr(node, "name", None), schema=getattr(node, "db", None) or None)]

    def get_table(self, parsed):
        if parsed.this:
            alias, ident2 = self.process_from_table(parsed.this)
            return {"alias": alias, "table": ident2}
        return {"alias": None, "table": None}

    def get_set(self, parsed):
        res: list[SetElement] = []
        for expr in (parsed.expressions or []):
            if not isinstance(expr, sqlglot.exp.EQ):
                continue
            se = SetElement()
            lhs = expr.args.get("this")
            rhs = expr.args.get("expression")
            if isinstance(lhs, sqlglot.exp.Column):
                se.column = Ident3(
                    column=lhs.name,
                    table=lhs.args.get("table").alias_or_name if lhs.args.get("table") else None,
                    schema=lhs.args.get("db").alias_or_name if lhs.args.get("db") else None,
                )
            se.value = _to_sql(rhs)
            res.append(se)
        return res

    def get_from(self, parsed):
        fr = parsed.args.get("from_")
        if fr:
            alias, ident2 = self.process_from_table(fr)
            return {"alias": alias, "table": ident2}
        return {"alias": None, "table": None}

    def get_where(self, parsed):
        where = parsed.args.get("where")
        return _build_operator_tree(where.args.get("this")) if where else None


# ======================================================================================================================
# MySelect
# ======================================================================================================================
class MySelect:
    """Light wrapper around sqlglot Select AST.

    Produces a dict-like structure consumed by PackedSelect.
    """

    def __init__(self, *columns: str):
        self.columns = columns
        self._source_sql = ""
        self.args: dict[str, Any] = {
            "tables": [],
            "distinct": False,
            "selected_names": None,
            "from": None,
            "join": [],
            "where": None,
            "group": [],
            "having": [],
            "order": [],
            "limit": 0,
            "offset": 0,
        }

    def parse_glot_tree(self, parsed, source_sql: str = ""):
        self._source_sql = source_sql or ""
        if isinstance(parsed, sqlglot.exp.Select):
            self.args["distinct"] = self.get_distinct(parsed)
            self.args["selected_names"] = self.get_names(parsed)
            self.args["from"] = self.get_from(parsed)
            self.args["join"] = self.get_join(parsed)
            self.args["where"] = self.get_where(parsed)
            self.args["group"] = self.get_group(parsed)
            self.args["having"] = self.get_having(parsed)
            self.args["order"] = self.get_order(parsed)
            self.args["limit"] = self.get_limit(parsed)
            self.args["offset"] = self.get_offset(parsed)
        return self.args

    def to_sql(self, obj: Any, dialect=None, pretty: bool = False) -> str:
        """Render a sqlglot node (or SQL string) back into SQL."""
        if obj is None:
            return ""
        if isinstance(obj, str):
            return sqlglot.parse_one(obj).sql(dialect=dialect, pretty=pretty)
        return obj.sql(dialect=dialect, pretty=pretty)

    def process_select_element(self, node):
        result = SelectedElement()
        result.alias = self.process_element_alias(node)
        if isinstance(node, sqlglot.exp.Alias):
            node = node.this
        if isinstance(node, sqlglot.exp.Column):
            result.column = self.process_element_column(node)
            result.expr = None
        elif isinstance(node, sqlglot.exp.Func):
            result.column = self.process_element_function(node)
            result.expr = self.to_sql(node)
        else:
            result.column = None
            result.expr = self.to_sql(node)
        return result

    def process_element_function(self, node: sqlglot.exp.Func) -> Ident3:
        """Convert a function expression like AVG(tbl1.fld3) into Ident3."""
        result = Ident3()
        arg = node.args.get("this")
        if arg is None:
            exprs = node.args.get("expressions")
            if exprs and len(exprs) == 1:
                arg = exprs[0]
        if isinstance(arg, sqlglot.exp.Column):
            result.column = arg.name
            result.table = arg.args.get("table").alias_or_name if arg.args.get("table") else None
            result.schema = arg.args.get("db").alias_or_name if arg.args.get("db") else None
        elif isinstance(arg, sqlglot.exp.Star):
            result.column = "*"
        elif arg is not None:
            result.column = self.to_sql(arg)
        if result.column and result.column.upper().startswith("DISTINCT "):
            result.column = result.column[9:].strip()
        return result

    def process_element_alias(self, node):
        return node.alias_or_name if isinstance(node, sqlglot.exp.Alias) else None

    def process_element_column(self, node):
        result = Ident3()
        if isinstance(node, sqlglot.exp.Column):
            result.column = node.name
            result.table = node.args.get("table").alias_or_name if node.args.get("table") else None
            result.schema = node.args.get("db").alias_or_name if node.args.get("db") else None
        return result

    def process_from_table(self, node):
        if isinstance(node, sqlglot.exp.From):
            node = node.this
        if isinstance(node, sqlglot.exp.Table):
            alias = node.alias
            ident2 = Ident2(table=node.name, schema=node.db or None)
            self.args["tables"].append((ident2.schema, ident2.table, alias))
            return [alias, ident2]
        return [None, None]

    def process_join_element(self, join):
        """Parse a sqlglot JOIN into JoinElement."""
        result = JoinElement(table1=JoinTable(), table2=JoinTable())
        if not isinstance(join, sqlglot.exp.Join):
            return result

        result.join_type = join.args.get("kind")
        result.join_side = join.args.get("side")
        result.join_index = self.process_join_index(result.join_type, result.join_side)

        join_table = join.this
        if isinstance(join_table, sqlglot.exp.Table):
            result.table1.ident = Ident2(table=join_table.name, schema=join_table.db or None)
            result.joined_table_alias = join_table.alias or None
            self.args["tables"].append(
                (result.table1.ident.schema, result.table1.ident.table, result.joined_table_alias)
            )

        on_expr = _build_operator_tree(join.args.get("on"))
        leafs = get_leafs(on_expr)
        eq_leafs = [
            lf for lf in (leafs or [])
            if isinstance(lf, dict) and lf.get("is_leaf") and lf.get("mid") == "="
        ]
        if not eq_leafs:
            return result

        def _matches_joined(table: str | None, schema: str | None) -> bool:
            if not table or not result.table1.ident:
                return False
            if result.joined_table_alias and table == result.joined_table_alias:
                return True
            if result.table1.ident.table and table != result.table1.ident.table:
                return False
            if result.table1.ident.schema and schema and schema != result.table1.ident.schema:
                return False
            return True

        for lf in eq_leafs:
            lhs = lf.get("lhs")
            rhs = lf.get("rhs")
            if not (isinstance(lhs, list) and len(lhs) == 3 and isinstance(rhs, list) and len(rhs) == 3):
                continue
            l_schema, l_table, l_col = lhs
            r_schema, r_table, r_col = rhs
            lhs_is_joined = _matches_joined(l_table, l_schema)
            rhs_is_joined = _matches_joined(r_table, r_schema)
            if lhs_is_joined == rhs_is_joined:
                continue
            if lhs_is_joined:
                if result.table2.ident is None:
                    result.table2.ident = Ident2(table=r_table, schema=r_schema)
                if l_col and l_col not in result.table1.columns:
                    result.table1.columns.append(l_col)
                if r_col and r_col not in result.table2.columns:
                    result.table2.columns.append(r_col)
            else:
                if result.table2.ident is None:
                    result.table2.ident = Ident2(table=l_table, schema=l_schema)
                if r_col and r_col not in result.table1.columns:
                    result.table1.columns.append(r_col)
                if l_col and l_col not in result.table2.columns:
                    result.table2.columns.append(l_col)

        return result

    def process_join_index(self, join_type, join_side):
        if (join_type is None and join_side is None) or (join_type == "INNER" and join_side is None):
            return 0
        if (join_type is None and join_side == "LEFT") or (join_type == "OUTER" and join_side == "LEFT"):
            return 1
        if (join_type is None and join_side == "RIGHT") or (join_type == "OUTER" and join_side == "RIGHT"):
            return 2
        if (join_type is None and join_side == "FULL") or (join_type == "OUTER" and join_side == "FULL"):
            return 3
        if join_type == "CROSS" and join_side is None:
            return 4
        return None

    def get_distinct(self, parsed):
        return bool(parsed.args.get("distinct"))

    def get_names(self, parsed):
        return [self.process_select_element(element) for element in parsed.selects]

    def get_from(self, parsed):
        if parsed.from_:
            from_data = self.process_from_table(parsed.args.get("from_"))
            return {"alias": from_data[0], "table": from_data[1]}
        return {"alias": None, "table": None}

    def get_join(self, parsed):
        joins = parsed.args.get("joins")
        if not joins:
            return []
        return [self.process_join_element(j) for j in joins]

    def get_where(self, parsed):
        where = parsed.args.get("where")
        return _build_operator_tree(where.args.get("this")) if where else None

    def get_having(self, parsed):
        having = parsed.args.get("having")
        return _build_operator_tree(having.args.get("this")) if having else None

    def _explicit_nulls_first(self, ordered: sqlglot.exp.Ordered) -> bool | None:
        return None

    def _extract_order_item_sqls(self) -> list[str]:
        sql = self._source_sql
        if not sql:
            return []

        order_match = re.search(r"\bORDER\s+BY\b", sql, flags=re.IGNORECASE)
        if not order_match:
            return []

        i = order_match.end()
        depth = 0
        quote = None
        end = len(sql)
        tail_keyword = re.compile(r"\b(LIMIT|OFFSET|FETCH|UNION|EXCEPT|INTERSECT|QUALIFY)\b", flags=re.IGNORECASE)

        while i < len(sql):
            ch = sql[i]

            if quote:
                if ch == quote:
                    quote = None
                i += 1
                continue

            if ch in ("'", '"', "`"):
                quote = ch
                i += 1
                continue

            if ch == "(":
                depth += 1
                i += 1
                continue

            if ch == ")":
                depth = max(0, depth - 1)
                i += 1
                continue

            if depth == 0:
                match = tail_keyword.match(sql, i)
                if match:
                    end = i
                    break

            i += 1

        clause = sql[order_match.end():end].strip()
        if not clause:
            return []

        items: list[str] = []
        start = 0
        depth = 0
        quote = None
        for i, ch in enumerate(clause):
            if quote:
                if ch == quote:
                    quote = None
                continue

            if ch in ("'", '"', "`"):
                quote = ch
                continue

            if ch == "(":
                depth += 1
                continue

            if ch == ")":
                depth = max(0, depth - 1)
                continue

            if ch == "," and depth == 0:
                items.append(clause[start:i].strip())
                start = i + 1

        items.append(clause[start:].strip())
        return items

    def _explicit_nulls_first_from_sql(self, order_item_sql: str) -> bool | None:
        sql_upper = re.sub(r"\s+", " ", order_item_sql.strip()).upper()
        if "NULLS FIRST" in sql_upper:
            return True
        if "NULLS LAST" in sql_upper:
            return False
        return None

    def _nulls_helper_info(self, ordered: sqlglot.exp.Ordered) -> tuple[str, bool] | None:
        helper_expr = _unwrap_paren(ordered.this)
        desc = bool(ordered.args.get("desc"))

        target_expr, is_not_null = _null_predicate_target(helper_expr)
        if target_expr is not None:
            nulls_first = (not desc) if is_not_null else desc
            return self._sql_norm(target_expr), nulls_first

        if isinstance(helper_expr, sqlglot.exp.Case):
            ifs = helper_expr.args.get("ifs") or []
            if len(ifs) != 1:
                return None

            branch = ifs[0]
            target_expr, is_not_null = _null_predicate_target(branch.args.get("this"))
            if target_expr is None or is_not_null:
                return None

            null_rank = _literal_int(branch.args.get("true"))
            non_null_rank = _literal_int(helper_expr.args.get("default"))
            if null_rank is None or non_null_rank is None or null_rank == non_null_rank:
                return None

            nulls_first = null_rank > non_null_rank if desc else null_rank < non_null_rank
            return self._sql_norm(target_expr), nulls_first

        return None

    def get_order(self, parsed):
        raw_items = []
        raw_order_sqls = self._extract_order_item_sqls()
        order = parsed.args.get("order")
        if not order:
            return raw_items

        for index, expr in enumerate(order.expressions):
            if not isinstance(expr, sqlglot.exp.Ordered):
                continue
            ob_expr = expr.this
            ob_sql_norm = self._sql_norm(ob_expr)
            mapped_index = None
            mapped_name = None

            for sel_idx, sel in enumerate(self.args.get("selected_names") or []):
                if not isinstance(sel, SelectedElement):
                    continue
                if sel.alias and self._sql_norm(sel.alias) == ob_sql_norm:
                    mapped_index = sel_idx + 1
                    mapped_name = sel.alias
                    break
                if sel.column and sel.column.column:
                    sel_col_fq = self._col_ref_sql_norm(sel.column)
                    if sel_col_fq and sel_col_fq == ob_sql_norm:
                        mapped_index = sel_idx + 1
                        mapped_name = sel.alias or sel.column.column
                        break
                    if self._sql_norm(sel.column.column) == ob_sql_norm:
                        mapped_index = sel_idx + 1
                        mapped_name = sel.alias or sel.column.column
                        break
                if sel.expr and self._sql_norm(sel.expr) == ob_sql_norm:
                    mapped_index = sel_idx + 1
                    mapped_name = sel.alias or sel.expr
                    break

            if mapped_name is None and isinstance(ob_expr, sqlglot.exp.Column):
                mapped_name = ob_expr.alias_or_name

            raw_item_sql = raw_order_sqls[index] if index < len(raw_order_sqls) else self.to_sql(expr)

            raw_items.append({
                "index": index + 1,
                "mapped_index": mapped_index,
                "column_name": mapped_name or ob_expr.alias_or_name,
                "expr": self.to_sql(ob_expr),
                "desc": bool(expr.args.get("desc")),
                "nulls_first": self._explicit_nulls_first_from_sql(raw_item_sql),
                "_ordered": expr,
                "_expr_norm": ob_sql_norm,
            })

        result = []
        skip_indexes = set()
        for idx, item in enumerate(raw_items):
            if idx in skip_indexes:
                continue

            helper_info = self._nulls_helper_info(item["_ordered"])
            if helper_info and idx + 1 < len(raw_items):
                target_norm, nulls_first = helper_info
                next_item = raw_items[idx + 1]
                if next_item["_expr_norm"] == target_norm:
                    if next_item["nulls_first"] is None:
                        next_item["nulls_first"] = nulls_first
                    skip_indexes.add(idx)
                    continue

            result.append({
                "index": item["index"],
                "mapped_index": item["mapped_index"],
                "column_name": item["column_name"],
                "expr": item["expr"],
                "desc": item["desc"],
                "nulls_first": item["nulls_first"],
            })

        return result

    def get_limit(self, parsed):
        limit = parsed.args.get("limit")
        return int(limit.expression.alias_or_name) if limit else 0

    def get_offset(self, parsed):
        offset = parsed.args.get("offset")
        return int(offset.expression.alias_or_name) if offset else 0

    def _sql_norm(self, expr) -> str:
        s = self.to_sql(expr) if not isinstance(expr, str) else expr
        s = re.sub(r"\s+", " ", (s or "").strip())
        return s.upper()

    def _col_ref_sql_norm(self, col: Ident3) -> str | None:
        if not col or not col.column:
            return None
        parts = []
        if col.schema:
            parts.append(col.schema)
        if col.table:
            parts.append(col.table)
        parts.append(col.column)
        return self._sql_norm(".".join(parts))

    def get_group(self, parsed):
        result = []
        group = parsed.args.get("group")
        if not group:
            return result

        for gexpr in group.expressions:
            g_sql = self._sql_norm(gexpr)
            mapped_index = None
            mapped_name = None

            for sel_idx, sel in enumerate(self.args.get("selected_names") or []):
                if not isinstance(sel, SelectedElement):
                    continue
                if sel.alias and self._sql_norm(sel.alias) == g_sql:
                    mapped_index = sel_idx + 1
                    mapped_name = sel.alias
                    break
                if sel.column and sel.column.column:
                    sel_col_fq = self._col_ref_sql_norm(sel.column)
                    if sel_col_fq and sel_col_fq == g_sql:
                        mapped_index = sel_idx + 1
                        mapped_name = sel.alias or sel.column.column
                        break
                    if self._sql_norm(sel.column.column) == g_sql:
                        mapped_index = sel_idx + 1
                        mapped_name = sel.alias or sel.column.column
                        break
                if sel.expr and self._sql_norm(sel.expr) == g_sql:
                    mapped_index = sel_idx + 1
                    mapped_name = sel.alias or sel.expr
                    break

            if mapped_name is None and isinstance(gexpr, sqlglot.exp.Column):
                mapped_name = gexpr.alias_or_name

            result.append({
                "index": mapped_index,
                "column_name": mapped_name or self.to_sql(gexpr),
                "expr": self.to_sql(gexpr),
            })

        return result


# ======================================================================================================================
# PackedSelect
# ======================================================================================================================
class PackedSelect:
    """Transform MySelect output into UI-friendly packed structure."""

    def __init__(self):
        self.args = {
            "aggr_funcs": [],
            "type": "SELECT",
            "distinct": False,
            "from": None,
            "rows": [],
            "join": [],
            "where": [],
            "having": [],
            "group_by": [],
            "order_by": [],
            "limit": 0,
            "offset": 0,
        }

    def _normalize_aggr_funcs(self, aggr_funcs: list | None) -> set[str]:
        if not aggr_funcs:
            return set()
        out: set[str] = set()
        for f in aggr_funcs:
            if not f:
                continue
            out.add(str(f).strip().split()[0].upper())
        return out

    def _is_distinct_aggregate(self, node) -> bool:
        if not node:
            return False
        try:
            if node.args.get("distinct"):
                return True
        except Exception:
            pass
        try:
            return next(node.find_all(sqlglot.exp.Distinct), None) is not None
        except Exception:
            return False

    def parse_glot(self, in_select):
        if isinstance(in_select, MySelect):
            self.args["aggr_funcs"] = sorted(self._normalize_aggr_funcs(self.args.get("aggr_funcs")))
            self.args["distinct"] = in_select.args["distinct"]
            self.args["from"] = in_select.args["from"]
            self.args["rows"] = self.process_rows(in_select)
            self.args["join"] = self.process_join(in_select)
            self.args["where"] = self.process_dynamic_filters(in_select.args["where"])
            self.args["group_by"] = self.process_group_by(in_select)
            self.args["having"] = self.process_dynamic_filters(in_select.args["having"])
            self.args["order_by"] = self.process_order_by(in_select)
            self.args["limit"] = in_select.args["limit"]
            self.args["offset"] = in_select.args["offset"]
        return self.args

    def _extract_row_from_expr_sql(self, expr_sql: str) -> TableRow:
        tr = TableRow()
        tr.full_name = expr_sql.strip() if expr_sql else None
        if not expr_sql:
            return tr
        try:
            node = sqlglot.parse_one(expr_sql)
        except Exception:
            tr.column = expr_sql
            tr.type_of_row = "expr"
            return tr

        if isinstance(node, sqlglot.exp.Column):
            tr.column = node.name
            tr.object = node.args.get("table").alias_or_name if node.args.get("table") else None
            tr.object_parent = node.args.get("db").alias_or_name if node.args.get("db") else None
            tr.type_of_row = "field"
            return tr

        if isinstance(node, sqlglot.exp.Star):
            tr.column = "*"
            tr.type_of_row = "field"
            return tr

        if isinstance(node, (sqlglot.exp.Func, sqlglot.exp.Anonymous)):
            func_name = node.key.upper() if node.key else None
            if func_name:
                tr.function = f"{func_name} DISTINCT" if self._is_distinct_aggregate(node) else func_name
            cols = list(node.find_all(sqlglot.exp.Column))
            if cols:
                col0 = cols[0]
                tr.column = col0.name
                tr.object = col0.args.get("table").alias_or_name if col0.args.get("table") else None
                tr.object_parent = col0.args.get("db").alias_or_name if col0.args.get("db") else None
            else:
                tr.column = "*" if isinstance(node.args.get("this"), sqlglot.exp.Star) else expr_sql
            base = (tr.function or "").split()[0].upper() if tr.function else ""
            aggr_set = self._normalize_aggr_funcs(self.args.get("aggr_funcs"))
            if base in aggr_set:
                arg = node.args.get("this")
                if arg is None:
                    exprs = node.args.get("expressions")
                    if exprs and len(exprs) == 1:
                        arg = exprs[0]
                if isinstance(arg, sqlglot.exp.Distinct):
                    d_exprs = arg.args.get("expressions")
                    if d_exprs and len(d_exprs) == 1:
                        arg = d_exprs[0]
                tr.type_of_row = "field" if isinstance(arg, (sqlglot.exp.Column, sqlglot.exp.Star)) else "expr"
            else:
                tr.type_of_row = "expr"
            return tr

        cols = list(node.find_all(sqlglot.exp.Column))
        if cols:
            col0 = cols[0]
            tr.column = col0.name
            tr.object = col0.args.get("table").alias_or_name if col0.args.get("table") else None
            tr.object_parent = col0.args.get("db").alias_or_name if col0.args.get("db") else None
        else:
            tr.column = expr_sql
        tr.type_of_row = "expr"
        return tr

    def process_rows(self, in_select):
        res = []
        aggr_whitelist = self._normalize_aggr_funcs(self.args.get("aggr_funcs"))
        for row in in_select.args.get("selected_names") or []:
            new_row = TableRow()
            new_row.alias = row.alias
            if row.column and row.expr is None:
                new_row.column = row.column.column
                new_row.object = row.column.table if row.column.table else None
                new_row.object_parent = row.column.schema if row.column.schema else None
                new_row.full_name = (
                    f"{row.column.table}.{row.column.column}" if row.column.table else row.column.column
                )
                new_row.type_of_row = "field"
            elif row.expr:
                extracted = self._extract_row_from_expr_sql(row.expr)
                new_row.function = extracted.function
                new_row.column = extracted.column
                new_row.object = extracted.object
                new_row.object_parent = extracted.object_parent
                new_row.full_name = extracted.full_name
                new_row.type_of_row = extracted.type_of_row
                if aggr_whitelist and new_row.function:
                    base = new_row.function.split()[0].upper()
                    if base not in aggr_whitelist:
                        new_row.function = None
            res.append(new_row)
        return res

    def process_join(self, in_select):
        return in_select.args.get("join") or []

    def process_group_by(self, in_select) -> list[dict[str, Any]]:
        res: list[dict[str, Any]] = []
        for idx, g in enumerate(in_select.args.get("group") or [], start=1):
            res.append({"index": idx, "mapped_index": g.get("index")})
        return res

    def _rhs_sql(self, rhs) -> str:
        if rhs is None:
            return "NULL"
        if isinstance(rhs, list):
            items = [("NULL" if x is None else str(x)) for x in rhs]
            return f"({', '.join(items)})"
        return str(rhs)

    def _format_leaf_filter_only(self, leaf) -> str:
        op = (leaf.get("mid") or "").upper()
        rhs = leaf.get("rhs_sql")
        if rhs is None:
            rhs = self._rhs_sql(leaf.get("rhs"))

        negated = leaf.get("parent_op") == "NOT"
        if negated:
            rhs_up = (rhs or "").strip().upper()
            if op == "IS" and rhs_up in {"NULL", "TRUE", "FALSE"}:
                return f"IS NOT {rhs_up}"
            if op == "IN":
                return f"NOT IN {rhs}"
            if op in {"LIKE", "ILIKE"}:
                return f"NOT {op} {rhs}"
            if op == "BETWEEN":
                return f"NOT BETWEEN {rhs}"
            return f"NOT {leaf.get('mid')} {rhs}"

        return f"{leaf.get('mid')} {rhs}"

    def _collect_or_groups(self, operator_tree) -> list[list]:
        """Split the operator tree into OR-separated groups of leaf lists.

        - OR  node  → left groups + right groups
        - AND node  → merge last of lhs with first of rhs
        - NOT(leaf) → single negated atom  (only valid after normalize_to_dnf)
        - leaf      → [[leaf]]
        """
        if operator_tree is None:
            return []
        if isinstance(operator_tree, str):
            return []
        if operator_tree.get("is_leaf"):
            return [[operator_tree]]

        mid = (operator_tree.get("mid") or "").upper()

        if mid == "NOT":
            rhs = operator_tree.get("rhs")
            if rhs is not None and rhs.get("is_leaf"):
                return [[rhs]]
            return self._collect_or_groups(rhs)

        lhs_groups = self._collect_or_groups(operator_tree.get("lhs"))
        rhs_groups = self._collect_or_groups(operator_tree.get("rhs"))

        if mid == "OR":
            return lhs_groups + rhs_groups

        if not lhs_groups and not rhs_groups:
            return []
        if not lhs_groups:
            return rhs_groups
        if not rhs_groups:
            return lhs_groups
        return lhs_groups[:-1] + [lhs_groups[-1] + rhs_groups[0]] + rhs_groups[1:]

    def _build_dynamic_element(self, leaf) -> tuple[DynamicElement, str]:
        """Build a DynamicElement + formatted filter expression from a leaf dict."""
        el = DynamicElement()
        if leaf["mid"] == "EXISTS":
            el.field = None
            el.type_of_row = "expr"
            expr = ("NOT " if leaf.get("double_parent_op") == "NOT" else "") + "EXISTS " + leaf["rhs"]
        else:
            lhs = leaf.get("lhs")
            if isinstance(lhs, str):
                el.field = lhs
                el.type_of_row = "expr"
            else:
                el.schema = lhs[0] if lhs else None
                el.table  = lhs[1] if lhs else None
                el.field  = lhs[2] if lhs else None
                el.type_of_row = "field"
            expr = self._format_leaf_filter_only(leaf)
        return el, expr

    def _pack_leaf_group(self, leaves: list) -> GroupElement:
        """Pack a list of leaves (all within one AND-block) into a GroupElement."""
        group = GroupElement()
        for leaf in leaves:
            el, expr = self._build_dynamic_element(leaf)
            for existing in group.elements:
                if existing.field == el.field and existing.table == el.table:
                    existing.filters.append(expr)
                    break
            else:
                el.filters.append(expr)
                group.elements.append(el)
        return group

    def process_dynamic_filters(self, operator_tree) -> list[GroupElement]:
        """Split WHERE/HAVING into OR-separated GroupElements.

        Before splitting, normalizes to DNF using boolean algebra:
          - NOT(A AND B)   =>  NOT(A) OR NOT(B)       -- De Morgan
          - NOT(A OR B)    =>  NOT(A) AND NOT(B)      -- De Morgan
          - A AND (B OR C) =>  (A AND B) OR (A AND C) -- distributivity
        """
        normalized = normalize_to_dnf(operator_tree)
        or_groups = self._collect_or_groups(normalized)
        return [self._pack_leaf_group(leaves) for leaves in or_groups if leaves]

    def process_order_by(self, in_select) -> list[dict[str, Any]]:
        res: list[dict[str, Any]] = []
        for idx, o in enumerate(in_select.args.get("order") or [], start=1):
            res.append({
                "index": idx,
                "mapped_index": o.get("mapped_index"),
                "desc": bool(o.get("desc")),
                "nulls_first": o.get("nulls_first"),
                })
        return res


# ======================================================================================================================
# PackedUpdate
# ======================================================================================================================
class PackedUpdate:
    """Transform MyUpdate output into a packed structure."""

    def __init__(self):
        self.args: dict[str, Any] = {
            "type": "UPDATE",
            "table": None,
            "set": [],
            "from": None,
            "where": [],
        }

    def parse_glot(self, in_update):
        if isinstance(in_update, MyUpdate):
            self.args["table"] = in_update.args["table"]
            self.args["set"] = self.process_set(in_update)
            self.args["from"] = in_update.args["from"]
            self.args["where"] = self.process_where(in_update)
        return self.args

    def process_set(self, in_update):
        res: list[dict[str, Any]] = []
        for se in in_update.args.get("set") or []:
            if isinstance(se, SetElement):
                res.append({"column": se.column, "value": se.value})
            elif isinstance(se, dict):
                res.append({"column": se.get("column"), "value": se.get("value")})
        return res

    def process_where(self, in_update):
        return PackedSelect().process_dynamic_filters(in_update.args.get("where"))


# ======================================================================================================================
# PackedDelete
# ======================================================================================================================
class PackedDelete:
    """Transform MyDelete output into a packed structure."""

    def __init__(self):
        self.args: dict[str, Any] = {
            "type": "DELETE",
            "from": None,
            "where": [],
        }

    def parse_glot(self, in_delete):
        if isinstance(in_delete, MyDelete):
            self.args["from"] = in_delete.args.get("from")
            self.args["where"] = self.process_where(in_delete)
        return self.args

    def process_where(self, in_delete):
        return PackedSelect().process_dynamic_filters(in_delete.args.get("where"))


# ======================================================================================================================
# PackedInsert
# ======================================================================================================================
class PackedInsert:
    """Transform MyInsert output into a packed structure."""

    def __init__(self):
        self.args: dict[str, Any] = {
            "type": "INSERT",
            "into": None,
            "columns": [],
            "values": [],
            "select": None,
            "on_conflict": None,
        }

    def parse_glot(self, in_insert):
        if isinstance(in_insert, MyInsert):
            self.args["into"] = in_insert.args["into"]
            self.args["columns"] = in_insert.args["columns"]
            self.args["values"] = in_insert.args["values"]
            self.args["select"] = self.process_select(in_insert)
            self.args["on_conflict"] = in_insert.args["on_conflict"]
        return self.args

    def process_select(self, in_insert):
        """If INSERT … SELECT, pack the nested SELECT via PackedSelect."""
        select_args = in_insert.args.get("select")
        if select_args is None:
            return None
        proxy = MySelect()
        proxy.args = select_args
        return PackedSelect().parse_glot(proxy)


# ======================================================================================================================
# Main SQL Parser
# ======================================================================================================================
def parse_sql(dialect, query, aggr_funcs):
    raw_glot = sqlglot.parse_one(sql=query, dialect=dialect)
    parsed_glot = None
    if isinstance(raw_glot, sqlglot.exp.Select):
        parsed_glot = MySelect()
        parsed_glot.parse_glot_tree(raw_glot, query)
    elif isinstance(raw_glot, sqlglot.exp.Update):
        parsed_glot = MyUpdate()
        parsed_glot.parse_glot_tree(raw_glot)
    elif isinstance(raw_glot, sqlglot.exp.Delete):
        parsed_glot = MyDelete()
        parsed_glot.parse_glot_tree(raw_glot)
    elif isinstance(raw_glot, sqlglot.exp.Insert):
        parsed_glot = MyInsert()
        parsed_glot.parse_glot_tree(raw_glot)

    if parsed_glot:
        if isinstance(parsed_glot, MySelect):
            result = PackedSelect()
            result.args["aggr_funcs"] = aggr_funcs
            return result.parse_glot(parsed_glot)
        if isinstance(parsed_glot, MyUpdate):
            return PackedUpdate().parse_glot(parsed_glot)
        if isinstance(parsed_glot, MyDelete):
            return PackedDelete().parse_glot(parsed_glot)
        if isinstance(parsed_glot, MyInsert):
            return PackedInsert().parse_glot(parsed_glot)

    return {"error": "Unsupported SQL operation"}
