from __future__ import annotations
from abc import ABC, abstractmethod
import datetime as dt
import math
from collections import defaultdict

from relationalai.semantics.std.datetime_format import DateFormatField, DateFormatLiteral, parse_julia_datetime_format

from . import sql_ir as sql

# SQL reserved words that must be quoted when used as identifiers
SQL_RESERVED_WORDS = frozenset({
    "add", "all", "alter", "and", "any", "as", "asc", "between", "by", "case",
    "check", "column", "constraint", "create", "cross", "current", "current_date",
    "current_time", "current_timestamp", "default", "delete", "desc", "distinct",
    "drop", "else", "end", "exists", "false", "for", "foreign", "from", "full",
    "group", "having", "if", "in", "index", "inner", "insert", "intersect", "into",
    "is", "join", "key", "left", "like", "limit", "natural", "not", "null", "offset",
    "on", "or", "order", "outer", "primary", "references", "right", "select", "set",
    "table", "then", "true", "union", "unique", "update", "using", "values", "view",
    "when", "where", "with", "to",
})

_TZ_PATTERN_KINDS = frozenset({"tz_offset", "tz_name"})
_DUCKDB_FORMAT_BY_KIND = {
    "year_2": "%y",
    "year_4": "%Y",
    "month_unpadded": "%-m",
    "month_padded": "%m",
    "day_unpadded": "%-d",
    "day_padded": "%d",
    "hour24_unpadded": "%-H",
    "hour24_padded": "%H",
    "minute_unpadded": "%-M",
    "minute_padded": "%M",
    "second_unpadded": "%-S",
    "second_padded": "%S",
    "millisecond": "%g",
    "weekday_abbr": "%a",
    "weekday_full": "%A",
    "month_abbr": "%b",
    "month_full": "%B",
    "tz_offset": "%z",
    "tz_name": "%Z",
    "hour12_unpadded": "%-I",
    "hour12_padded": "%I",
    "ampm": "%p",
}
_DATE_INVALID_FORMAT_KINDS = frozenset({
    "second_unpadded",
    "second_padded",
    "millisecond",
    "hour24_unpadded",
    "hour24_padded",
    "minute_unpadded",
    "minute_padded",
    "tz_offset",
    "tz_name",
    "hour12_unpadded",
    "hour12_padded",
    "ampm",
})


def _single_field_kind(fmt: str) -> str | None:
    parts = parse_julia_datetime_format(fmt)
    if len(parts) == 1 and isinstance(parts[0], DateFormatField):
        return parts[0].kind
    return None


def _translate_duckdb_format(fmt: str) -> tuple[str, set[str]]:
    parts = parse_julia_datetime_format(fmt)
    out: list[str] = []
    seen_kinds: set[str] = set()
    for part in parts:
        if isinstance(part, DateFormatLiteral):
            out.append(part.text.replace("%", "%%"))
            continue
        if isinstance(part, DateFormatField):
            seen_kinds.add(part.kind)
            mapped = _DUCKDB_FORMAT_BY_KIND.get(part.kind)
            if mapped is None:
                raise ValueError(f"Unsupported date format token kind {part.kind!r} in {fmt!r}")
            out.append(mapped)
            continue
        raise ValueError(f"Unknown date format part in {fmt!r}.")
    return "".join(out), seen_kinds


def _quote_sql_string(s: str) -> str:
    return "'" + s.replace("'", "''") + "'"


def _literal_int_arg(node: sql.IRNode) -> int | None:
    if not isinstance(node, sql.Literal):
        return None
    value = node.value
    if isinstance(value, (int, str)):
        return int(value)
    return None

#------------------------------------------------------
# SQLDialect
#------------------------------------------------------

class SQLDialect(ABC):
    @abstractmethod
    def quote_identifier(self, name: str) -> str:
        """Quote an identifier (table name, column name, etc.)."""
        pass

    @abstractmethod
    def format_table_name(self, name: str) -> str:
        """Format a fully qualified table name for this dialect."""
        pass

    @abstractmethod
    def emit_rowid(self, table_alias: str) -> str:
        """Emit a rowid expression for the given table."""
        pass

    def emit_expression(self, emitter: SQLEmitter, node: sql.Expression) -> str | None:
        """Emit a dialect-specific expression. Return None to use default handling."""
        return None

    def emit_literal(self, emitter: SQLEmitter, node: sql.Literal) -> str | None:
        """Emit a dialect-specific literal. Return None to use default handling."""
        return None

    def emit_table_function(self, emitter: SQLEmitter, node: sql.TableFunction) -> str | None:
        """Emit a dialect-specific table function. Return None to use default handling."""
        return None

#------------------------------------------------------
# DuckDBDialect
#------------------------------------------------------

