from __future__ import annotations

import typing as t

from sqlglot import exp, generator
from sqlglot.dialects.dialect import (
    DATE_ADD_OR_SUB,
    Dialect,
    groupconcat_sql,
    no_last_day_sql,
    rename_func,
    strposition_sql,
    timestrtotime_sql,
    timestamptrunc_sql,
)
from sqlglot.errors import UnsupportedError
from sqlglot.generator import unsupported_args
from sqlglot.optimizer.scope import build_scope, find_in_scope
from sqlglot.parsers.exasol import DATE_UNITS


def _sha2_sql(self: ExasolGenerator, expression: exp.SHA2) -> str:
    length = expression.text("length")
    func_name = "HASH_SHA256" if length == "256" else "HASH_SHA512"
    return self.func(func_name, expression.this)


def _date_diff_sql(self: ExasolGenerator, expression: exp.DateDiff | exp.TsOrDsDiff) -> str:
    unit = expression.text("unit").upper() or "DAY"

    if unit not in DATE_UNITS:
        self.unsupported(f"'{unit}' is not supported in Exasol.")
        return self.function_fallback_sql(expression)

    return self.func(f"{unit}S_BETWEEN", expression.this, expression.expression)


# https://docs.exasol.com/db/latest/sql/select.htm#:~:text=If%20you%20have,local.x%3E10
def _add_local_prefix_for_aliases(expression: exp.Expr, dialect: Dialect) -> exp.Expr:
    def _key(ident: exp.Identifier) -> str:
        return dialect.normalize_identifier(ident.copy()).this

    if isinstance(expression, exp.Select):
        aliases: dict[str, exp.Identifier] = {
            _key(alias): alias
            for sel in expression.selects
            if isinstance(sel, exp.Alias) and (alias := sel.args.get("alias"))
        }

        table = expression.find(exp.Table)
        table_ident = table.this if table else None

        if (
            table_ident
            and table_ident.name.upper() == "LOCAL"
            and not bool(table_ident.args.get("quoted"))
        ):
            table_ident.replace(exp.to_identifier(table_ident.name.upper(), quoted=True))

        def prefix_local(node: exp.Expr, visible_aliases: dict[str, exp.Identifier]) -> exp.Expr:
            if isinstance(node, exp.Column) and not node.table:
                alias = visible_aliases.get(_key(node.this))
                if alias is not None:
                    return exp.Column(
                        this=alias,
                        table=exp.to_identifier("LOCAL", quoted=False),
                    )
            return node

        for key in ("where", "group", "having"):
            if arg := expression.args.get(key):
                expression.set(key, arg.transform(lambda node: prefix_local(node, aliases)))

        seen_aliases: dict[str, exp.Identifier] = {}
        new_selects: list[exp.Expr] = []
        for sel in expression.selects:
            if isinstance(sel, exp.Alias):
                inner = sel.this.transform(lambda node: prefix_local(node, seen_aliases))
                sel.set("this", inner)

                if alias_node := sel.args.get("alias"):
                    seen_aliases[_key(alias_node)] = alias_node
                new_selects.append(sel)
            else:
                new_selects.append(sel.transform(lambda node: prefix_local(node, seen_aliases)))
        expression.set("expressions", new_selects)

    return expression


def _trunc_sql(
    self: ExasolGenerator, kind: str, expression: exp.DateTrunc | exp.TimestampTrunc
) -> str:
    unit_expr = expression.args.get("unit")
    unit = (
        self.weekstart_name(unit_expr)
        if isinstance(unit_expr, exp.WeekStart)
        else expression.text("unit")
    )
    node = expression.this.this if isinstance(expression.this, exp.Cast) else expression.this
    expr_sql = self.sql(node)
    if isinstance(node, exp.Literal) and node.is_string:
        expr_sql = (
            f"{kind} '{node.this.replace('T', ' ')}'"
            if kind == "TIMESTAMP"
            else f"DATE '{node.this}'"
        )
    return f"DATE_TRUNC('{unit}', {expr_sql})"


