from __future__ import annotations

import datetime as dt
import math
import re
from typing import cast

from relationalai.semantics.std.datetime_format import DateFormatField, DateFormatLiteral, parse_julia_datetime_format
from relationalai.semantics.backends.sql.dialect import (
    _DATE_INVALID_FORMAT_KINDS,
    _TZ_PATTERN_KINDS,
    _single_field_kind,
    _translate_duckdb_format,
    _translate_snowflake_format,
)

from .ir import (
    SQL2BinaryExpr,
    SQL2CastExpr,
    SQL2ColumnExpr,
    SQL2CreateViewOp,
    SQL2ExistsExpr,
    SQL2Expr,
    SQL2FromItem,
    SQL2FunctionExpr,
    SQL2Join,
    SQL2LiteralExpr,
    SQL2NullExpr,
    SQL2OrderByExpr,
    SQL2Query,
    SQL2RecursiveQuery,
    SQL2SelectItem,
    SQL2SelectQuery,
    SQL2SubquerySource,
    SQL2TableSource,
    SQL2UnionQuery,
    SQL2WindowExpr,
)


class SQL2Emitter:
    """Renderer for SQL2 structured IR."""

    class _IdentityDialect:
        @staticmethod
        def format_table_name(name: str) -> str:
            return name

    def __init__(self, *, dialect=None):
        self.dialect = dialect if dialect is not None else self._IdentityDialect()

    def emit(self, operation: SQL2CreateViewOp | SQL2Query) -> str:
        if isinstance(operation, SQL2CreateViewOp):
            return f"CREATE OR REPLACE VIEW {self._qid_path(operation.name)} AS\n{self.emit_query(operation.query)}"
        return self.emit_query(operation)

    def emit_query(self, query: SQL2Query) -> str:
        if isinstance(query, SQL2SelectQuery):
            return self._emit_select(query)
        if isinstance(query, SQL2UnionQuery):
            values_sql = self._emit_values_union(query)
            if values_sql is not None:
                return values_sql
            joiner = "\nUNION\n" if query.distinct else "\nUNION ALL\n"
            return joiner.join(self._wrap_query(part) for part in query.parts)
        if isinstance(query, SQL2RecursiveQuery):
            return self._emit_recursive(query)
        raise TypeError(f"Unsupported SQL2 query type: {type(query)!r}")

    def _emit_recursive(self, query: SQL2RecursiveQuery) -> str:
        if query.base_query is None or query.recursive_query is None:
            raise TypeError("SQL2RecursiveQuery requires both base_query and recursive_query.")
        if query.final_query is None:
            raise TypeError("SQL2RecursiveQuery requires final_query.")

        union_kw = "UNION" if query.distinct else "UNION ALL"
        column_sql = ""
        if query.column_names:
            column_sql = "(" + ", ".join(self._qid(name) for name in query.column_names) + ")"
        cte_sql = (
            f"WITH RECURSIVE {self._qid(query.cte_name)}{column_sql} AS (\n"
            f"{self._wrap_query(query.base_query)}\n"
            f"{union_kw}\n"
            f"{self._wrap_query(query.recursive_query)}\n"
            f")"
        )
        return f"{cte_sql}\n{self.emit_query(query.final_query)}"

    def _emit_select(self, query: SQL2SelectQuery) -> str:
        distinct = "DISTINCT " if query.distinct else ""
        select_items = (
            ",\n  ".join(self._emit_select_item(item) for item in query.select_items)
            or f'1 AS {self._qid("empty")}'
        )
        lines = [f"SELECT {distinct}\n  {select_items}"]
        limit_zero = self._is_canonical_empty_where(query.where)
        deferred_where: list[SQL2Expr] = []
        available_aliases: set[str] = set()
        if query.from_item is not None:
            lines.append(f"FROM {self._emit_from_item(query.from_item)}")
            available_aliases.add(query.from_item.alias)
        for join in query.joins:
            rendered_join = join
            if join.join_type.upper() == "INNER" and join.on:
                local_aliases = set(available_aliases)
                local_aliases.add(join.source.alias)
                local_on: list[SQL2Expr] = []
                for predicate in join.on:
                    aliases = self._expr_table_aliases(predicate)
                    if aliases.issubset(local_aliases):
                        local_on.append(predicate)
                    else:
                        deferred_where.append(predicate)
                rendered_join = SQL2Join(source=join.source, join_type=join.join_type, on=tuple(local_on))
            lines.append(self._emit_join(rendered_join))
            available_aliases.add(join.source.alias)
        effective_where = tuple((*query.where, *deferred_where))
        if effective_where and not limit_zero:
            where_sql = " AND ".join(self._emit_expr(predicate) for predicate in effective_where)
            lines.append(f"WHERE {where_sql}")
        if query.group_by:
            group_sql = ", ".join(self._emit_expr(expr) for expr in query.group_by)
            lines.append(f"GROUP BY {group_sql}")
        if query.having:
            having_sql = " AND ".join(self._emit_expr(predicate) for predicate in query.having)
            lines.append(f"HAVING {having_sql}")
        if limit_zero:
            lines.append("LIMIT 0")
        return "\n".join(lines)

    def _expr_table_aliases(self, expr: SQL2Expr) -> set[str]:
        if isinstance(expr, SQL2ColumnExpr):
            return {expr.table_alias}
        if isinstance(expr, SQL2BinaryExpr):
            return self._expr_table_aliases(expr.left) | self._expr_table_aliases(expr.right)
        if isinstance(expr, SQL2CastExpr):
            return self._expr_table_aliases(expr.expr)
        if isinstance(expr, SQL2FunctionExpr):
            out: set[str] = set()
            for arg in expr.args:
                out |= self._expr_table_aliases(arg)
            return out
        if isinstance(expr, SQL2WindowExpr):
            out: set[str] = set()
            for arg in expr.args:
                out |= self._expr_table_aliases(arg)
            for part in expr.partition_by:
                out |= self._expr_table_aliases(part)
            for order in expr.order_by:
                out |= self._expr_table_aliases(order.expr)
            return out
        return set()

    def _is_canonical_empty_where(self, where: tuple[SQL2Expr, ...]) -> bool:
        if len(where) != 1:
            return False
        predicate = where[0]
        return (
            isinstance(predicate, SQL2BinaryExpr)
            and predicate.op == "="
            and isinstance(predicate.left, SQL2LiteralExpr)
            and isinstance(predicate.right, SQL2LiteralExpr)
            and predicate.left.value == 1
            and predicate.right.value == 0
        )

    def _emit_values_union(self, query: SQL2UnionQuery) -> str | None:
        if query.distinct or not query.parts:
            return None

        aliases: tuple[str, ...] | None = None
        rows: list[tuple[SQL2Expr, ...]] = []
        for part in query.parts:
            if not isinstance(part, SQL2SelectQuery):
                return None
            if part.from_item is not None or part.joins or part.where or part.group_by or part.having or part.distinct:
                return None
            if not part.select_items or any(item.alias is None for item in part.select_items):
                return None

            part_aliases = tuple(item.alias for item in part.select_items)
            assert all(alias is not None for alias in part_aliases)
            if aliases is None:
                aliases = tuple(alias for alias in part_aliases if alias is not None)
            elif aliases != tuple(alias for alias in part_aliases if alias is not None):
                return None

            row_exprs = tuple(item.expr for item in part.select_items)
            if any(not isinstance(expr, (SQL2LiteralExpr, SQL2NullExpr)) for expr in row_exprs):
                return None
            rows.append(row_exprs)

        if aliases is None or not rows:
            return None

        values_rows = ",\n    ".join(
            "(" + ", ".join(self._emit_expr(expr) for expr in row) + ")"
            for row in rows
        )
        projected = ",\n  ".join(
            f'{self._qid("_values")}.{self._qid(alias)} AS {self._qid(alias)}'
            for alias in aliases
        )
        alias_columns = ", ".join(self._qid(alias) for alias in aliases)
        return (
            f"SELECT \n  {projected}\n"
            f"FROM (\n  VALUES\n    {values_rows}\n) AS {self._qid('_values')} ({alias_columns})"
        )

    def _emit_select_item(self, item: SQL2SelectItem) -> str:
        expr_sql = self._emit_expr(item.expr)
        if item.alias is None:
            return expr_sql
        return f"{expr_sql} AS {self._qid(item.alias)}"

    def _emit_join(self, join: SQL2Join) -> str:
        join_type = join.join_type.upper()
        join_sql = f"{join_type} JOIN {self._emit_from_item(join.source)}"
        if join.on:
            on_sql = " AND ".join(self._emit_expr(predicate) for predicate in join.on)
            return f"{join_sql} ON {on_sql}"
        if join_type == "INNER":
            return f"CROSS JOIN {self._emit_from_item(join.source)}"
        return f"{join_sql} ON TRUE"

    def _emit_from_item(self, item: SQL2FromItem) -> str:
        lateral = "LATERAL " if item.lateral else ""
        if isinstance(item.source, SQL2TableSource):
            source_sql = self._qid_path(item.source.name)
        elif isinstance(item.source, SQL2SubquerySource):
            source_sql = self._emit_subquery_source(item)
        else:
            raise TypeError(f"Unsupported SQL2 source: {type(item.source)!r}")
        return f"{lateral}{source_sql} AS {self._qid(item.alias)}"

    def _emit_subquery_source(self, item: SQL2FromItem) -> str:
        assert isinstance(item.source, SQL2SubquerySource)
        query = item.source.query
        source_sql = self._wrap_query(query)
        if isinstance(query, SQL2UnionQuery) and self._requires_from_union_wrapper(item):
            # Some dialects reject raw set-op forms in FROM/LATERAL positions.
            source_sql = f"(SELECT * FROM {source_sql} AS {self._qid('_union_src')})"
        return source_sql

    def _requires_from_union_wrapper(self, item: SQL2FromItem) -> bool:
        return False

    def _emit_expr(self, expr: SQL2Expr) -> str:
        if isinstance(expr, SQL2ColumnExpr):
            return f"{self._qid(expr.table_alias)}.{self._qid(expr.column_name)}"
        if isinstance(expr, SQL2LiteralExpr):
            if expr.value is None:
                return "NULL"
            if isinstance(expr.value, bool):
                return "TRUE" if expr.value else "FALSE"
            if isinstance(expr.value, float):
                if not math.isfinite(expr.value):
                    if math.isnan(expr.value):
                        return "NULL"
                    return "CAST('inf' AS DOUBLE)" if expr.value > 0 else "CAST('-inf' AS DOUBLE)"
            if isinstance(expr.value, dt.datetime):
                value = expr.value
                if value.tzinfo is not None:
                    value = value.astimezone(dt.timezone.utc).replace(tzinfo=None)
                literal = value.isoformat(sep=" ")
                escaped = literal.replace("'", "''")
                return f"CAST('{escaped}' AS TIMESTAMP)"
            if isinstance(expr.value, dt.date):
                return f"CAST('{expr.value.isoformat()}' AS DATE)"
            if isinstance(expr.value, str):
                return "'" + expr.value.replace("'", "''") + "'"
            return str(expr.value)
        if isinstance(expr, SQL2NullExpr):
            return "NULL"
        if isinstance(expr, SQL2FunctionExpr):
            return self._emit_function(expr)
        if isinstance(expr, SQL2WindowExpr):
            return self._emit_window(expr)
        if isinstance(expr, SQL2CastExpr):
            return f"CAST({self._emit_expr(expr.expr)} AS {expr.target_type})"
        if isinstance(expr, SQL2BinaryExpr):
            return f"({self._emit_expr(expr.left)} {expr.op} {self._emit_expr(expr.right)})"
        if isinstance(expr, SQL2ExistsExpr):
            prefix = "NOT EXISTS" if expr.negated else "EXISTS"
            return f"{prefix} {self._wrap_query(expr.query)}"
        raise TypeError(f"Unsupported SQL2 expr type: {type(expr)!r}")

    def _emit_function(self, expr: SQL2FunctionExpr) -> str:
        name = expr.name.lower()
        args_sql = [self._emit_expr(arg) for arg in expr.args]

        if (
            name == "count"
            and expr.distinct
            and len(expr.args) == 1
            and isinstance(expr.args[0], SQL2FunctionExpr)
            and expr.args[0].name.lower() == "row"
        ):
            rewritten = self._emit_count_distinct_row(expr.args[0].args)
            if rewritten is not None:
                return rewritten

        if name in {
            "day",
            "week",
            "month",
            "year",
            "hour",
            "minute",
            "second",
            "millisecond",
            "microsecond",
            "nanosecond",
        } and len(args_sql) == 1:
            return args_sql[0]
        if name in {"date_add", "date_subtract", "datetime_add", "datetime_subtract"} and len(expr.args) == 2:
            base_sql = args_sql[0]
            period = self._extract_period(expr.args[1])
            if period is not None:
                value_sql, unit = period
                return self._emit_date_add_or_subtract(
                    op_name=name,
                    base_sql=base_sql,
                    value_sql=value_sql,
                    unit=unit,
                )
        if name == "like" and len(args_sql) == 2:
            return f"({args_sql[0]} LIKE {args_sql[1]})"
        if name == "starts_with" and len(args_sql) == 2:
            return f"({args_sql[0]} LIKE CONCAT({args_sql[1]}, '%'))"
        if name == "ends_with" and len(args_sql) == 2:
            return f"({args_sql[0]} LIKE CONCAT('%', {args_sql[1]}))"
        if name == "contains" and len(args_sql) == 2:
            return f"({args_sql[0]} LIKE CONCAT('%', {args_sql[1]}, '%'))"
        if name == "regex_match" and len(args_sql) == 2:
            return self._emit_regex_match(pattern_sql=args_sql[0], value_sql=args_sql[1])
        if name == "md5" and len(args_sql) == 1:
            # CONCAT already yields a string expression; avoid redundant CAST noise.
            if isinstance(expr.args[0], SQL2FunctionExpr) and expr.args[0].name.lower() == "concat":
                return f"MD5({args_sql[0]})"
            return f"MD5(CAST({args_sql[0]} AS VARCHAR))"
        if name in {"strip", "trim"} and len(args_sql) == 1:
            return f"TRIM({args_sql[0]})"
        if name in {"natural_log", "ln"} and len(args_sql) == 1:
            return f"LN({args_sql[0]})"
        if name == "parse_float" and len(args_sql) == 1:
            return f"TRY_CAST({args_sql[0]} AS DOUBLE)"
        if name == "isinf" and len(args_sql) == 1:
            return f"COALESCE(ISINF({args_sql[0]}), FALSE)"
        if name == "isnan" and len(args_sql) == 1:
            return f"(({args_sql[0]}) IS NULL OR COALESCE(ISNAN({args_sql[0]}), FALSE))"
        if name in {"parse_number", "parse_decimal"} and len(args_sql) == 1:
            return f"TRY_CAST({args_sql[0]} AS DECIMAL(38,14))"
        if name in {"parse_number", "parse_decimal"} and len(expr.args) >= 3:
            precision_arg = expr.args[1]
            scale_arg = expr.args[2]
            if (
                isinstance(precision_arg, SQL2LiteralExpr)
                and isinstance(scale_arg, SQL2LiteralExpr)
                and isinstance(precision_arg.value, int)
                and isinstance(scale_arg.value, int)
            ):
                return f"TRY_CAST({args_sql[0]} AS DECIMAL({precision_arg.value},{scale_arg.value}))"
            return f"TRY_CAST({args_sql[0]} AS DECIMAL(38,14))"
        if name == "parse_int" and len(args_sql) == 1:
            return f"TRY_CAST({args_sql[0]} AS DECIMAL(38,0))"
        if name == "parse_uuid" and len(args_sql) == 1:
            return f"TRY_CAST({args_sql[0]} AS UUID)"
        if name == "uuid_to_string" and len(args_sql) == 1:
            return f"CAST({args_sql[0]} AS VARCHAR)"
        if name == "acot" and len(args_sql) == 1:
            return f"ATAN(1.0 / ({args_sql[0]}))"
        if name == "factorial" and len(args_sql) == 1:
            return f"FACTORIAL(CAST({args_sql[0]} AS INTEGER))"
        if name == "date_year" and len(args_sql) == 1:
            return f"EXTRACT(YEAR FROM {args_sql[0]})"
        if name == "date_month" and len(args_sql) == 1:
            return f"EXTRACT(MONTH FROM {args_sql[0]})"
        if name == "date_day" and len(args_sql) == 1:
            return f"EXTRACT(DAY FROM {args_sql[0]})"
        if name == "date_week" and len(args_sql) == 1:
            return f"WEEK({args_sql[0]})"
        if name == "date_quarter" and len(args_sql) == 1:
            return f"QUARTER({args_sql[0]})"
        if name == "date_dayofyear" and len(args_sql) == 1:
            return f"DAYOFYEAR({args_sql[0]})"
        if name in {"date_weekday", "date_isoweekday"} and len(args_sql) == 1:
            return self._emit_iso_weekday(args_sql[0])
        if name.startswith("datetime_") and len(args_sql) >= 1:
            base = args_sql[0]
            if len(args_sql) >= 2:
                base = self._convert_datetime_timezone_for_output(value_sql=base, tz_sql=args_sql[1])
            if name == "datetime_year":
                return f"EXTRACT(YEAR FROM {base})"
            if name == "datetime_month":
                return f"EXTRACT(MONTH FROM {base})"
            if name == "datetime_day":
                return f"EXTRACT(DAY FROM {base})"
            if name == "datetime_week":
                return f"WEEK({base})"
            if name == "datetime_quarter":
                return f"QUARTER({base})"
            if name == "datetime_dayofyear":
                return f"DAYOFYEAR({base})"
            if name in {"datetime_weekday", "datetime_isoweekday"}:
                return self._emit_iso_weekday(base)
            if name == "datetime_hour":
                return f"HOUR({base})"
            if name == "datetime_minute":
                return f"MINUTE({base})"
            if name == "datetime_second":
                return f"SECOND({base})"
        if name == "dates_period_days" and len(args_sql) == 2:
            return self._emit_dates_period_days(start_sql=args_sql[0], end_sql=args_sql[1])
        if name == "datetimes_period_milliseconds" and len(args_sql) == 2:
            return self._emit_datetimes_period_milliseconds(start_sql=args_sql[0], end_sql=args_sql[1])
        if name in {"date_diff", "datetime_diff"} and len(args_sql) == 3:
            return self._emit_date_diff_fn(unit_sql=args_sql[0], start_sql=args_sql[1], end_sql=args_sql[2])
        if name == "date_format" and len(args_sql) == 2:
            assert len(expr.args) == 2
            fmt_arg = expr.args[1]
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                single_kind = _single_field_kind(fmt_arg.value)
                if single_kind in _DATE_INVALID_FORMAT_KINDS:
                    return "NULL"
                translated, _ = self._translate_datetime_format_literal(fmt_arg.value)
                fmt_sql = "'" + translated.replace("'", "''") + "'"
            else:
                fmt_sql = args_sql[1]
            return self._emit_date_format_call(value_sql=args_sql[0], format_sql=fmt_sql)
        if name == "datetime_format" and len(expr.args) >= 2:
            value_sql = args_sql[0]
            fmt_arg = expr.args[1]
            if len(expr.args) >= 3:
                tz_sql = args_sql[2]
                value_sql = self._convert_datetime_timezone_for_output(value_sql=value_sql, tz_sql=tz_sql)
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                single_kind = _single_field_kind(fmt_arg.value)
                if single_kind == "tz_name":
                    return "'UTC'"
                if single_kind == "tz_offset":
                    return "'+00:00'"
                if single_kind == "millisecond":
                    return self._emit_datetime_millisecond(value_sql)
                translated, _ = self._translate_datetime_format_literal(fmt_arg.value)
                fmt_sql = "'" + translated.replace("'", "''") + "'"
            else:
                fmt_sql = args_sql[1]
            return self._emit_datetime_format_call(value_sql=value_sql, format_sql=fmt_sql)
        if name == "parse_date" and len(expr.args) == 2:
            fmt_arg = expr.args[1]
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                translated, _ = self._translate_datetime_format_literal(fmt_arg.value)
                fmt_sql = "'" + translated.replace("'", "''") + "'"
                return self._emit_parse_date(value_sql=args_sql[0], format_sql=fmt_sql)
        if name == "parse_datetime" and len(expr.args) == 2:
            fmt_arg = expr.args[1]
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                translated, tokens = self._translate_datetime_format_literal(fmt_arg.value)
                fmt_sql = "'" + translated.replace("'", "''") + "'"
                return self._emit_parse_datetime(value_sql=args_sql[0], format_sql=fmt_sql, tokens=tokens)
        if name == "construct_datetime_ms_tz" and len(args_sql) == 8:
            return self._emit_construct_datetime_ms_tz(args_sql=args_sql)
        if name == "construct_date" and len(args_sql) == 3:
            return self._emit_construct_date(args_sql=args_sql)
        if name == "construct_date_from_datetime" and len(args_sql) == 2:
            return self._emit_construct_date_from_datetime(value_sql=args_sql[0], tz_sql=args_sql[1])
        if name == "substring" and len(args_sql) in {2, 3}:
            start = f"CAST({args_sql[1]} AS BIGINT)"
            if len(args_sql) == 2:
                return f"SUBSTRING({args_sql[0]}, ({start} + 1))"
            stop = f"CAST({args_sql[2]} AS BIGINT)"
            return f"SUBSTRING({args_sql[0]}, ({start} + 1), ({stop} - {start}))"
        if name in {"split_part", "split", "str_split"} and len(args_sql) == 3:
            index_sql = f"(CAST({args_sql[2]} AS BIGINT) + 1)"
            return f"SPLIT_PART({args_sql[0]}, {args_sql[1]}, {index_sql})"

        rendered_args = ", ".join(args_sql)
        if expr.distinct and rendered_args:
            rendered_args = f"DISTINCT {rendered_args}"
        return f"{expr.name.upper()}({rendered_args})"

    def _translate_datetime_format_literal(self, fmt: str) -> tuple[str, set[str]]:
        return _translate_duckdb_format(fmt)

    def _emit_date_add_or_subtract(
        self,
        *,
        op_name: str,
        base_sql: str,
        value_sql: str,
        unit: str,
    ) -> str:
        sign = "+" if op_name in {"date_add", "datetime_add"} else "-"
        if unit == "NANOSECOND":
            return f"({base_sql} {sign} (({value_sql}) * INTERVAL 1 MICROSECOND) / 1000)"
        if unit == "MILLISECOND":
            signed_value = f"({value_sql})" if sign == "+" else f"(-({value_sql}))"
            day_part = f"CAST(({signed_value}) / 86400000 AS BIGINT)"
            ms_part = f"CAST(({signed_value}) % 86400000 AS BIGINT)"
            return (
                f"({base_sql} + ({day_part}) * INTERVAL 1 DAY "
                f"+ ({ms_part}) * INTERVAL 1 MILLISECOND)"
            )
        return f"({base_sql} {sign} ({value_sql}) * INTERVAL 1 {unit})"

    def _emit_regex_match(self, *, pattern_sql: str, value_sql: str) -> str:
        return f"REGEXP_MATCHES({value_sql}, {pattern_sql})"

    def _emit_iso_weekday(self, value_sql: str) -> str:
        return f"DATE_PART('isodow', {value_sql})"

    def _convert_datetime_timezone_for_output(self, *, value_sql: str, tz_sql: str) -> str:
        return f"TIMEZONE({tz_sql}, TIMEZONE('UTC', {value_sql}))"

    def _emit_dates_period_days(self, *, start_sql: str, end_sql: str) -> str:
        return f"DATE_DIFF('day', {start_sql}, {end_sql})"

    def _emit_datetimes_period_milliseconds(self, *, start_sql: str, end_sql: str) -> str:
        return f"DATE_DIFF('millisecond', {start_sql}, {end_sql})"

    def _emit_date_diff_fn(self, *, unit_sql: str, start_sql: str, end_sql: str) -> str:
        return f"DATE_DIFF({unit_sql}, {start_sql}, {end_sql})"

    def _emit_date_format_call(self, *, value_sql: str, format_sql: str) -> str:
        return f"STRFTIME({value_sql}, {format_sql})"

    def _emit_datetime_format_call(self, *, value_sql: str, format_sql: str) -> str:
        return f"STRFTIME({value_sql}, {format_sql})"

    def _emit_datetime_millisecond(self, value_sql: str) -> str:
        return f"CAST((CAST(DATE_PART('milliseconds', {value_sql}) AS BIGINT) % 1000) AS VARCHAR)"

    def _emit_parse_date(self, *, value_sql: str, format_sql: str) -> str:
        return f"CAST(STRPTIME({value_sql}, {format_sql}) AS DATE)"

    def _emit_parse_datetime(self, *, value_sql: str, format_sql: str, tokens: set[str]) -> str:
        parsed = f"STRPTIME({value_sql}, {format_sql})"
        if tokens & _TZ_PATTERN_KINDS:
            return f"CAST(TIMEZONE('UTC', {parsed}) AS TIMESTAMP)"
        return f"CAST({parsed} AS TIMESTAMP)"

    def _emit_construct_datetime_ms_tz(self, *, args_sql: list[str]) -> str:
        sec_with_fraction = f"(CAST({args_sql[5]} AS DOUBLE) + CAST({args_sql[6]} AS DOUBLE) / 1000.0)"
        local_ts = (
            "MAKE_TIMESTAMP("
            f"CAST({args_sql[0]} AS BIGINT), "
            f"CAST({args_sql[1]} AS BIGINT), "
            f"CAST({args_sql[2]} AS BIGINT), "
            f"CAST({args_sql[3]} AS BIGINT), "
            f"CAST({args_sql[4]} AS BIGINT), "
            f"{sec_with_fraction})"
        )
        return f"CAST(TIMEZONE('UTC', TIMEZONE({args_sql[7]}, {local_ts})) AS TIMESTAMP)"

    def _emit_construct_date(self, *, args_sql: list[str]) -> str:
        return (
            "MAKE_DATE("
            f"CAST({args_sql[0]} AS BIGINT), "
            f"CAST({args_sql[1]} AS BIGINT), "
            f"CAST({args_sql[2]} AS BIGINT))"
        )

    def _emit_construct_date_from_datetime(self, *, value_sql: str, tz_sql: str) -> str:
        return f"CAST(TIMEZONE({tz_sql}, TIMEZONE('UTC', {value_sql})) AS DATE)"

    def _emit_count_distinct_row(self, row_args: tuple[SQL2Expr, ...]) -> str | None:
        return None

    def _emit_window(self, expr: SQL2WindowExpr) -> str:
        args_sql = ", ".join(self._emit_expr(arg) for arg in expr.args)
        if expr.distinct and args_sql:
            args_sql = f"DISTINCT {args_sql}"
        fn_sql = f"{expr.name.upper()}({args_sql})"

        parts: list[str] = []
        if expr.partition_by:
            parts.append("PARTITION BY " + ", ".join(self._emit_expr(part) for part in expr.partition_by))
        if expr.order_by:
            parts.append(
                "ORDER BY "
                + ", ".join(
                    self._emit_order_item(item)
                    for item in expr.order_by
                )
            )
        over_sql = " ".join(parts)
        if over_sql:
            return f"{fn_sql} OVER ({over_sql})"
        return f"{fn_sql} OVER ()"

    def _emit_order_item(self, item: SQL2OrderByExpr) -> str:
        direction = "DESC" if item.descending else "ASC"
        return f"{self._emit_expr(item.expr)} {direction}"

    def _extract_period(self, period_expr: SQL2Expr) -> tuple[str, str] | None:
        # Period constructor outputs can be type-coerced (for example,
        # CAST(WEEK(x) AS DECIMAL(...))). Strip casts so we preserve units.
        while isinstance(period_expr, SQL2CastExpr):
            period_expr = period_expr.expr
        if not isinstance(period_expr, SQL2FunctionExpr) or len(period_expr.args) != 1:
            return None
        unit = period_expr.name.upper()
        allowed_units = {
            "NANOSECOND",
            "MICROSECOND",
            "MILLISECOND",
            "SECOND",
            "MINUTE",
            "HOUR",
            "DAY",
            "WEEK",
            "MONTH",
            "YEAR",
        }
        if unit not in allowed_units:
            return None
        return self._emit_expr(period_expr.args[0]), unit

    def _wrap_query(self, query: SQL2Query) -> str:
        return f"(\n{self.emit_query(query)}\n)"

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

    def _qid_path(self, path: str) -> str:
        return ".".join(self._qid(part) for part in path.split("."))

    def _quote_identifier(self, name: str) -> str:
        return self._qid(name)