class DuckDBDialect(SQLDialect):
    _DATE_EXTRACT_MAP = {
        "DATE_YEAR": "YEAR",
        "DATE_QUARTER": "QUARTER",
        "DATE_MONTH": "MONTH",
        "DATE_WEEK": "WEEK",
        "DATE_DAY": "DAY",
        "DATE_DAYOFYEAR": "DAYOFYEAR",
        "DATE_WEEKDAY": "ISODOW",
    }
    _DATETIME_EXTRACT_MAP = {
        "DATETIME_YEAR": "YEAR",
        "DATETIME_QUARTER": "QUARTER",
        "DATETIME_MONTH": "MONTH",
        "DATETIME_WEEK": "WEEK",
        "DATETIME_DAY": "DAY",
        "DATETIME_DAYOFYEAR": "DAYOFYEAR",
        "DATETIME_HOUR": "HOUR",
        "DATETIME_MINUTE": "MINUTE",
        "DATETIME_WEEKDAY": "ISODOW",
    }
    def quote_identifier(self, name: str) -> str:
        # escaped = name.replace('"', '""')
        # return f'"{escaped}"'
        return name

    def format_table_name(self, name: str) -> str:
        # DuckDB uses db_schema.table format
        # Convert db.schema.table -> db_schema.table
        parts = name.split('.')
        if len(parts) == 3:
            db, schema, table = parts
            return f"{db}_{schema}.{table}"
        return name

    def emit_rowid(self, table_alias: str) -> str:
        return f"{table_alias}.rowid"

    def _extract_period(self, emitter: SQLEmitter, node: sql.IRNode) -> tuple[str, str] | None:
        if not isinstance(node, sql.Expression) or len(node.args) != 1:
            return None
        op = node.op.upper()
        allowed_units = {
            "NANOSECOND",
            "MICROSECOND",
            "MILLISECOND",
            "SECOND",
            "MINUTE",
            "HOUR",
            "DAY",
            "WEEK",
            "MONTH",
            "YEAR",
        }
        if op not in allowed_units:
            return None
        return emitter._emit(node.args[0]), op

    def _apply_tz(self, base: str, tz_node: sql.IRNode | None, emitter: SQLEmitter) -> str:
        if tz_node is None:
            return base
        tz = emitter._emit(tz_node)
        return f"TIMEZONE({tz}, TIMEZONE('UTC', {base}))"

    def emit_expression(self, emitter: SQLEmitter, node: sql.Expression) -> str | None:
        op = node.op.upper()
        if op in {"DAY", "WEEK", "MONTH", "YEAR", "HOUR", "MINUTE", "SECOND", "MILLISECOND", "MICROSECOND", "NANOSECOND"} and len(node.args) == 1:
            return emitter._emit(node.args[0])
        if op in {"DATE_ADD", "DATE_SUBTRACT", "DATETIME_ADD", "DATETIME_SUBTRACT"} and len(node.args) == 2:
            base = emitter._emit(node.args[0])
            period = self._extract_period(emitter, node.args[1])
            if period is not None:
                value, unit = period
                sign = "+" if op in {"DATE_ADD", "DATETIME_ADD"} else "-"
                if unit == "NANOSECOND":
                    return f"({base} {sign} (({value}) * INTERVAL 1 MICROSECOND) / 1000)"
                return f"({base} {sign} ({value}) * INTERVAL 1 {unit})"
        if op == "DATES_PERIOD_DAYS" and len(node.args) == 2:
            start = emitter._emit(node.args[0])
            end = emitter._emit(node.args[1])
            return f"DATE_DIFF('day', {start}, {end})"
        if op == "DATETIMES_PERIOD_MILLISECONDS" and len(node.args) == 2:
            start = emitter._emit(node.args[0])
            end = emitter._emit(node.args[1])
            return f"DATE_DIFF('millisecond', {start}, {end})"
        if op in self._DATE_EXTRACT_MAP and len(node.args) == 1:
            fn = self._DATE_EXTRACT_MAP[op]
            val = emitter._emit(node.args[0])
            if fn == "ISODOW":
                return f"DATE_PART('isodow', {val})"
            return f"{fn}({val})"
        if op in self._DATETIME_EXTRACT_MAP and len(node.args) >= 1:
            fn = self._DATETIME_EXTRACT_MAP[op]
            val = emitter._emit(node.args[0])
            tz_node = node.args[1] if len(node.args) >= 2 else None
            val = self._apply_tz(val, tz_node, emitter)
            if fn == "ISODOW":
                return f"DATE_PART('isodow', {val})"
            return f"{fn}({val})"
        if op == "DATETIME_HOUR" and len(node.args) >= 1:
            base = emitter._emit(node.args[0])
            tz_node = node.args[1] if len(node.args) >= 2 else None
            base = self._apply_tz(base, tz_node, emitter)
            return f"HOUR({base})"
        if op == "DATETIME_MINUTE" and len(node.args) >= 1:
            base = emitter._emit(node.args[0])
            tz_node = node.args[1] if len(node.args) >= 2 else None
            base = self._apply_tz(base, tz_node, emitter)
            return f"MINUTE({base})"
        if op == "DATETIME_SECOND" and len(node.args) >= 1:
            return f"SECOND({emitter._emit(node.args[0])})"
        if op == "CONSTRUCT_DATETIME_MS_TZ" and len(node.args) == 8:
            year = emitter._emit(node.args[0])
            month = emitter._emit(node.args[1])
            day = emitter._emit(node.args[2])
            hour = emitter._emit(node.args[3])
            minute = emitter._emit(node.args[4])
            second = emitter._emit(node.args[5])
            millisecond = emitter._emit(node.args[6])
            tz = emitter._emit(node.args[7])
            sec_with_fraction = f"(CAST({second} AS DOUBLE) + CAST({millisecond} AS DOUBLE) / 1000.0)"
            local_ts = (
                f"MAKE_TIMESTAMP(CAST({year} AS BIGINT), CAST({month} AS BIGINT), "
                f"CAST({day} AS BIGINT), CAST({hour} AS BIGINT), CAST({minute} AS BIGINT), {sec_with_fraction})"
            )
            return f"CAST(TIMEZONE('UTC', TIMEZONE({tz}, {local_ts})) AS TIMESTAMP)"
        if op == "CONSTRUCT_DATE" and len(node.args) == 3:
            year = emitter._emit(node.args[0])
            month = emitter._emit(node.args[1])
            day = emitter._emit(node.args[2])
            return (
                f"MAKE_DATE(CAST({year} AS BIGINT), CAST({month} AS BIGINT), "
                f"CAST({day} AS BIGINT))"
            )
        if op == "CONSTRUCT_DATE_FROM_DATETIME" and len(node.args) == 2:
            datetime_expr = emitter._emit(node.args[0])
            tz = emitter._emit(node.args[1])
            return f"CAST(TIMEZONE({tz}, TIMEZONE('UTC', {datetime_expr})) AS DATE)"
        if op == "DATE_FORMAT" and len(node.args) == 2:
            val = emitter._emit(node.args[0])
            if isinstance(node.args[1], sql.Literal) and isinstance(node.args[1].value, str):
                single_kind = _single_field_kind(node.args[1].value)
                if single_kind in _DATE_INVALID_FORMAT_KINDS:
                    return "NULL"
                fmt, _ = _translate_duckdb_format(node.args[1].value)
                return f"STRFTIME({val}, {_quote_sql_string(fmt)})"
            raise ValueError("DATE_FORMAT requires a literal format string in SQL backends.")
        if op == "DATETIME_FORMAT" and len(node.args) >= 2:
            val = emitter._emit(node.args[0])
            tz_node = node.args[2] if len(node.args) >= 3 else None
            val = self._apply_tz(val, tz_node, emitter)
            if isinstance(node.args[1], sql.Literal) and isinstance(node.args[1].value, str):
                single_kind = _single_field_kind(node.args[1].value)
                if single_kind == "tz_name":
                    return "'UTC'"
                if single_kind == "tz_offset":
                    return "'+00:00'"
                if single_kind == "millisecond":
                    return f"CAST((CAST(DATE_PART('milliseconds', {val}) AS BIGINT) % 1000) AS VARCHAR)"
                fmt, _ = _translate_duckdb_format(node.args[1].value)
                return f"STRFTIME({val}, {_quote_sql_string(fmt)})"
            raise ValueError("DATETIME_FORMAT requires a literal format string in SQL backends.")
        if op == "PARSE_DATE" and len(node.args) == 2:
            val = emitter._emit(node.args[0])
            if isinstance(node.args[1], sql.Literal) and isinstance(node.args[1].value, str):
                fmt, _ = _translate_duckdb_format(node.args[1].value)
                return f"CAST(STRPTIME({val}, {_quote_sql_string(fmt)}) AS DATE)"
            raise ValueError("PARSE_DATE requires a literal format string in SQL backends.")
        if op == "PARSE_DATETIME" and len(node.args) == 2:
            val = emitter._emit(node.args[0])
            if isinstance(node.args[1], sql.Literal) and isinstance(node.args[1].value, str):
                fmt, tokens = _translate_duckdb_format(node.args[1].value)
                parsed = f"STRPTIME({val}, {_quote_sql_string(fmt)})"
                if tokens & _TZ_PATTERN_KINDS:
                    return f"CAST(TIMEZONE('UTC', {parsed}) AS TIMESTAMP)"
                return f"CAST({parsed} AS TIMESTAMP)"
            raise ValueError("PARSE_DATETIME requires a literal format string in SQL backends.")
        if op == "PARSE_NUMBER":
            if len(node.args) == 1:
                arg0 = emitter._emit(node.args[0])
                return f"TRY_CAST({arg0} AS DECIMAL(38,14))"
            if len(node.args) == 3:
                arg0 = emitter._emit(node.args[0])
                precision = _literal_int_arg(node.args[1])
                scale = _literal_int_arg(node.args[2])
                if precision is not None and scale is not None:
                    return f"TRY_CAST({arg0} AS DECIMAL({precision},{scale}))"
                return f"TRY_CAST({arg0} AS DECIMAL(38,14))"
        if op == "STARTS_WITH" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"STARTS_WITH({left}, {right})"
        if op == "ENDS_WITH" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"ENDS_WITH({left}, {right})"
        if op == "CONTAINS" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"CONTAINS({left}, {right})"
        if op == "REGEX_MATCH" and len(node.args) == 2:
            regex = emitter._emit(node.args[0])
            value = emitter._emit(node.args[1])
            return f"REGEXP_MATCHES({value}, {regex})"
        if op == "PARSE_FLOAT" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"TRY_CAST({arg0} AS DOUBLE)"
        if op == "NATURAL_LOG" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"LN({arg0})"
        if op == "ACOT" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"ATAN(1.0 / ({arg0}))"
        if op == "PARSE_UUID" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"TRY_CAST({arg0} AS UUID)"
        if op == "UUID_TO_STRING" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"CAST({arg0} AS VARCHAR)"
        if op == "FACTORIAL" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"FACTORIAL(CAST({arg0} AS INTEGER))"
        if op == "SUBSTRING" and len(node.args) in (2, 3):
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            start = f"CAST({arg1} AS BIGINT)"
            if len(node.args) == 2:
                return f"SUBSTRING({arg0}, ({start} + 1))"
            arg2 = emitter._emit(node.args[2])
            stop = f"CAST({arg2} AS BIGINT)"
            return f"SUBSTRING({arg0}, ({start} + 1), ({stop} - {start}))"
        if op == "SPLIT_PART" and len(node.args) == 3:
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            arg2 = emitter._emit(node.args[2])
            return f"SPLIT_PART({arg0}, {arg1}, CAST({arg2} AS BIGINT))"
        if op == "SPLIT" and len(node.args) == 3:
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            arg2 = emitter._emit(node.args[2])
            return f"SPLIT_PART({arg0}, {arg1}, CAST({arg2} AS BIGINT))"
        if op == "STRIP" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"TRIM({arg0})"
        if op == "JOIN" and len(node.args) == 2:
            seq_node = node.args[0]
            sep = emitter._emit(node.args[1])
            if isinstance(seq_node, sql.Expression) and seq_node.op.upper() == "TUPLE":
                pieces = [emitter._emit(arg) for arg in seq_node.args]
                if not pieces:
                    return "''"
                result = pieces[0]
                for piece in pieces[1:]:
                    result = f"({result} || {sep} || {piece})"
                return result
            raise ValueError("JOIN expects a static tuple/list of values in SQL backends.")
        return None

    def emit_table_function(self, emitter: SQLEmitter, node: sql.TableFunction) -> str | None:
        op = node.op.upper()
        if op == "SPLIT_TABLE" and len(node.args) == 2:
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            return f"UNNEST(STRING_SPLIT({arg0}, {arg1})) WITH ORDINALITY"
        if op == "RANGE_TABLE" and len(node.args) == 3:
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            arg2 = emitter._emit(node.args[2])
            return f"UNNEST(RANGE(CAST({arg0} AS BIGINT), CAST({arg1} AS BIGINT), CAST({arg2} AS BIGINT)))"
        return None