def _date_trunc_sql(self: ExasolGenerator, expression: exp.DateTrunc) -> str:
    return _trunc_sql(self, "DATE", expression)


def _timestamp_trunc_sql(
    self: ExasolGenerator, expression: exp.DateTrunc | exp.TimestampTrunc
) -> str:
    return _trunc_sql(self, "TIMESTAMP", expression)


def is_case_insensitive(node: exp.Expr) -> bool:
    return isinstance(node, exp.Collate) and node.text("expression").upper() == "UTF8_LCASE"


def _substring_index_sql(self: ExasolGenerator, expression: exp.SubstringIndex) -> str:
    this = expression.this
    delimiter = expression.args["delimiter"]
    count_node = expression.args["count"]
    count_sql = self.sql(expression, "count")
    num = count_node.to_py() if count_node.is_number else 0

    haystack_sql = self.sql(this)
    if num == 0:
        return self.func("SUBSTR", haystack_sql, "1", "0")

    from_right = num < 0
    direction = "-1" if from_right else "1"
    occur = self.func("ABS", count_sql) if from_right else count_sql

    delimiter_sql = self.sql(delimiter)

    position = self.func(
        "INSTR",
        self.func("LOWER", haystack_sql) if is_case_insensitive(this) else haystack_sql,
        self.func("LOWER", delimiter_sql) if is_case_insensitive(delimiter) else delimiter_sql,
        direction,
        occur,
    )
    nullable_pos = self.func("NULLIF", position, "0")

    if from_right:
        start = self.func(
            "NVL", f"{nullable_pos} + {self.func('LENGTH', delimiter_sql)}", direction
        )
        return self.func("SUBSTR", haystack_sql, start)

    length = self.func("NVL", f"{nullable_pos} - 1", self.func("LENGTH", haystack_sql))
    return self.func("SUBSTR", haystack_sql, direction, length)


# https://docs.exasol.com/db/latest/sql/select.htm#:~:text=The%20select_list%20defines%20the%20columns%20of%20the%20result%20table.%20If%20*%20is%20used%2C%20all%20columns%20are%20listed.%20You%20can%20use%20an%20expression%20like%20t.*%20to%20list%20all%20columns%20of%20the%20table%20t%2C%20the%20view%20t%2C%20or%20the%20object%20with%20the%20table%20alias%20t.
def _qualify_unscoped_star(expression: exp.Expr) -> exp.Expr:
    """
    Exasol doesn't support a bare * alongside other select items, so we rewrite it
    Rewrite: SELECT *, <other> FROM <Table>
    Into: SELECT T.*, <other> FROM <Table> AS T
    """

    if not isinstance(expression, exp.Select):
        return expression

    select_expressions = expression.expressions or []

    def is_bare_star(expr: exp.Expr) -> bool:
        return isinstance(expr, exp.Star) and expr.this is None

    has_other_expression = False
    bare_star_expr: exp.Expr | None = None
    for expr in select_expressions:
        has_bare_star = is_bare_star(expr)
        if has_bare_star and bare_star_expr is None:
            bare_star_expr = expr
        elif not has_bare_star:
            has_other_expression = True
        if bare_star_expr and has_other_expression:
            break

    if not (bare_star_expr and has_other_expression):
        return expression

    scope = build_scope(expression)

    if not scope or not scope.selected_sources:
        return expression

    table_identifiers: list[exp.Identifier] = []

    for source_name, (source_expr, _) in scope.selected_sources.items():
        ident = (
            source_expr.this.copy()
            if isinstance(source_expr, exp.Table) and isinstance(source_expr.this, exp.Identifier)
            else exp.to_identifier(source_name)
        )
        table_identifiers.append(ident)

    qualified_star_columns = [
        exp.Column(this=bare_star_expr.copy(), table=ident) for ident in table_identifiers
    ]

    new_select_expressions: list[exp.Expr] = []

    for select_expr in select_expressions:
        new_select_expressions.extend(qualified_star_columns) if is_bare_star(
            select_expr
        ) else new_select_expressions.append(select_expr)

    expression.set("expressions", new_select_expressions)
    return expression