class SQL2SnowflakeEmitter(SQL2Emitter):
    """Snowflake-targeted SQL2 emitter."""

    def emit(self, operation: SQL2CreateViewOp | SQL2Query) -> str:
        if isinstance(operation, SQL2CreateViewOp):
            # Snowflake can produce incorrect results on some self-join + inequality
            # patterns when relation definitions remain deeply nested views. Materialize
            # install-time relations as tables and keep query-time SQL mechanical.
            return f"CREATE OR REPLACE TABLE {self._qid_path(operation.name)} AS\n{self.emit_query(operation.query)}"
        return self.emit_query(operation)

    def _emit_select(self, query: SQL2SelectQuery) -> str:
        range_sql = self._emit_range_unnest_select(query)
        if range_sql is not None:
            return range_sql
        return super()._emit_select(query)

    def _emit_range_unnest_select(self, query: SQL2SelectQuery) -> str | None:
        # Snowflake has no UNNEST(RANGE(...)) scalar form. Lowering emits this
        # shape for std range/split helpers, so rewrite it to FLATTEN over
        # ARRAY_GENERATE_RANGE.
        if (
            query.from_item is not None
            or query.joins
            or query.where
            or query.group_by
            or query.having
            or query.distinct
            or len(query.select_items) != 1
        ):
            return None
        item = query.select_items[0]
        expr = item.expr
        if not isinstance(expr, SQL2FunctionExpr) or expr.name.lower() != "unnest" or len(expr.args) != 1:
            return None
        range_expr = expr.args[0]
        if not isinstance(range_expr, SQL2FunctionExpr) or range_expr.name.lower() != "range" or len(range_expr.args) != 3:
            return None

        start_sql = self._emit_expr(range_expr.args[0])
        stop_sql = self._emit_expr(range_expr.args[1])
        step_sql = self._emit_expr(range_expr.args[2])
        alias = item.alias or "value"
        return (
            "SELECT\n"
            f"  CAST(\"_f\".\"VALUE\" AS BIGINT) AS {self._qid(alias)}\n"
            "FROM TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE("
            f"CAST({start_sql} AS BIGINT), "
            f"CAST({stop_sql} AS BIGINT), "
            f"CAST({step_sql} AS BIGINT)"
            "))) AS \"_f\""
        )

    def _emit_function(self, expr: SQL2FunctionExpr) -> str:
        name = expr.name.lower()
        args_sql = [self._emit_expr(arg) for arg in expr.args]

        if name == "levenshtein" and len(args_sql) == 2:
            return f"EDITDISTANCE({args_sql[0]}, {args_sql[1]})"
        if name in {"string_split", "str_split"} and len(args_sql) == 2:
            return f"SPLIT({args_sql[0]}, {args_sql[1]})"
        if name == "array_length" and len(args_sql) == 1:
            return f"ARRAY_SIZE({args_sql[0]})"
        if name == "isinf" and len(args_sql) == 1:
            return (
                "COALESCE(UPPER(TO_VARCHAR("
                f"{args_sql[0]}"
                ")) IN ('INF', '-INF', 'INFINITY', '-INFINITY'), FALSE)"
            )
        if name == "isnan" and len(args_sql) == 1:
            return (
                f"(({args_sql[0]}) IS NULL OR "
                "COALESCE(UPPER(TO_VARCHAR("
                f"{args_sql[0]}"
                ")) = 'NAN', FALSE))"
            )
        if name == "acot" and len(args_sql) == 1:
            return (
                f"CASE WHEN ({args_sql[0]}) = 0 THEN PI() / 2 "
                f"ELSE ATAN(1 / TO_DOUBLE({args_sql[0]})) END"
            )
        if name == "date_format" and len(expr.args) == 2:
            fmt_arg = expr.args[1]
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                single_kind = _single_field_kind(fmt_arg.value)
                if single_kind in _DATE_INVALID_FORMAT_KINDS:
                    return "NULL"
                if self._snowflake_format_requires_manual(fmt_arg.value):
                    return self._emit_snowflake_manual_format(value_sql=args_sql[0], fmt=fmt_arg.value)
        if name == "datetime_format" and len(expr.args) >= 2:
            value_sql = args_sql[0]
            fmt_arg = expr.args[1]
            if len(expr.args) >= 3:
                tz_sql = args_sql[2]
                value_sql = self._convert_datetime_timezone_for_output(value_sql=value_sql, tz_sql=tz_sql)
            if isinstance(fmt_arg, SQL2LiteralExpr) and isinstance(fmt_arg.value, str):
                single_kind = _single_field_kind(fmt_arg.value)
                if single_kind == "tz_name":
                    return "'UTC'"
                if single_kind == "tz_offset":
                    return "'+00:00'"
                if single_kind == "millisecond":
                    return self._emit_datetime_millisecond(value_sql)
                if self._snowflake_format_requires_manual(fmt_arg.value):
                    return self._emit_snowflake_manual_format(value_sql=value_sql, fmt=fmt_arg.value)

        return super()._emit_function(expr)

    def _snowflake_format_requires_manual(self, fmt: str) -> bool:
        for part in parse_julia_datetime_format(fmt):
            if not isinstance(part, DateFormatField):
                continue
            if part.kind.endswith("_unpadded") or part.kind in {"weekday_full", "month_full"}:
                return True
        return False

    def _emit_snowflake_manual_format(self, *, value_sql: str, fmt: str) -> str:
        part_sql: list[str] = []
        token_sql_by_kind = {
            "weekday_abbr": "DY",
            "month_abbr": "MON",
            "tz_offset": "TZHTZM",
            "tz_name": "TZD",
            "ampm": "AM",
            "year_2": "YY",
        }
        for part in parse_julia_datetime_format(fmt):
            if isinstance(part, DateFormatLiteral):
                text = part.text.replace("'", "''")
                part_sql.append(f"'{text}'")
                continue
            assert isinstance(part, DateFormatField)
            kind = part.kind
            if kind == "year_4":
                part_sql.append(f"TO_VARCHAR(EXTRACT(YEAR FROM {value_sql}))")
            elif kind == "year_2":
                part_sql.append(f"LPAD(TO_VARCHAR(MOD(EXTRACT(YEAR FROM {value_sql}), 100)), 2, '0')")
            elif kind == "month_unpadded":
                part_sql.append(f"TO_VARCHAR(EXTRACT(MONTH FROM {value_sql}))")
            elif kind == "month_padded":
                part_sql.append(f"LPAD(TO_VARCHAR(EXTRACT(MONTH FROM {value_sql})), 2, '0')")
            elif kind == "day_unpadded":
                part_sql.append(f"TO_VARCHAR(EXTRACT(DAY FROM {value_sql}))")
            elif kind == "day_padded":
                part_sql.append(f"LPAD(TO_VARCHAR(EXTRACT(DAY FROM {value_sql})), 2, '0')")
            elif kind == "hour24_unpadded":
                part_sql.append(f"TO_VARCHAR(EXTRACT(HOUR FROM {value_sql}))")
            elif kind == "hour24_padded":
                part_sql.append(f"LPAD(TO_VARCHAR(EXTRACT(HOUR FROM {value_sql})), 2, '0')")
            elif kind == "minute_unpadded":
                part_sql.append(f"TO_VARCHAR(EXTRACT(MINUTE FROM {value_sql}))")
            elif kind == "minute_padded":
                part_sql.append(f"LPAD(TO_VARCHAR(EXTRACT(MINUTE FROM {value_sql})), 2, '0')")
            elif kind == "second_unpadded":
                part_sql.append(f"TO_VARCHAR(EXTRACT(SECOND FROM {value_sql}))")
            elif kind == "second_padded":
                part_sql.append(f"LPAD(TO_VARCHAR(EXTRACT(SECOND FROM {value_sql})), 2, '0')")
            elif kind == "hour12_unpadded":
                part_sql.append(
                    f"TO_VARCHAR(IFF(MOD(EXTRACT(HOUR FROM {value_sql}), 12) = 0, 12, MOD(EXTRACT(HOUR FROM {value_sql}), 12)))"
                )
            elif kind == "hour12_padded":
                part_sql.append(
                    f"LPAD(TO_VARCHAR(IFF(MOD(EXTRACT(HOUR FROM {value_sql}), 12) = 0, 12, MOD(EXTRACT(HOUR FROM {value_sql}), 12))), 2, '0')"
                )
            elif kind == "weekday_full":
                part_sql.append(
                    f"CASE DAYOFWEEKISO({value_sql}) "
                    "WHEN 1 THEN 'Monday' WHEN 2 THEN 'Tuesday' WHEN 3 THEN 'Wednesday' "
                    "WHEN 4 THEN 'Thursday' WHEN 5 THEN 'Friday' WHEN 6 THEN 'Saturday' "
                    "ELSE 'Sunday' END"
                )
            elif kind == "month_full":
                part_sql.append(
                    f"CASE EXTRACT(MONTH FROM {value_sql}) "
                    "WHEN 1 THEN 'January' WHEN 2 THEN 'February' WHEN 3 THEN 'March' "
                    "WHEN 4 THEN 'April' WHEN 5 THEN 'May' WHEN 6 THEN 'June' "
                    "WHEN 7 THEN 'July' WHEN 8 THEN 'August' WHEN 9 THEN 'September' "
                    "WHEN 10 THEN 'October' WHEN 11 THEN 'November' ELSE 'December' END"
                )
            else:
                token = token_sql_by_kind.get(kind)
                if token is None:
                    translated, _ = self._translate_datetime_format_literal(fmt)
                    escaped = translated.replace("'", "''")
                    return f"TO_VARCHAR({value_sql}, '{escaped}')"
                part_sql.append(f"TO_VARCHAR({value_sql}, '{token}')")
        if not part_sql:
            return "''"
        return " || ".join(part_sql)

    def _requires_from_union_wrapper(self, item: SQL2FromItem) -> bool:
        return item.lateral

    def _qid_path(self, path: str) -> str:
        return ".".join(self._snowflake_path_identifier(part) for part in path.split("."))

    def _snowflake_path_identifier(self, name: str) -> str:
        token = name.strip()
        if token == "*":
            return token
        quote_identifier = getattr(self.dialect, "quote_identifier", None)
        if callable(quote_identifier):
            return cast(str, quote_identifier(token))
        if token.startswith('"') and token.endswith('"'):
            return token
        if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_$]*", token):
            return token.upper()
        return self._qid(token)

    def _translate_datetime_format_literal(self, fmt: str) -> tuple[str, set[str]]:
        return _translate_snowflake_format(fmt)

    def _emit_date_add_or_subtract(
        self,
        *,
        op_name: str,
        base_sql: str,
        value_sql: str,
        unit: str,
    ) -> str:
        sign = 1 if op_name in {"date_add", "datetime_add"} else -1
        return f"DATEADD({unit.lower()}, ({sign} * ({value_sql})), {base_sql})"

    def _emit_regex_match(self, *, pattern_sql: str, value_sql: str) -> str:
        return f"REGEXP_LIKE({value_sql}, {pattern_sql})"

    def _emit_iso_weekday(self, value_sql: str) -> str:
        return f"DAYOFWEEKISO({value_sql})"

    def _convert_datetime_timezone_for_output(self, *, value_sql: str, tz_sql: str) -> str:
        return f"CONVERT_TIMEZONE('UTC', {tz_sql}, {value_sql})"

    def _emit_dates_period_days(self, *, start_sql: str, end_sql: str) -> str:
        return f"DATEDIFF(day, {start_sql}, {end_sql})"

    def _emit_datetimes_period_milliseconds(self, *, start_sql: str, end_sql: str) -> str:
        return f"DATEDIFF(millisecond, {start_sql}, {end_sql})"

    def _emit_date_diff_fn(self, *, unit_sql: str, start_sql: str, end_sql: str) -> str:
        return f"DATEDIFF({unit_sql}, {start_sql}, {end_sql})"

    def _emit_date_format_call(self, *, value_sql: str, format_sql: str) -> str:
        return f"TO_VARCHAR({value_sql}, {format_sql})"

    def _emit_datetime_format_call(self, *, value_sql: str, format_sql: str) -> str:
        return f"TO_VARCHAR({value_sql}, {format_sql})"

    def _emit_datetime_millisecond(self, value_sql: str) -> str:
        frac = f"TO_VARCHAR({value_sql}, 'FF3')"
        return f"IFF({frac} = '000', '0', LTRIM({frac}, '0'))"

    def _emit_parse_date(self, *, value_sql: str, format_sql: str) -> str:
        return f"TRY_TO_DATE({value_sql}, {format_sql})"

    def _emit_parse_datetime(self, *, value_sql: str, format_sql: str, tokens: set[str]) -> str:
        if tokens & _TZ_PATTERN_KINDS:
            return f"CAST(CONVERT_TIMEZONE('UTC', TRY_TO_TIMESTAMP_TZ({value_sql}, {format_sql})) AS TIMESTAMP_NTZ)"
        return f"CAST(TRY_TO_TIMESTAMP_NTZ({value_sql}, {format_sql}) AS TIMESTAMP_NTZ)"

    def _emit_construct_datetime_ms_tz(self, *, args_sql: list[str]) -> str:
        local_ts = (
            "TO_TIMESTAMP_NTZ("
            f"LPAD(CAST({args_sql[0]} AS VARCHAR), 4, '0') || '-' || "
            f"LPAD(CAST({args_sql[1]} AS VARCHAR), 2, '0') || '-' || "
            f"LPAD(CAST({args_sql[2]} AS VARCHAR), 2, '0') || ' ' || "
            f"LPAD(CAST({args_sql[3]} AS VARCHAR), 2, '0') || ':' || "
            f"LPAD(CAST({args_sql[4]} AS VARCHAR), 2, '0') || ':' || "
            f"LPAD(CAST({args_sql[5]} AS VARCHAR), 2, '0') || '.' || "
            f"LPAD(CAST({args_sql[6]} AS VARCHAR), 3, '0'), "
            "'YYYY-MM-DD HH24:MI:SS.FF3')"
        )
        return f"CAST(CONVERT_TIMEZONE({args_sql[7]}, 'UTC', {local_ts}) AS TIMESTAMP_NTZ)"

    def _emit_construct_date(self, *, args_sql: list[str]) -> str:
        return (
            "DATE_FROM_PARTS("
            f"CAST({args_sql[0]} AS BIGINT), "
            f"CAST({args_sql[1]} AS BIGINT), "
            f"CAST({args_sql[2]} AS BIGINT))"
        )

    def _emit_construct_date_from_datetime(self, *, value_sql: str, tz_sql: str) -> str:
        return f"CAST(CONVERT_TIMEZONE('UTC', {tz_sql}, {value_sql}) AS DATE)"

    def _emit_count_distinct_row(self, row_args: tuple[SQL2Expr, ...]) -> str | None:
        if not row_args:
            return "COUNT(*)"
        rendered_args = ", ".join(self._emit_expr(arg) for arg in row_args)
        return f"COUNT(DISTINCT {rendered_args})"