#------------------------------------------------------
# SnowflakeDialect
#------------------------------------------------------

class SnowflakeDialect(SQLDialect):
    def quote_identifier(self, name: str) -> str:
        return name

    def format_table_name(self, name: str) -> str:
        # Snowflake uses db.schema.table format natively
        return name

    def emit_rowid(self, table_alias: str) -> str:
        return f"HASH({table_alias}.*)"

    def emit_expression(self, emitter: SQLEmitter, node: sql.Expression) -> str | None:
        op = node.op.upper()
        if op == "PARSE_NUMBER":
            if len(node.args) == 1:
                arg0 = emitter._emit(node.args[0])
                return f"TRY_TO_DECIMAL({arg0}, 38, 14)"
            if len(node.args) == 3:
                arg0 = emitter._emit(node.args[0])
                precision = _literal_int_arg(node.args[1])
                scale = _literal_int_arg(node.args[2])
                if precision is not None and scale is not None:
                    return f"TRY_TO_DECIMAL({arg0}, {precision}, {scale})"
                return f"TRY_TO_DECIMAL({arg0}, 38, 14)"
        if op == "STARTS_WITH" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"STARTSWITH({left}, {right})"
        if op == "ENDS_WITH" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"ENDSWITH({left}, {right})"
        if op == "CONTAINS" and len(node.args) == 2:
            left = emitter._emit(node.args[0])
            right = emitter._emit(node.args[1])
            return f"CONTAINS({left}, {right})"
        if op == "REGEX_MATCH" and len(node.args) == 2:
            regex = emitter._emit(node.args[0])
            value = emitter._emit(node.args[1])
            return f"REGEXP_LIKE({value}, {regex})"
        if op == "PARSE_FLOAT" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"TRY_TO_DOUBLE({arg0})"
        if op == "NATURAL_LOG" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"LN({arg0})"
        if op == "ACOT" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"ATAN(1.0 / ({arg0}))"
        if op == "PARSE_UUID" and len(node.args) == 1:
            return emitter._emit(node.args[0])
        if op == "UUID_TO_STRING" and len(node.args) == 1:
            return emitter._emit(node.args[0])
        if op == "FACTORIAL" and len(node.args) == 1:
            arg0 = emitter._emit(node.args[0])
            return f"FACTORIAL(CAST({arg0} AS INTEGER))"
        return None

    def emit_table_function(self, emitter: SQLEmitter, node: sql.TableFunction) -> str | None:
        op = node.op.upper()
        if op == "SPLIT_TABLE" and len(node.args) == 2:
            arg0 = emitter._emit(node.args[0])
            arg1 = emitter._emit(node.args[1])
            return f"TABLE(SPLIT_TO_TABLE({arg0}, {arg1}))"
        return None