def _add_date_sql(self: ExasolGenerator, expression: DATE_ADD_OR_SUB) -> str:
    interval = expression.expression if isinstance(expression.expression, exp.Interval) else None

    unit = (
        (interval.text("unit") or "DAY").upper()
        if interval is not None
        else (expression.text("unit") or "DAY").upper()
    )

    if unit not in DATE_UNITS:
        self.unsupported(f"'{unit}' is not supported in Exasol.")
        return self.function_fallback_sql(expression)

    offset_expr: exp.Expr = expression.expression
    if interval is not None:
        offset_expr = interval.this

    if isinstance(expression, exp.DateSub):
        offset_expr = exp.Neg(this=offset_expr)

    return self.func(f"ADD_{unit}S", expression.this, offset_expr)


def _group_by_all(expression: exp.Expr) -> exp.Expr:
    if not isinstance(expression, exp.Select):
        return expression

    group = expression.args.get("group")
    if not group or not group.args.get("all"):
        return expression

    if expression.is_star:
        if any(find_in_scope(proj, exp.AggFunc) for proj in expression.expressions):
            raise UnsupportedError(
                "GROUP BY ALL with star projection and aggregates is not supported by Exasol"
            )
        expression.set("distinct", exp.Distinct())
        expression.set("group", None)
        return expression

    group_positions = [
        exp.Literal.number(i)
        for i, proj in enumerate(expression.expressions, start=1)
        if not find_in_scope(proj, exp.AggFunc)
    ]

    if not group_positions:
        expression.set("group", None)
        return expression

    group.set("expressions", group_positions)
    group.set("all", None)

    return expression


