from __future__ import annotations

import typing as t

from sqlglot import exp, generator, transforms
from sqlglot.dialects.dialect import (
    bool_xor_sql,
    bracket_to_element_at_sql,
    datestrtodate_sql,
    encode_decode_sql,
    if_sql,
    left_to_substring_sql,
    no_ilike_sql,
    no_pivot_sql,
    no_timestamp_sql,
    regexp_extract_sql,
    rename_func,
    right_to_substring_sql,
    strposition_sql,
    struct_extract_sql,
    timestamptrunc_sql,
    timestrtotime_sql,
    ts_or_ds_add_cast,
    unit_to_str,
    week_unit_to_dow,
    sequence_sql,
    explode_to_unnest_sql,
)
from sqlglot.dialects.hive import Hive
from sqlglot.generator import unsupported_args
from sqlglot.optimizer.scope import find_all_in_scope
from sqlglot.transforms import unqualify_columns

DATE_ADD_OR_SUB = t.Union[exp.DateAdd, exp.TimestampAdd, exp.DateSub]


def _sha2_digest_sql(self: PrestoGenerator, expression: exp.SHA2Digest) -> str:
    length = expression.text("length") or "256"
    if length not in ("256", "512"):
        self.unsupported(f"SHA{length} is not supported in Presto")

    this = expression.this
    if this.is_type(*exp.DataType.TEXT_TYPES):
        # the native digest takes VARBINARY, so a text-typed argument needs encoding
        this = exp.Encode(this=this, charset=exp.Literal.string("utf-8"))

    return self.func(f"SHA{length}", this)


def _initcap_sql(self: PrestoGenerator, expression: exp.Initcap) -> str:
    delimiters = expression.expression
    if delimiters and not (
        delimiters.is_string and delimiters.this == self.dialect.INITCAP_DEFAULT_DELIMITER_CHARS
    ):
        self.unsupported("INITCAP does not support custom delimiters")

    regex = r"(\w)(\w*)"
    return f"REGEXP_REPLACE({self.sql(expression, 'this')}, '{regex}', x -> UPPER(x[1]) || LOWER(x[2]))"


def _no_sort_array(self: PrestoGenerator, expression: exp.SortArray) -> str:
    if expression.args.get("asc") == exp.false():
        comparator = "(a, b) -> CASE WHEN a < b THEN 1 WHEN a > b THEN -1 ELSE 0 END"
    else:
        comparator = None
    return self.func("ARRAY_SORT", expression.this, comparator)


def _schema_sql(self: PrestoGenerator, expression: exp.Schema) -> str:
    if isinstance(expression.parent, exp.PartitionedByProperty):
        # Any columns in the ARRAY[] string literals should not be quoted
        expression.transform(lambda n: n.name if isinstance(n, exp.Identifier) else n, copy=False)

        partition_exprs = [
            self.sql(c) if isinstance(c, (exp.Func, exp.Property)) else self.sql(c, "this")
            for c in expression.expressions
        ]
        return self.sql(exp.Array(expressions=[exp.Literal.string(c) for c in partition_exprs]))

    if expression.parent:
        for schema in expression.parent.find_all(exp.Schema):
            if schema is expression:
                continue

            column_defs = schema.find_all(exp.ColumnDef)
            if column_defs and isinstance(schema.parent, exp.Property):
                expression.expressions.extend(column_defs)

    return self.schema_sql(expression)


def _quantile_sql(self: PrestoGenerator, expression: exp.Quantile) -> str:
    self.unsupported("Presto does not support exact quantiles")
    return self.func("APPROX_PERCENTILE", expression.this, expression.args.get("quantile"))


def _str_to_time_sql(
    self: PrestoGenerator, expression: exp.StrToDate | exp.StrToTime | exp.TsOrDsToDate
) -> str:
    return self.func("DATE_PARSE", expression.this, self.format_time(expression))


def _ts_or_ds_to_date_sql(self: PrestoGenerator, expression: exp.TsOrDsToDate) -> str:
    time_format = self.format_time(expression)
    dialect_class = type(self.dialect)
    if time_format and time_format not in (dialect_class.TIME_FORMAT, dialect_class.DATE_FORMAT):
        return self.sql(exp.cast(_str_to_time_sql(self, expression), exp.DType.DATE))
    return self.sql(exp.cast(exp.cast(expression.this, exp.DType.TIMESTAMP), exp.DType.DATE))