#------------------------------------------------------
# SQLEmitter
#------------------------------------------------------

class SQLEmitter:
    def __init__(self, dialect: SQLDialect | None = None):
        self.dialect = dialect or DuckDBDialect()
        self._quote_select_aliases_stack: list[bool] = []

    #------------------------------------------------------
    # Main entry point
    #------------------------------------------------------

    def emit(self, node: sql.IRNode, *, quote_select_aliases: bool = False) -> str:
        self._quote_select_aliases_stack.append(quote_select_aliases)
        try:
            return self._emit(node)
        finally:
            self._quote_select_aliases_stack.pop()

    def _quote_select_aliases_enabled(self) -> bool:
        return self._quote_select_aliases_stack[-1] if self._quote_select_aliases_stack else False

    #------------------------------------------------------
    # Dispatch
    #------------------------------------------------------

    def _emit(self, node: sql.IRNode) -> str:
        if isinstance(node, sql.Fragment):
            return self._emit_fragment(node)
        elif isinstance(node, sql.TableFunction):
            return self._emit_table_function(node)
        elif isinstance(node, sql.Expression):
            return self._emit_expression(node)
        elif isinstance(node, sql.WindowExpression):
            return self._emit_window_expression(node)
        elif isinstance(node, sql.Literal):
            return self._emit_literal(node)
        elif isinstance(node, sql.Filter):
            return self._emit_filter(node)
        elif isinstance(node, sql.Join):
            return self._emit_join(node)
        elif isinstance(node, sql.TableRef):
            return self._emit_table_ref(node)
        elif isinstance(node, sql.ColumnRef):
            return self._emit_column_ref(node)
        elif isinstance(node, sql.AggregatePlaceholder):
            return self._emit_aggregate(node)
        elif isinstance(node, sql.Alias):
            return self._emit_alias(node)
        elif isinstance(node, sql.RowId):
            return self._emit_rowid(node)
        elif isinstance(node, sql.Star):
            return self._emit_star(node)
        elif isinstance(node, sql.Values):
            return self._emit_values(node)
        elif isinstance(node, sql.CreateView):
            return self._emit_create_view(node)
        elif isinstance(node, sql.Union):
            return self._emit_union(node)
        elif isinstance(node, sql.UnionAll):
            return self._emit_union_all(node)
        elif isinstance(node, sql.GroupByMax):
            return self._emit_group_by_max(node)
        elif isinstance(node, sql.SemiNaiveLoop):
            return self._emit_semi_naive_loop(node)
        elif isinstance(node, sql.And):
            return self._emit_and(node)
        elif isinstance(node, sql.Or):
            return self._emit_or(node)
        elif isinstance(node, sql.CaseWhen):
            return self._emit_case_when(node)
        elif isinstance(node, sql.Exists):
            return self._emit_exists(node)
        elif isinstance(node, sql.Not):
            return self._emit_not(node)
        elif isinstance(node, sql.Distinct):
            return self._emit_distinct(node)
        elif isinstance(node, sql.Lateral):
            return self._emit_lateral(node)
        else:
            raise NotImplementedError(f"Cannot emit node of type {type(node).__name__}")

    #------------------------------------------------------
    # Fragment
    #------------------------------------------------------

    def _emit_fragment(self, node: sql.Fragment) -> str:
        parts = []

        # SELECT
        if node.selects:
            select_items = self._emit_select_items(node.selects)
            parts.append(f"SELECT {select_items}")
        else:
            parts.append("SELECT *")

        # FROM with properly ordered JOINs
        if node.scans:
            if node.joins:
                ordered_joins = self._order_joins(node.scans, node.joins)
                # Root tables are those never appearing as target of a join
                target_tables = {j.target for j in node.joins}
                roots = [s for s in node.scans if s not in target_tables]

                if len(roots) <= 1:
                    from_clause = ", ".join(self._emit(s) for s in roots)
                else:
                    # Use explicit CROSS JOIN for root scans to avoid dialect-specific
                    # scoping issues with mixed comma joins and explicit JOIN ... ON.
                    from_clause = " CROSS JOIN ".join(self._emit(s) for s in roots)
                join_clause = " ".join(self._emit(j) for j in ordered_joins)
                parts.append(f"FROM {from_clause} {join_clause}")
            else:
                from_items = ", ".join(self._emit(s) for s in node.scans)
                parts.append(f"FROM {from_items}")

        # WHERE
        if node.filters:
            where_clauses = " AND ".join(self._emit(f) for f in node.filters)
            parts.append(f"WHERE {where_clauses}")

        # GROUP BY
        if node.group_by:
            group_items = ", ".join(self._emit(g) for g in node.group_by)
            parts.append(f"GROUP BY {group_items}")

        return "\n".join(parts)

    def _emit_select_items(self, selects: list[sql.IRNode]) -> str:
        alias_counts: dict[str, int] = defaultdict(int)
        items: list[str] = []
        for select in selects:
            if isinstance(select, sql.Alias):
                alias_counts[select.alias] += 1
                alias_name = select.alias
                if alias_counts[select.alias] > 1:
                    alias_name = f"{select.alias}_{alias_counts[select.alias]}"
                inner = self._emit(select.node)
                alias_sql = (
                    self._force_quote_identifier(alias_name)
                    if self._quote_select_aliases_enabled()
                    else self._quote_identifier(alias_name)
                )
                if isinstance(select.node, (sql.Fragment, sql.Union, sql.UnionAll, sql.Values, sql.GroupByMax)):
                    items.append(f"(\n{inner}\n) AS {alias_sql}")
                else:
                    items.append(f"{inner} AS {alias_sql}")
                continue
            items.append(self._emit(select))
        return ", ".join(items)

    def _order_joins(self, scans: list[sql.IRNode], joins: list[sql.Join]) -> list[sql.Join]:
        """Order joins so all incoming tables are available before joining the target."""
        if not joins:
            return []

        # Find roots - tables that are never the target of a join
        target_tables = {j.target for j in joins}
        available = {s for s in scans if s not in target_tables}

        ordered: list[sql.Join] = []
        remaining = list(joins)

        # Iteratively add joins whose incoming tables are all available
        while remaining:
            added_any = False
            for join in list(remaining):
                if join.incoming <= available:  # All dependencies satisfied
                    ordered.append(join)
                    available.add(join.target)
                    remaining.remove(join)
                    added_any = True
                    break  # Restart to maintain stable ordering

            if not added_any and remaining:
                # Fallback: add remaining joins in original order (handles edge cases)
                ordered.extend(remaining)
                break

        return ordered

    #------------------------------------------------------
    # Expression
    #------------------------------------------------------

    def _emit_expression(self, node: sql.Expression) -> str:
        # Let dialect handle it first
        result = self.dialect.emit_expression(self, node)
        if result is not None:
            return result

        op = node.op.upper()

        # Special cases
        if op == "CAST":
            arg = self._emit(node.args[0])
            # Get the type name and convert to SQL type
            type_node = node.args[1]
            if isinstance(type_node, sql.Literal) and isinstance(type_node.value, str):
                sql_type = self._to_sql_type(type_node.value)
            else:
                sql_type = self._emit(type_node)
            return f"CAST({arg} AS {sql_type})"

        # MD5 with multiple args - concatenate them
        if op == "MD5" and len(node.args) > 1:
            parts = [f"CAST({self._emit(a)} AS VARCHAR)" for a in node.args]
            concatenated = " || ".join(parts)
            return f"MD5({concatenated})"

        # Binary operators
        if len(node.args) == 2 and op in ("+", "-", "*", "/", "||", "%", "AND", "OR"):
            left = self._emit(node.args[0])
            right = self._emit(node.args[1])
            return f"({left} {op} {right})"

        # COUNT over all rows must be emitted as COUNT(*) for Snowflake compatibility.
        if op == "COUNT" and len(node.args) == 0:
            return "COUNT(*)"

        # Function call
        args = ", ".join(self._emit(a) for a in node.args)
        return f"{op}({args})"

    #------------------------------------------------------
    # TableFunction
    #------------------------------------------------------

    def _emit_table_function(self, node: sql.TableFunction) -> str:
        result = self.dialect.emit_table_function(self, node)
        if result is not None:
            return result
        raise ValueError(f"Unsupported table function {node.op!r} for dialect {self.dialect.__class__.__name__}")

    #------------------------------------------------------
    # WindowExpression
    #------------------------------------------------------

    def _emit_window_expression(self, node: sql.WindowExpression) -> str:
        func_args = ", ".join(self._emit(a) for a in node.args)
        func = f"{node.op.upper()}({func_args})"
        parts = []
        if node.partition_by:
            parts.append("PARTITION BY " + ", ".join(self._emit(p) for p in node.partition_by))
        if node.order_by:
            parts.append("ORDER BY " + ", ".join(self._emit(o) for o in node.order_by))
        over = " ".join(parts)
        if over:
            return f"{func} OVER ({over})"
        return f"{func} OVER ()"

    #------------------------------------------------------
    # Literal
    #------------------------------------------------------

    def _emit_literal(self, node: sql.Literal) -> str:
        # Let dialect handle it first
        result = self.dialect.emit_literal(self, node)
        if result is not None:
            return result

        type_lower = node.type.lower() if node.type else ""
        if type_lower == "string":
            escaped = str(node.value).replace("'", "''")
            return f"'{escaped}'"
        elif type_lower == "bool":
            return "TRUE" if node.value else "FALSE"
        elif type_lower == "date":
            return f"DATE '{node.value}'"
        elif type_lower == "datetime":
            # Convert timezone-aware datetimes to UTC for consistent output
            if isinstance(node.value, dt.datetime) and node.value.tzinfo is not None:
                utc_value = node.value.astimezone(dt.timezone.utc).replace(tzinfo=None)
                return f"TIMESTAMP '{utc_value}'"
            return f"TIMESTAMP '{node.value}'"
        elif node.value is None:
            return "NULL"
        elif isinstance(node.value, float) and math.isnan(node.value):
            return "NULL"
        elif type_lower.startswith("number("):
            # Handle decimal types with precision, e.g., Number(38,14) -> DECIMAL(38,14)
            # Extract precision and scale from type like "number(38,14)"
            precision_scale = type_lower[7:-1]  # Remove "number(" and ")"
            return f"CAST({node.value} AS DECIMAL({precision_scale}))"
            # return str(node.value)
        else:
            return str(node.value)

    #------------------------------------------------------
    # Filter
    #------------------------------------------------------

    def _emit_filter(self, node: sql.Filter) -> str:
        left = self._emit(node.left)
        right = self._emit(node.right)
        op = node.op

        # Route function-like filters through dialect expression mapping.
        expr_node = sql.Expression(op=op, args=[node.left, node.right])
        dialect_result = self.dialect.emit_expression(self, expr_node)
        if dialect_result is not None:
            return dialect_result

        # Handle special operators
        if op == "=" and right == "NULL":
            return f"{left} IS NULL"
        elif op == "!=" and right == "NULL":
            return f"{left} IS NOT NULL"

        return f"{left} {op} {right}"

    #------------------------------------------------------
    # And
    #------------------------------------------------------

    def _emit_and(self, node: sql.And) -> str:
        if not node.conditions:
            return "TRUE"
        parts = [self._emit(c) for c in node.conditions]
        return "(" + " AND ".join(parts) + ")"

    #------------------------------------------------------
    # Or
    #------------------------------------------------------

    def _emit_or(self, node: sql.Or) -> str:
        if not node.conditions:
            return "FALSE"
        parts = [self._emit(c) for c in node.conditions]
        return "(" + " OR ".join(parts) + ")"

    #------------------------------------------------------
    # CaseWhen
    #------------------------------------------------------

    def _emit_case_when(self, node: sql.CaseWhen) -> str:
        parts = ["CASE"]
        for condition, result in node.branches:
            cond_sql = self._emit(condition)
            result_sql = self._emit(result)
            parts.append(f"WHEN {cond_sql} THEN {result_sql}")
        if node.else_result is not None:
            else_sql = self._emit(node.else_result)
            parts.append(f"ELSE {else_sql}")
        parts.append("END")
        return " ".join(parts)

    #------------------------------------------------------
    # Exists
    #------------------------------------------------------

    def _emit_exists(self, node: sql.Exists) -> str:
        subquery_sql = self._emit(node.subquery)
        return f"EXISTS (\n{subquery_sql}\n)"

    #------------------------------------------------------
    # Not
    #------------------------------------------------------

    def _emit_not(self, node: sql.Not) -> str:
        condition_sql = self._emit(node.condition)
        return f"NOT ({condition_sql})"

    #------------------------------------------------------
    # Distinct
    #------------------------------------------------------

    def _emit_distinct(self, node: sql.Distinct) -> str:
        inner = self._emit(node.node)
        return f"DISTINCT {inner}"

    #------------------------------------------------------
    # Lateral
    #------------------------------------------------------

    def _emit_lateral(self, node: sql.Lateral) -> str:
        inner = self._emit(node.node)
        return f"LATERAL {inner}"

    #------------------------------------------------------
    # Join
    #------------------------------------------------------

    def _emit_join(self, node: sql.Join) -> str:
        target = self._emit(node.target)

        if node.filters:
            on_clause = " AND ".join(self._emit(f) for f in node.filters)
            return f"{node.type} JOIN {target} ON {on_clause}"
        if node.type != "INNER":
            return f"{node.type} JOIN {target} ON TRUE"
        return f"CROSS JOIN {target}"

    #------------------------------------------------------
    # TableRef
    #------------------------------------------------------

    def _emit_table_ref(self, node: sql.TableRef) -> str:
        name = self.dialect.format_table_name(node.name)
        # Quote each part of the table name separately (handles db.schema.table)
        name = ".".join(self._quote_identifier(part) for part in name.split("."))
        if node.alias:
            alias = self._quote_identifier(node.alias)
            return f"{name} AS {alias}"
        return name

    #------------------------------------------------------
    # ColumnRef
    #------------------------------------------------------

    def _emit_column_ref(self, node: sql.ColumnRef) -> str:
        table_alias = node.table.alias if node.table.alias else node.table.name
        table_alias = self._quote_identifier(table_alias)
        column_name = self._quote_identifier(node.name)
        return f"{table_alias}.{column_name}"

    #------------------------------------------------------
    # Aggregate
    #------------------------------------------------------

    def _emit_aggregate(self, node: sql.AggregatePlaceholder) -> str:
        # for agg placeholders we only need to emit the reference, everything
        # else is handled elsewhere through compilation
        exp_sql = self._emit(node.ref)
        return exp_sql

    #------------------------------------------------------
    # Alias
    #------------------------------------------------------

    def _emit_alias(self, node: sql.Alias) -> str:
        if node.node is None:
            raise ValueError(f"Alias '{node.alias}' has None inner node")
        inner = self._emit(node.node)
        alias = self._quote_identifier(node.alias)
        if isinstance(node.node, (sql.Fragment, sql.Union, sql.UnionAll, sql.Values, sql.GroupByMax)):
            return f"(\n{inner}\n) AS {alias}"
        return f"{inner} AS {alias}"

    #------------------------------------------------------
    # RowId
    #------------------------------------------------------

    def _emit_rowid(self, node: sql.RowId) -> str:
        table_alias = node.table.alias if node.table.alias else node.table.name
        return self.dialect.emit_rowid(table_alias)

    #------------------------------------------------------
    # Star
    #------------------------------------------------------

    def _emit_star(self, node: sql.Star) -> str:
        if node.table:
            return f"{self._quote_identifier(node.table.alias or node.table.name)}.*"
        return "*"

    #------------------------------------------------------
    # Values
    #------------------------------------------------------

    def _emit_values(self, node: sql.Values) -> str:
        column_list = ", ".join(self._quote_identifier(col.name) for col in node.columns)
        col_types = [col.column.fields[-1].type.name for col in node.columns] # type: ignore
        value_rows = []
        for row in node.data.itertuples():
            literals = []
            for ix, col_type in enumerate(col_types):
                value = row[ix]
                literal_node = sql.Literal(value=value, type=col_type)
                literal_sql = self._emit_literal(literal_node)
                literals.append(literal_sql)
            value_row = "(" + ", ".join(literals) + ")"
            value_rows.append(value_row)
        values_clause = ",\n".join(value_rows)
        alias = self._quote_identifier(node.alias)
        return f"(VALUES\n{values_clause}\n) AS {alias} ({column_list})"

    #------------------------------------------------------
    # CreateView
    #------------------------------------------------------

    def _emit_create_view(self, node: sql.CreateView) -> str:
        formatted = self.dialect.format_table_name(node.name)
        name = ".".join(self._quote_identifier(part) for part in formatted.split("."))
        query = self._emit(node.query)
        return f"CREATE OR REPLACE VIEW {name} AS\n{query}"

    #------------------------------------------------------
    # Union
    #------------------------------------------------------

    def _emit_union(self, node: sql.Union) -> str:
        if not node.queries:
            return ""
        parts = [self._emit(q) for q in node.queries]
        return "\nUNION\n".join(parts)

    #------------------------------------------------------
    # UnionAll
    #------------------------------------------------------

    def _emit_union_all(self, node: sql.UnionAll) -> str:
        if not node.queries:
            return ""
        parts = [self._emit(q) for q in node.queries]
        return "\nUNION ALL\n".join(parts)

    #------------------------------------------------------
    # GroupByMax
    #------------------------------------------------------

    def _emit_group_by_max(self, node: sql.GroupByMax) -> str:
        # Build SELECT clause: group cols + MAX(other cols)
        select_parts = []
        for col_ref in node.group_cols:
            col_name = self._quote_identifier(col_ref.name)
            select_parts.append(f"{self._emit(col_ref)} AS {col_name}")
        for col_ref in node.max_cols:
            col_name = self._quote_identifier(col_ref.name)
            select_parts.append(f"MAX({self._emit(col_ref)}) AS {col_name}")

        select_clause = ", ".join(select_parts)

        # Emit source as subquery
        source_sql = self._emit(node.source)

        # Build GROUP BY clause
        group_clause = ", ".join(self._emit(col_ref) for col_ref in node.group_cols)

        return f"SELECT {select_clause}\nFROM {source_sql}\nGROUP BY {group_clause}"

    def _quote_identifier(self, name: str) -> str:
        """Quote an identifier if it contains special characters or is a reserved word."""
        # Check if identifier needs quoting:
        # - Contains non-alphanumeric/underscore characters
        # - Starts with a digit or underscore
        # - Is a reserved word
        needs_quote = (
            name.lower() in SQL_RESERVED_WORDS or
            name.startswith('_') or
            (name and name[0].isdigit()) or
            not name.replace('_', '').isalnum()
        )
        if needs_quote:
            escaped = name.replace('"', '""')
            return f'"{escaped}"'
        return self.dialect.quote_identifier(name)

    def _force_quote_identifier(self, name: str) -> str:
        escaped = name.replace('"', '""')
        return f'"{escaped}"'

    def _to_sql_type(self, type_name: str) -> str:
        """Convert an internal type name to a SQL type."""
        # Handle Number(precision, scale) -> DECIMAL(precision, scale)
        if type_name.startswith("Number("):
            return "DECIMAL" + type_name[6:]  # Replace "Number" with "DECIMAL"
        # Handle simple type mappings
        type_map = {
            "String": "VARCHAR",
            "Number": "DECIMAL",
            "Boolean": "BOOLEAN",
            "Date": "DATE",
            "DateTime": "TIMESTAMP",
            "Float": "DOUBLE",
        }
        return type_map.get(type_name, type_name)

    #------------------------------------------------------
    # SemiNaiveLoop
    #------------------------------------------------------

    def _emit_semi_naive_loop(self, node: sql.SemiNaiveLoop) -> str:
        # Semi-naive emission is deferred - raise NotImplementedError
        table_names = ", ".join(t.table_name for t in node.tables)
        raise NotImplementedError(
            f"Semi-naive loop emission is not yet implemented. "
            f"This requires dialect-specific handling: "
            f"DuckDB uses recursive CTEs, Snowflake uses temp tables with iteration. "
            f"Tables: {table_names}"
        )