class ExasolGenerator(generator.Generator):
    SELECT_KINDS: tuple[str, ...] = ()
    TRY_SUPPORTED = False
    SUPPORTS_UESCAPE = False
    SUPPORTS_DECODE_CASE = False

    AFTER_HAVING_MODIFIER_TRANSFORMS = generator.AFTER_HAVING_MODIFIER_TRANSFORMS

    # https://docs.exasol.com/db/latest/sql_references/data_types/datatypedetails.htm#StringDataType
    STRING_TYPE_MAPPING: t.ClassVar = {
        exp.DType.BLOB: "VARCHAR",
        exp.DType.LONGBLOB: "VARCHAR",
        exp.DType.LONGTEXT: "VARCHAR",
        exp.DType.MEDIUMBLOB: "VARCHAR",
        exp.DType.MEDIUMTEXT: "VARCHAR",
        exp.DType.TINYBLOB: "VARCHAR",
        exp.DType.TINYTEXT: "VARCHAR",
        # https://docs.exasol.com/db/latest/sql_references/data_types/datatypealiases.htm
        exp.DType.TEXT: "LONG VARCHAR",
        exp.DType.VARBINARY: "VARCHAR",
    }

    # https://docs.exasol.com/db/latest/sql_references/data_types/datatypealiases.htm
    TYPE_MAPPING = {
        **generator.Generator.TYPE_MAPPING,
        **STRING_TYPE_MAPPING,
        exp.DType.TINYINT: "SMALLINT",
        exp.DType.MEDIUMINT: "INT",
        exp.DType.DECIMAL32: "DECIMAL",
        exp.DType.DECIMAL64: "DECIMAL",
        exp.DType.DECIMAL128: "DECIMAL",
        exp.DType.DECIMAL256: "DECIMAL",
        exp.DType.DATETIME: "TIMESTAMP",
        exp.DType.TIMESTAMPTZ: "TIMESTAMP",
        exp.DType.TIMESTAMPLTZ: "TIMESTAMP",
        exp.DType.TIMESTAMPNTZ: "TIMESTAMP",
    }

    def select_sql(self, expression: exp.Select) -> str:
        processed = _qualify_unscoped_star(expression)
        processed = _add_local_prefix_for_aliases(processed, self.dialect)
        processed = _group_by_all(processed)
        return super().select_sql(t.cast(exp.Select, processed))

    def datatype_sql(self, expression: exp.DataType) -> str:
        # Exasol supports a fixed default precision of 3 for TIMESTAMP WITH LOCAL TIME ZONE
        # and does not allow specifying a different custom precision
        if expression.is_type(exp.DType.TIMESTAMPLTZ):
            return "TIMESTAMP WITH LOCAL TIME ZONE"

        return super().datatype_sql(expression)

    TRANSFORMS = {
        **generator.Generator.TRANSFORMS,
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/every.htm
        exp.All: rename_func("EVERY"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_and.htm
        exp.BitwiseAnd: rename_func("BIT_AND"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_or.htm
        exp.BitwiseOr: rename_func("BIT_OR"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_not.htm
        exp.BitwiseNot: rename_func("BIT_NOT"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_lshift.htm
        exp.BitwiseLeftShift: rename_func("BIT_LSHIFT"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_rshift.htm
        exp.BitwiseRightShift: rename_func("BIT_RSHIFT"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/bit_xor.htm
        exp.BitwiseXor: rename_func("BIT_XOR"),
        exp.CurrentSchema: lambda *_: "CURRENT_SCHEMA",
        exp.DateDiff: _date_diff_sql,
        exp.DateAdd: _add_date_sql,
        exp.TsOrDsAdd: _add_date_sql,
        exp.DateSub: _add_date_sql,
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/div.htm#DIV
        exp.IntDiv: rename_func("DIV"),
        exp.TsOrDsDiff: _date_diff_sql,
        exp.DateTrunc: _date_trunc_sql,
        exp.DayOfWeek: lambda self, e: f"CAST(TO_CHAR({self.sql(e, 'this')}, 'D') AS INTEGER)",
        exp.DatetimeTrunc: timestamptrunc_sql(),
        exp.GroupConcat: lambda self, e: groupconcat_sql(self, e, on_overflow=True),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/edit_distance.htm#EDIT_DISTANCE
        exp.Levenshtein: unsupported_args("ins_cost", "del_cost", "sub_cost", "max_dist")(
            rename_func("EDIT_DISTANCE")
        ),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/mod.htm
        exp.Mod: rename_func("MOD"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/from_posix_time.htm
        exp.UnixToTime: lambda self, e: self.func("FROM_POSIX_TIME", e.this),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/rank.htm
        exp.Rank: unsupported_args("expressions")(lambda *_: "RANK()"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/dense_rank.htm
        exp.DenseRank: unsupported_args("expressions")(lambda *_: "DENSE_RANK()"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/regexp_substr.htm
        exp.RegexpExtract: unsupported_args("parameters", "group")(rename_func("REGEXP_SUBSTR")),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/regexp_replace.htm
        exp.RegexpReplace: unsupported_args("modifiers")(rename_func("REGEXP_REPLACE")),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/var_pop.htm
        exp.VariancePop: rename_func("VAR_POP"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/approximate_count_distinct.htm
        exp.ApproxDistinct: unsupported_args("accuracy")(rename_func("APPROXIMATE_COUNT_DISTINCT")),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_char%20(datetime).htm
        exp.TimeToStr: lambda self, e: self.func("TO_CHAR", e.this, self.format_time(e)),
        exp.ToChar: lambda self, e: self.func("TO_CHAR", e.this, self.format_time(e)),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_date.htm
        exp.TsOrDsToDate: lambda self, e: self.func("TO_DATE", e.this, self.format_time(e)),
        exp.TimeStrToTime: timestrtotime_sql,
        exp.TimestampTrunc: _timestamp_trunc_sql,
        exp.StrToTime: lambda self, e: self.func("TO_DATE", e.this, self.format_time(e)),
        exp.CurrentUser: lambda *_: "CURRENT_USER",
        exp.AtTimeZone: lambda self, e: self.func(
            "CONVERT_TZ",
            e.this,
            "'UTC'",
            e.args.get("zone"),
        ),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/instr.htm
        exp.StrPosition: lambda self, e: strposition_sql(
            self, e, func_name="INSTR", supports_position=True, supports_occurrence=True
        ),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/hash_sha%5B1%5D.htm#HASH_SHA%5B1%5D
        exp.SHA: rename_func("HASH_SHA"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/hash_sha256.htm
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/hash_sha512.htm
        exp.SHA2: _sha2_sql,
        exp.MD5: rename_func("HASH_MD5"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/hashtype_md5.htm
        exp.MD5Digest: rename_func("HASHTYPE_MD5"),
        # https://docs.exasol.com/db/latest/sql/create_view.htm
        exp.CommentColumnConstraint: lambda self, e: f"COMMENT IS {self.sql(e, 'this')}",
        exp.SubstringIndex: _substring_index_sql,
        exp.WeekOfYear: rename_func("WEEK"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_date.htm
        exp.Date: rename_func("TO_DATE"),
        # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_timestamp.htm
        exp.Timestamp: rename_func("TO_TIMESTAMP"),
        exp.Quarter: lambda self, e: f"CEIL(MONTH(TO_DATE({self.sql(e, 'this')}))/3)",
        exp.LastDay: no_last_day_sql,
    }

    # https://docs.exasol.com/db/7.1/sql_references/system_tables/metadata/exa_sql_keywords.htm
    RESERVED_KEYWORDS = {
        "absolute",
        "action",
        "add",
        "after",
        "all",
        "allocate",
        "alter",
        "and",
        "any",
        "append",
        "are",
        "array",
        "as",
        "asc",
        "asensitive",
        "assertion",
        "at",
        "attribute",
        "authid",
        "authorization",
        "before",
        "begin",
        "between",
        "bigint",
        "binary",
        "bit",
        "blob",
        "blocked",
        "bool",
        "boolean",
        "both",
        "by",
        "byte",
        "call",
        "called",
        "cardinality",
        "cascade",
        "cascaded",
        "case",
        "casespecific",
        "cast",
        "catalog",
        "chain",
        "char",
        "character",
        "character_set_catalog",
        "character_set_name",
        "character_set_schema",
        "characteristics",
        "check",
        "checked",
        "clob",
        "close",
        "coalesce",
        "collate",
        "collation",
        "collation_catalog",
        "collation_name",
        "collation_schema",
        "column",
        "commit",
        "condition",
        "connect_by_iscycle",
        "connect_by_isleaf",
        "connect_by_root",
        "connection",
        "constant",
        "constraint",
        "constraint_state_default",
        "constraints",
        "constructor",
        "contains",
        "continue",
        "control",
        "convert",
        "corresponding",
        "create",
        "cs",
        "csv",
        "cube",
        "current",
        "current_cluster",
        "current_cluster_uid",
        "current_date",
        "current_path",
        "current_role",
        "current_schema",
        "current_session",
        "current_statement",
        "current_time",
        "current_timestamp",
        "current_user",
        "cursor",
        "cycle",
        "data",
        "datalink",
        "datetime_interval_code",
        "datetime_interval_precision",
        "day",
        "dbtimezone",
        "deallocate",
        "dec",
        "decimal",
        "declare",
        "default",
        "default_like_escape_character",
        "deferrable",
        "deferred",
        "defined",
        "definer",
        "delete",
        "deref",
        "derived",
        "desc",
        "describe",
        "descriptor",
        "deterministic",
        "disable",
        "disabled",
        "disconnect",
        "dispatch",
        "distinct",
        "dlurlcomplete",
        "dlurlpath",
        "dlurlpathonly",
        "dlurlscheme",
        "dlurlserver",
        "dlvalue",
        "do",
        "domain",
        "double",
        "drop",
        "dynamic",
        "dynamic_function",
        "dynamic_function_code",
        "each",
        "else",
        "elseif",
        "elsif",
        "emits",
        "enable",
        "enabled",
        "end",
        "end-exec",
        "endif",
        "enforce",
        "equals",
        "errors",
        "escape",
        "except",
        "exception",
        "exec",
        "execute",
        "exists",
        "exit",
        "export",
        "external",
        "extract",
        "false",
        "fbv",
        "fetch",
        "file",
        "final",
        "first",
        "float",
        "following",
        "for",
        "forall",
        "force",
        "format",
        "found",
        "free",
        "from",
        "fs",
        "full",
        "function",
        "general",
        "generated",
        "geometry",
        "get",
        "global",
        "go",
        "goto",
        "grant",
        "granted",
        "group",
        "group_concat",
        "grouping",
        "groups",
        "hashtype",
        "hashtype_format",
        "having",
        "high",
        "hold",
        "hour",
        "identity",
        "if",
        "ifnull",
        "immediate",
        "impersonate",
        "implementation",
        "import",
        "in",
        "index",
        "indicator",
        "inner",
        "inout",
        "input",
        "insensitive",
        "insert",
        "instance",
        "instantiable",
        "int",
        "integer",
        "integrity",
        "intersect",
        "interval",
        "into",
        "inverse",
        "invoker",
        "is",
        "iterate",
        "join",
        "key_member",
        "key_type",
        "large",
        "last",
        "lateral",
        "ldap",
        "leading",
        "leave",
        "left",
        "level",
        "like",
        "limit",
        "listagg",
        "localtime",
        "localtimestamp",
        "locator",
        "log",
        "longvarchar",
        "loop",
        "low",
        "map",
        "match",
        "matched",
        "merge",
        "method",
        "minus",
        "minute",
        "mod",
        "modifies",
        "modify",
        "module",
        "month",
        "names",
        "national",
        "natural",
        "nchar",
        "nclob",
        "new",
        "next",
        "nls_date_format",
        "nls_date_language",
        "nls_first_day_of_week",
        "nls_numeric_characters",
        "nls_timestamp_format",
        "no",
        "nocycle",
        "nologging",
        "none",
        "not",
        "null",
        "nullif",
        "number",
        "numeric",
        "nvarchar",
        "nvarchar2",
        "object",
        "of",
        "off",
        "old",
        "on",
        "only",
        "open",
        "option",
        "options",
        "or",
        "order",
        "ordering",
        "ordinality",
        "others",
        "out",
        "outer",
        "output",
        "over",
        "overlaps",
        "overlay",
        "overriding",
        "pad",
        "parallel_enable",
        "parameter",
        "parameter_specific_catalog",
        "parameter_specific_name",
        "parameter_specific_schema",
        "parquet",
        "partial",
        "path",
        "permission",
        "placing",
        "plus",
        "preceding",
        "preferring",
        "prepare",
        "preserve",
        "prior",
        "privileges",
        "procedure",
        "profile",
        "qualify",
        "random",
        "range",
        "read",
        "reads",
        "real",
        "recovery",
        "recursive",
        "ref",
        "references",
        "referencing",
        "refresh",
        "regexp_like",
        "relative",
        "release",
        "rename",
        "repeat",
        "replace",
        "restore",
        "restrict",
        "result",
        "return",
        "returned_length",
        "returned_octet_length",
        "returns",
        "revoke",
        "right",
        "rollback",
        "rollup",
        "routine",
        "row",
        "rows",
        "rowtype",
        "savepoint",
        "schema",
        "scope",
        "scope_user",
        "script",
        "scroll",
        "search",
        "second",
        "section",
        "security",
        "select",
        "selective",
        "self",
        "sensitive",
        "separator",
        "sequence",
        "session",
        "session_user",
        "sessiontimezone",
        "set",
        "sets",
        "shortint",
        "similar",
        "smallint",
        "some",
        "source",
        "space",
        "specific",
        "specifictype",
        "sql",
        "sql_bigint",
        "sql_bit",
        "sql_char",
        "sql_date",
        "sql_decimal",
        "sql_double",
        "sql_float",
        "sql_integer",
        "sql_longvarchar",
        "sql_numeric",
        "sql_preprocessor_script",
        "sql_real",
        "sql_smallint",
        "sql_timestamp",
        "sql_tinyint",
        "sql_type_date",
        "sql_type_timestamp",
        "sql_varchar",
        "sqlexception",
        "sqlstate",
        "sqlwarning",
        "start",
        "state",
        "statement",
        "static",
        "structure",
        "style",
        "substring",
        "subtype",
        "sysdate",
        "system",
        "system_user",
        "systimestamp",
        "table",
        "temporary",
        "text",
        "then",
        "time",
        "timestamp",
        "timezone_hour",
        "timezone_minute",
        "tinyint",
        "to",
        "trailing",
        "transaction",
        "transform",
        "transforms",
        "translation",
        "treat",
        "trigger",
        "trim",
        "true",
        "truncate",
        "under",
        "union",
        "unique",
        "unknown",
        "unlink",
        "unnest",
        "until",
        "update",
        "usage",
        "user",
        "using",
        "value",
        "values",
        "varchar",
        "varchar2",
        "varray",
        "verify",
        "view",
        "when",
        "whenever",
        "where",
        "while",
        "window",
        "with",
        "within",
        "without",
        "work",
        "year",
        "yes",
        "zone",
    }

    def converttimezone_sql(self, expression: exp.ConvertTimezone) -> str:
        from_tz = expression.args.get("source_tz")
        to_tz = expression.args.get("target_tz")
        datetime = expression.args.get("timestamp")
        options = expression.args.get("options")

        return self.func("CONVERT_TZ", datetime, from_tz, to_tz, options)

    def if_sql(self, expression: exp.If) -> str:
        this = self.sql(expression, "this")
        true = self.sql(expression, "true")
        false = self.sql(expression, "false")
        return f"IF {this} THEN {true} ELSE {false} ENDIF"

    # https://docs.exasol.com/db/latest/sql/open_schema.htm
    def use_sql(self, expression: exp.Use) -> str:
        kind = expression.args.get("kind")
        if kind and self.sql(kind).upper() != "SCHEMA":
            self.unsupported(f"'USE {self.sql(kind)}' is not supported in Exasol")
            return super().use_sql(expression)
        this = self.sql(expression, "this")
        return f"OPEN SCHEMA {this}"

    # https://docs.exasol.com/db/latest/sql_references/metadata/metadata_system_tables.htm
    def show_sql(self, expression: exp.Show) -> str:
        if expression.name == "TABLES":
            db_name = expression.text("db")
            schema_filter: exp.Expression = (
                exp.Literal.string(db_name.upper()) if db_name else exp.CurrentSchema()
            )
            select = (
                exp.select(exp.column("TABLE_NAME"))
                .from_(exp.table_("EXA_ALL_TABLES", db="SYS"))
                .where(exp.column("TABLE_SCHEMA").eq(schema_filter))
            )
            return self.sql(select)

        if expression.name == "DATABASES":
            select = exp.select(exp.column("SCHEMA_NAME")).from_(
                exp.table_("EXA_SCHEMAS", db="SYS")
            )
            return self.sql(select)

        return super().show_sql(expression)

    def collate_sql(self, expression: exp.Collate) -> str:
        return self.sql(expression.this)

    def jsonextract_sql(self, expression: exp.JSONExtract) -> str:
        sql = self.func(
            "JSON_EXTRACT", expression.this, expression.expression, *expression.expressions
        )

        emits = self.sql(expression, "emits")
        if emits:
            sql = f"{sql} EMITS {emits}"

        return sql

    @unsupported_args("flag")
    def regexplike_sql(self, expression: exp.RegexpLike) -> str:
        if not expression.args.get("full_match"):
            pattern = expression.expression
            if pattern.is_string:
                expression.set("expression", exp.Literal.string(f".*{pattern.name}.*"))
            else:
                expression.set(
                    "expression",
                    exp.Paren(
                        this=exp.Concat(
                            expressions=[
                                exp.Literal.string(".*"),
                                pattern,
                                exp.Literal.string(".*"),
                            ]
                        )
                    ),
                )
        return self.binary(expression, "REGEXP_LIKE")