def _ts_or_ds_add_sql(self: PrestoGenerator, expression: exp.TsOrDsAdd) -> str:
    expression = ts_or_ds_add_cast(expression)
    unit = unit_to_str(expression)
    return self.func("DATE_ADD", unit, expression.expression, expression.this)


def _ts_or_ds_diff_sql(self: PrestoGenerator, expression: exp.TsOrDsDiff) -> str:
    this = exp.cast(expression.this, exp.DType.TIMESTAMP)
    expr = exp.cast(expression.expression, exp.DType.TIMESTAMP)
    unit = unit_to_str(expression)
    return self.func("DATE_DIFF", unit, expr, this)


def _first_last_sql(self: PrestoGenerator, expression: exp.Func) -> str:
    """
    Trino doesn't support FIRST / LAST as functions, but they're valid in the context
    of MATCH_RECOGNIZE, so we need to preserve them in that case. In all other cases
    they're converted into an ARBITRARY call.

    Reference: https://trino.io/docs/current/sql/match-recognize.html#logical-navigation-functions
    """
    if isinstance(expression.find_ancestor(exp.MatchRecognize, exp.Select), exp.MatchRecognize):
        return self.function_fallback_sql(expression)

    return rename_func("ARBITRARY")(self, expression)


def _unix_to_time_sql(self: PrestoGenerator, expression: exp.UnixToTime) -> str:
    scale = expression.args.get("scale")
    timestamp = self.sql(expression, "this")
    if scale in (None, exp.UnixToTime.SECONDS):
        return rename_func("FROM_UNIXTIME")(self, expression)

    return f"FROM_UNIXTIME(CAST({timestamp} AS DOUBLE) / POW(10, {scale}))"


def _to_int(self: PrestoGenerator, expression: exp.Expr) -> exp.Expr:
    if not expression.type:
        from sqlglot.optimizer.annotate_types import annotate_types

        annotate_types(expression, dialect=self.dialect)
    if expression.type and expression.type.this not in exp.DataType.INTEGER_TYPES:
        return exp.cast(expression, to=exp.DType.BIGINT)
    return expression


def _date_delta_sql(
    name: str, negate_interval: bool = False
) -> t.Callable[[PrestoGenerator, DATE_ADD_OR_SUB], str]:
    def _delta_sql(self: PrestoGenerator, expression: DATE_ADD_OR_SUB) -> str:
        interval = _to_int(self, expression.expression)
        return self.func(
            name,
            unit_to_str(expression),
            interval * (-1) if negate_interval else interval,
            expression.this,
        )

    return _delta_sql


def _date_diff_sql(
    self: PrestoGenerator, expression: exp.DateDiff | exp.DatetimeDiff | exp.TimestampDiff
) -> str:
    # Presto/Trino only expose date_diff(unit, ts1, ts2); it returns ts2 - ts1, so the
    # operands are emitted as (expression, this) to preserve `this - expression` semantics.
    this: exp.Expr = expression.this
    expr: exp.Expr = expression.expression
    unit = unit_to_str(expression)

    # DATE_DIFF counts complete units between its operands, whereas dialects that set
    # date_part_boundary count unit boundary crossings, so the operands are truncated
    # down to the unit to make the two coincide
    if unit and expression.args.get("date_part_boundary"):
        raw_unit = expression.args.get("unit")
        dow = week_unit_to_dow(raw_unit)

        if dow is not None:
            unit = exp.Literal.string("WEEK")

            # DATE_TRUNC('WEEK', ...) is Monday-based; shifting both operands by the same
            # delta realigns it to the requested week start without changing the diff
            shift_days = 1 if dow == 7 else 1 - dow
            if shift_days:
                delta = exp.Interval(this=exp.Literal.string(str(shift_days)), unit=exp.var("DAY"))
                this = exp.Add(this=this, expression=delta)
                expr = exp.Add(this=expr, expression=delta.copy())

        if not isinstance(raw_unit, exp.WeekStart) or dow is not None:
            this = exp.DateTrunc(unit=unit.copy(), this=this)
            expr = exp.DateTrunc(unit=unit.copy(), this=expr)

    return self.func("DATE_DIFF", unit, expr, this)


def _explode_to_unnest_sql(self: PrestoGenerator, expression: exp.Lateral) -> str:
    explode = expression.this
    if isinstance(explode, exp.Explode):
        exploded_type = explode.this.type
        alias = expression.args.get("alias")

        # This attempts a best-effort transpilation of LATERAL VIEW EXPLODE on a struct array
        if (
            isinstance(alias, exp.TableAlias)
            and isinstance(exploded_type, exp.DataType)
            and exploded_type.is_type(exp.DType.ARRAY)
            and exploded_type.expressions
            and exploded_type.expressions[0].is_type(exp.DType.STRUCT)
        ):
            # When unnesting a ROW in Presto, it produces N columns, so we need to fix the alias
            alias.set("columns", [c.this.copy() for c in exploded_type.expressions[0].expressions])
    elif isinstance(explode, exp.Inline):
        explode.replace(exp.Explode(this=explode.this.copy()))

    return explode_to_unnest_sql(self, expression)


def amend_exploded_column_table(expression: exp.Expr) -> exp.Expr:
    # We check for expression.type because the columns can be amended only if types were inferred
    if isinstance(expression, exp.Select) and expression.type:
        for lateral in expression.args.get("laterals") or []:
            alias = lateral.args.get("alias")
            if (
                not isinstance(lateral.this, exp.Explode)
                or not isinstance(alias, exp.TableAlias)
                or len(alias.columns) != 1
            ):
                continue

            new_table = alias.this
            old_table = alias.columns[0].name.lower()

            # When transpiling a LATERAL VIEW EXPLODE Spark query, the exploded fields may be qualified
            # with the struct column, resulting in invalid Presto references that need to be amended
            for column in find_all_in_scope(expression, exp.Column):
                if column.db.lower() == old_table:
                    column.set("table", column.args["db"].pop())
                elif column.table.lower() == old_table:
                    column.set("table", new_table.copy())
                elif column.name.lower() == old_table and isinstance(column.parent, exp.Dot):
                    column.parent.replace(exp.column(column.parent.expression, table=new_table))

    return expression


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

    AFTER_HAVING_MODIFIER_TRANSFORMS = generator.AFTER_HAVING_MODIFIER_TRANSFORMS

    INTERVAL_ALLOWS_PLURAL_FORM = False
    JOIN_HINTS = False
    TABLE_HINTS = False
    QUERY_HINTS = False
    IS_BOOL_ALLOWED = False
    TZ_TO_WITH_TIME_ZONE = True
    NVL2_SUPPORTED = False
    STRUCT_DELIMITER = ("(", ")")
    LIMIT_ONLY_LITERALS = True
    SUPPORTS_SINGLE_ARG_CONCAT = False
    LIKE_PROPERTY_INSIDE_SCHEMA = True
    MULTI_ARG_DISTINCT = False
    SUPPORTS_TO_NUMBER = False
    HEX_FUNC = "TO_HEX"
    PARSE_JSON_NAME: str | None = "JSON_PARSE"
    PAD_FILL_PATTERN_IS_REQUIRED = True
    EXCEPT_INTERSECT_SUPPORT_ALL_CLAUSE = False
    SUPPORTS_MEDIAN = False
    ARRAY_SIZE_NAME = "CARDINALITY"

    PROPERTIES_LOCATION = {
        **generator.Generator.PROPERTIES_LOCATION,
        exp.LocationProperty: exp.Properties.Location.UNSUPPORTED,
        exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED,
    }

    TYPE_MAPPING = {
        **generator.Generator.TYPE_MAPPING,
        exp.DType.BINARY: "VARBINARY",
        exp.DType.BIT: "BOOLEAN",
        exp.DType.DATETIME: "TIMESTAMP",
        exp.DType.DATETIME64: "TIMESTAMP",
        exp.DType.FLOAT: "REAL",
        exp.DType.HLLSKETCH: "HYPERLOGLOG",
        exp.DType.INT: "INTEGER",
        exp.DType.STRUCT: "ROW",
        exp.DType.TEXT: "VARCHAR",
        exp.DType.TIMESTAMPTZ: "TIMESTAMP",
        exp.DType.TIMESTAMPNTZ: "TIMESTAMP",
        exp.DType.TIMETZ: "TIME",
    }

    TRANSFORMS = {
        **generator.Generator.TRANSFORMS,
        exp.AnyValue: rename_func("ARBITRARY"),
        exp.ApproxQuantile: lambda self, e: self.func(
            "APPROX_PERCENTILE",
            e.this,
            e.args.get("weight"),
            e.args.get("quantile"),
            e.args.get("accuracy"),
        ),
        exp.ArgMax: rename_func("MAX_BY"),
        exp.ArgMin: rename_func("MIN_BY"),
        exp.Array: transforms.preprocess(
            [transforms.inherit_struct_field_names],
            generator=lambda self, e: f"ARRAY[{self.expressions(e, flat=True)}]",
        ),
        exp.ArrayAny: rename_func("ANY_MATCH"),
        exp.ArrayConcat: rename_func("CONCAT"),
        exp.ArrayContains: rename_func("CONTAINS"),
        exp.ArrayToString: rename_func("ARRAY_JOIN"),
        exp.ArrayUniqueAgg: rename_func("SET_AGG"),
        exp.ArraySlice: rename_func("SLICE"),
        exp.AtTimeZone: rename_func("AT_TIMEZONE"),
        exp.BitwiseAnd: lambda self, e: self.func("BITWISE_AND", e.this, e.expression),
        exp.BitwiseLeftShift: rename_func("BITWISE_LEFT_SHIFT"),
        exp.BitwiseNot: lambda self, e: self.func("BITWISE_NOT", e.this),
        exp.BitwiseOr: lambda self, e: self.func("BITWISE_OR", e.this, e.expression),
        exp.BitwiseRightShift: rename_func("BITWISE_RIGHT_SHIFT"),
        exp.BitwiseXor: lambda self, e: self.func("BITWISE_XOR", e.this, e.expression),
        exp.Cast: transforms.preprocess([transforms.epoch_cast_to_ts]),
        exp.CurrentTime: lambda *_: "CURRENT_TIME",
        exp.CurrentTimestamp: lambda *_: "CURRENT_TIMESTAMP",
        exp.CurrentUser: lambda *_: "CURRENT_USER",
        exp.DateAdd: _date_delta_sql("DATE_ADD"),
        exp.DateDiff: _date_diff_sql,
        exp.DatetimeDiff: _date_diff_sql,
        exp.TimestampDiff: _date_diff_sql,
        exp.DateStrToDate: datestrtodate_sql,
        exp.DateToDi: lambda self, e: (
            f"CAST(DATE_FORMAT({self.sql(e, 'this')}, {type(self.dialect).DATEINT_FORMAT}) AS INT)"
        ),
        exp.DateSub: _date_delta_sql("DATE_ADD", negate_interval=True),
        exp.DayOfWeek: lambda self, e: f"(({self.func('DAY_OF_WEEK', e.this)} % 7) + 1)",
        exp.DayOfWeekIso: rename_func("DAY_OF_WEEK"),
        exp.Decode: lambda self, e: encode_decode_sql(self, e, "FROM_UTF8"),
        exp.DiToDate: lambda self, e: (
            f"CAST(DATE_PARSE(CAST({self.sql(e, 'this')} AS VARCHAR), {type(self.dialect).DATEINT_FORMAT}) AS DATE)"
        ),
        exp.Encode: lambda self, e: encode_decode_sql(self, e, "TO_UTF8"),
        exp.FileFormatProperty: lambda self, e: f"format={self.sql(exp.Literal.string(e.name))}",
        exp.First: _first_last_sql,
        exp.FromISO8601Date: rename_func("FROM_ISO8601_DATE"),
        exp.FromISO8601Timestamp: rename_func("FROM_ISO8601_TIMESTAMP"),
        exp.FromTimeZone: lambda self, e: (
            f"WITH_TIMEZONE({self.sql(e, 'this')}, {self.sql(e, 'zone')}) AT TIME ZONE 'UTC'"
        ),
        exp.GenerateSeries: sequence_sql,
        exp.GenerateDateArray: sequence_sql,
        exp.If: if_sql(),
        exp.ILike: no_ilike_sql,
        exp.Initcap: _initcap_sql,
        exp.Last: _first_last_sql,
        exp.LastDay: lambda self, e: self.func("LAST_DAY_OF_MONTH", e.this),
        exp.Lateral: _explode_to_unnest_sql,
        exp.Left: left_to_substring_sql,
        exp.Levenshtein: unsupported_args("ins_cost", "del_cost", "sub_cost", "max_dist")(
            rename_func("LEVENSHTEIN_DISTANCE")
        ),
        exp.LogicalAnd: rename_func("BOOL_AND"),
        exp.LogicalOr: rename_func("BOOL_OR"),
        exp.Pivot: no_pivot_sql,
        exp.Quantile: _quantile_sql,
        exp.RegexpExtract: regexp_extract_sql,
        exp.RegexpExtractAll: regexp_extract_sql,
        exp.Right: right_to_substring_sql,
        exp.Schema: _schema_sql,
        exp.SchemaCommentProperty: lambda self, e: self.naked_property(e),
        exp.Select: transforms.preprocess(
            [
                transforms.eliminate_window_clause,
                transforms.eliminate_qualify,
                transforms.eliminate_distinct_on,
                transforms.explode_projection_to_unnest(1, unnest_map=True),
                transforms.eliminate_semi_and_anti_joins,
                amend_exploded_column_table,
            ]
        ),
        exp.SortArray: _no_sort_array,
        exp.SqlSecurityProperty: lambda self, e: f"SECURITY {self.sql(e.this)}",
        exp.StrPosition: lambda self, e: strposition_sql(self, e, supports_occurrence=True),
        exp.StrToDate: lambda self, e: f"CAST({_str_to_time_sql(self, e)} AS DATE)",
        exp.StrToMap: rename_func("SPLIT_TO_MAP"),
        exp.StrToTime: _str_to_time_sql,
        exp.StructExtract: struct_extract_sql,
        exp.Table: transforms.preprocess([transforms.unnest_generate_series]),
        exp.Timestamp: no_timestamp_sql,
        exp.TimestampAdd: _date_delta_sql("DATE_ADD"),
        exp.TimestampTrunc: timestamptrunc_sql(),
        exp.TimeStrToDate: timestrtotime_sql,
        exp.TimeStrToTime: timestrtotime_sql,
        exp.TimeStrToUnix: lambda self, e: self.func(
            "TO_UNIXTIME", self.func("DATE_PARSE", e.this, type(self.dialect).TIME_FORMAT)
        ),
        exp.TimeToStr: lambda self, e: self.func("DATE_FORMAT", e.this, self.format_time(e)),
        exp.TimeToUnix: rename_func("TO_UNIXTIME"),
        exp.ToChar: lambda self, e: self.func("DATE_FORMAT", e.this, self.format_time(e)),
        exp.TryCast: transforms.preprocess([transforms.epoch_cast_to_ts]),
        exp.TsOrDiToDi: lambda self, e: (
            f"CAST(SUBSTR(REPLACE(CAST({self.sql(e, 'this')} AS VARCHAR), '-', ''), 1, 8) AS INT)"
        ),
        exp.TsOrDsAdd: _ts_or_ds_add_sql,
        exp.TsOrDsDiff: _ts_or_ds_diff_sql,
        exp.TsOrDsToDate: _ts_or_ds_to_date_sql,
        exp.Unhex: rename_func("FROM_HEX"),
        exp.UnixToStr: lambda self, e: (
            f"DATE_FORMAT(FROM_UNIXTIME({self.sql(e, 'this')}), {self.format_time(e)})"
        ),
        exp.UnixToTime: _unix_to_time_sql,
        exp.UnixToTimeStr: lambda self, e: f"CAST(FROM_UNIXTIME({self.sql(e, 'this')}) AS VARCHAR)",
        exp.VariancePop: rename_func("VAR_POP"),
        exp.With: transforms.preprocess([transforms.add_recursive_cte_column_names]),
        exp.WithinGroup: transforms.preprocess([transforms.remove_within_group_for_percentiles]),
        # Note: Presto's TRUNCATE always returns DOUBLE, even with decimals=0, whereas
        # most dialects return INT (SQLite also returns REAL, see sqlite.py). This creates
        # a bidirectional transpilation gap: Presto→Other may change float division to int
        # division, and vice versa. Modeling precisely would require exp.FloatTrunc or
        # similar, deemed overengineering for this subtle semantic difference.
        exp.Trunc: rename_func("TRUNCATE"),
        exp.Xor: bool_xor_sql,
        exp.MD5Digest: rename_func("MD5"),
        exp.SHA: rename_func("SHA1"),
        exp.SHA1Digest: rename_func("SHA1"),
        exp.SHA2Digest: _sha2_digest_sql,
        exp.Substring: rename_func("SUBSTR"),
    }

    RESERVED_KEYWORDS = {
        "alter",
        "and",
        "as",
        "between",
        "by",
        "case",
        "cast",
        "constraint",
        "create",
        "cross",
        "current_time",
        "current_timestamp",
        "deallocate",
        "delete",
        "describe",
        "distinct",
        "drop",
        "else",
        "end",
        "escape",
        "except",
        "execute",
        "exists",
        "extract",
        "false",
        "for",
        "from",
        "full",
        "group",
        "having",
        "in",
        "inner",
        "insert",
        "intersect",
        "into",
        "is",
        "join",
        "left",
        "like",
        "natural",
        "not",
        "null",
        "on",
        "or",
        "order",
        "outer",
        "prepare",
        "right",
        "select",
        "table",
        "then",
        "true",
        "union",
        "using",
        "values",
        "when",
        "where",
        "with",
    }

    def extract_sql(self, expression: exp.Extract) -> str:
        date_part = expression.name

        if not date_part.startswith("EPOCH"):
            return super().extract_sql(expression)

        if date_part == "EPOCH_MILLISECOND":
            scale = 10**3
        elif date_part == "EPOCH_MICROSECOND":
            scale = 10**6
        elif date_part == "EPOCH_NANOSECOND":
            scale = 10**9
        else:
            scale = None

        value = expression.expression

        ts = exp.cast(value, to=exp.DType.TIMESTAMP.into_expr())
        to_unix: exp.Expr = exp.TimeToUnix(this=ts)

        if scale:
            to_unix = exp.Mul(this=to_unix, expression=exp.Literal.number(scale))

        return self.sql(to_unix)

    def jsonformat_sql(self, expression: exp.JSONFormat) -> str:
        this = expression.this
        is_json = expression.args.get("is_json")

        if this and not (is_json or this.type):
            from sqlglot.optimizer.annotate_types import annotate_types

            this = annotate_types(this, dialect=self.dialect)

        if not (is_json or this.is_type(exp.DType.JSON)):
            this.replace(exp.cast(this, exp.DType.JSON))

        return self.function_fallback_sql(expression)

    def md5_sql(self, expression: exp.MD5) -> str:
        this = expression.this

        if not this.type:
            from sqlglot.optimizer.annotate_types import annotate_types

            this = annotate_types(this, dialect=self.dialect)

        if this.is_type(*exp.DataType.TEXT_TYPES):
            this = exp.Encode(this=this, charset=exp.Literal.string("utf-8"))

        return self.func("LOWER", self.func("TO_HEX", self.func("MD5", self.sql(this))))

    def sha2_sql(self, expression: exp.SHA2) -> str:
        length = expression.text("length") or "256"
        if length not in ("256", "512"):
            self.unsupported(f"SHA{length} is not supported in Presto")

        this = expression.this

        if this.is_type(*exp.DataType.TEXT_TYPES):
            this = exp.Encode(this=this, charset=exp.Literal.string("utf-8"))

        return self.func("LOWER", self.func("TO_HEX", self.func(f"SHA{length}", self.sql(this))))

    def strtounix_sql(self, expression: exp.StrToUnix) -> str:
        # Since `TO_UNIXTIME` requires a `TIMESTAMP`, we need to parse the argument into one.
        # To do this, we first try to `DATE_PARSE` it, but since this can fail when there's a
        # timezone involved, we wrap it in a `TRY` call and use `PARSE_DATETIME` as a fallback,
        # which seems to be using the same time mapping as Hive, as per:
        # https://joda-time.sourceforge.net/apidocs/org/joda/time/format/DateTimeFormat.html
        this = expression.this
        value_as_text = exp.cast(this, exp.DType.TEXT)
        value_as_timestamp = exp.cast(this, exp.DType.TIMESTAMP) if this.is_string else this

        parse_without_tz = self.func("DATE_PARSE", value_as_text, self.format_time(expression))

        formatted_value = self.func("DATE_FORMAT", value_as_timestamp, self.format_time(expression))
        parse_with_tz = self.func(
            "PARSE_DATETIME",
            formatted_value,
            self.format_time(expression, Hive.INVERSE_TIME_MAPPING, Hive.INVERSE_TIME_TRIE),
        )
        coalesced = self.func("COALESCE", self.func("TRY", parse_without_tz), parse_with_tz)
        return self.func("TO_UNIXTIME", coalesced)

    def bracket_sql(self, expression: exp.Bracket) -> str:
        if expression.args.get("safe"):
            return bracket_to_element_at_sql(self, expression)
        return super().bracket_sql(expression)

    def struct_sql(self, expression: exp.Struct) -> str:
        if not expression.type:
            from sqlglot.optimizer.annotate_types import annotate_types

            annotate_types(expression, dialect=self.dialect)

        values: list[str] = []
        schema: list[str] = []
        unknown_type = False

        for e in expression.expressions:
            if isinstance(e, exp.PropertyEQ):
                if e.type and e.type.is_type(exp.DType.UNKNOWN):
                    unknown_type = True
                else:
                    schema.append(f"{self.sql(e, 'this')} {self.sql(e.type)}")
                values.append(self.sql(e, "expression"))
            else:
                values.append(self.sql(e))

        size = len(expression.expressions)

        if not size or len(schema) != size:
            if unknown_type:
                self.unsupported(
                    "Cannot convert untyped key-value definitions (try annotate_types)."
                )
            return self.func("ROW", *values)
        return f"CAST(ROW({', '.join(values)}) AS ROW({', '.join(schema)}))"

    def interval_sql(self, expression: exp.Interval) -> str:
        if expression.this and expression.text("unit").upper().startswith("WEEK"):
            return f"({expression.this.name} * INTERVAL '7' DAY)"
        return super().interval_sql(expression)

    def transaction_sql(self, expression: exp.Transaction) -> str:
        modes = expression.args.get("modes")
        modes = f" {', '.join(modes)}" if modes else ""
        return f"START TRANSACTION{modes}"

    def offset_limit_modifiers(
        self, expression: exp.Expr, fetch: bool, limit: exp.Fetch | exp.Limit | None
    ) -> list[str]:
        return [
            self.sql(expression, "offset"),
            self.sql(limit),
        ]

    def create_sql(self, expression: exp.Create) -> str:
        """
        Presto doesn't support CREATE VIEW with expressions (ex: `CREATE VIEW x (cola)` then `(cola)` is the expression),
        so we need to remove them
        """
        kind = expression.args["kind"]
        schema = expression.this
        if kind == "VIEW" and schema.expressions:
            expression.this.set("expressions", None)
        return super().create_sql(expression)

    def delete_sql(self, expression: exp.Delete) -> str:
        """
        Presto only supports DELETE FROM for a single table without an alias, so we need
        to remove the unnecessary parts. If the original DELETE statement contains more
        than one table to be deleted, we can't safely map it 1-1 to a Presto statement.
        """
        tables = expression.args.get("tables") or [expression.this]
        if len(tables) > 1:
            return super().delete_sql(expression)

        table = tables[0]
        expression.set("this", table)
        expression.set("tables", None)

        if isinstance(table, exp.Table):
            table_alias = table.args.get("alias")
            if table_alias:
                table_alias.pop()
                expression = t.cast(exp.Delete, expression.transform(unqualify_columns))

        return super().delete_sql(expression)

    def jsonextract_sql(self, expression: exp.JSONExtract) -> str:
        is_json_extract = self.dialect.settings.get("variant_extract_is_json_extract", True)

        # Generate JSON_EXTRACT unless the user has configured that a Snowflake / Databricks
        # VARIANT extract (e.g. col:x.y) should map to dot notation (i.e ROW access) in Presto/Trino
        if not expression.args.get("variant_extract") or is_json_extract:
            return self.func(
                "JSON_EXTRACT", expression.this, expression.expression, *expression.expressions
            )

        this = self.sql(expression, "this")

        # Convert the JSONPath extraction `JSON_EXTRACT(col, '$.x.y) to a ROW access col.x.y
        segments = []
        for path_key in expression.expression.expressions[1:]:
            if not isinstance(path_key, exp.JSONPathKey):
                # Cannot transpile subscripts, wildcards etc to dot notation
                self.unsupported(f"Cannot transpile JSONPath segment '{path_key}' to ROW access")
                continue
            key = path_key.this
            if not exp.SAFE_IDENTIFIER_RE.match(key):
                key = f'"{key}"'
            segments.append(f".{key}")

        expr = "".join(segments)

        return f"{this}{expr}"

    def groupconcat_sql(self, expression: exp.GroupConcat) -> str:
        return self.func(
            "ARRAY_JOIN",
            self.func("ARRAY_AGG", expression.this),
            expression.args.get("separator"),
        )
