from django.db.models.fields import BigAutoField
from django.db.models.sql import compiler

from clickhouse_backend.compat import dj4
from clickhouse_backend.idworker import id_worker


class ClickhouseMixin:
    def as_sql(self, *args, **kwargs):
        sql, params = super().as_sql(*args, **kwargs)
        if self.query.explain_info:
            prefix, suffix = self.connection.ops.explain_query(
                format=self.query.explain_info.format,
                type=self.query.explain_info.type,
                **self.query.explain_info.options,
            )
            sql = "%s %s" % (prefix, sql.lstrip())
            if suffix:
                sql = "%s %s" % (sql, suffix)
        if hasattr(self.query, "setting_info") and self.query.setting_info:
            setting_sql, setting_params = self.connection.ops.settings_sql(
                **self.query.setting_info
            )
            sql = "%s %s" % (sql, setting_sql)
            params = (*params, *setting_params)
        return sql, params


class SQLCompiler(ClickhouseMixin, compiler.SQLCompiler):
    pass


class SQLInsertCompiler(ClickhouseMixin, compiler.SQLInsertCompiler):
    def as_sql(self):
        # We don"t need quote_name_unless_alias() here, since these are all
        # going to be column names (so we can avoid the extra overhead).
        qn = self.connection.ops.quote_name
        opts = self.query.get_meta()
        if dj4:
            insert_statement = self.connection.ops.insert_statement(
                on_conflict=self.query.on_conflict,
            )
        else:
            insert_statement = self.connection.ops.insert_statement(
                ignore_conflicts=self.query.ignore_conflicts
            )

        fields = self.query.fields
        # For compatible with BigAutoField, add value generated by snowflake algorithm if needed.
        absent_of_pk = isinstance(opts.pk, BigAutoField) and opts.pk not in fields
        if absent_of_pk:
            fields = fields + [opts.pk]
            for obj in self.query.objs:
                setattr(obj, opts.pk.attname, id_worker.get_id())

        result = [
            "%s %s(%s)" % (
                insert_statement,
                qn(opts.db_table),
                ", ".join(qn(f.column) for f in fields),
            )
        ]

        value_rows = [
            [self.prepare_value(field, self.pre_save_val(field, obj))
             for field in fields]
            for obj in self.query.objs
        ]

        placeholder_rows, param_rows = self.assemble_as_sql(fields, value_rows)
        result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows))
        if hasattr(self.query, "get_settings"):
            settings_string = self.query.get_settings()
            if settings_string:
                result.append(settings_string)
        return [(" ".join(result), param_rows)]

    def execute_sql(self, returning_fields=None):
        as_sql = self.as_sql()
        self.returning_fields = returning_fields
        with self.connection.cursor() as cursor:
            for sql, params in as_sql:
                cursor.execute(sql, params)


class SQLDeleteCompiler(ClickhouseMixin, compiler.SQLDeleteCompiler):
    def _as_sql(self, query):
        """
        When execute DELETE and UPDATE query. Clickhouse does not support
        "table"."column" in WHERE clause.
        """
        table = self.quote_name_unless_alias(query.base_table)
        result = [
            "ALTER TABLE %s DELETE" % table
        ]
        where, params = self.compile(query.where)
        where = where.replace(table + ".", "")
        if where:
            result.append("WHERE %s" % where)
        else:
            result.append("WHERE 1")
        return " ".join(result), tuple(params)


class SQLUpdateCompiler(ClickhouseMixin, compiler.SQLUpdateCompiler):
    def as_sql(self):
        """
        When execute DELETE and UPDATE query. Clickhouse does not support
        "table"."column" in WHERE clause.
        """
        self.pre_sql_setup()
        if not self.query.values:
            return "", ()
        qn = self.quote_name_unless_alias
        values, update_params = [], []
        for field, model, val in self.query.values:
            if hasattr(val, "resolve_expression"):
                val = val.resolve_expression(
                    self.query, allow_joins=False, for_save=True
                )
                if val.contains_aggregate:
                    raise compiler.FieldError(
                        "Aggregate functions are not allowed in this query "
                        "(%s=%r)." % (field.name, val)
                    )
                if val.contains_over_clause:
                    raise compiler.FieldError(
                        "Window expressions are not allowed in this query "
                        "(%s=%r)." % (field.name, val)
                    )
            elif hasattr(val, "prepare_database_save"):
                if field.remote_field:
                    val = field.get_db_prep_save(
                        val.prepare_database_save(field),
                        connection=self.connection,
                    )
                else:
                    raise TypeError(
                        "Tried to update field %s with a model instance, %r. "
                        "Use a value compatible with %s."
                        % (field, val, field.__class__.__name__)
                    )
            else:
                val = field.get_db_prep_save(val, connection=self.connection)

            # Getting the placeholder for the field.
            if hasattr(field, "get_placeholder"):
                placeholder = field.get_placeholder(val, self, self.connection)
            else:
                placeholder = "%s"
            name = field.column
            if hasattr(val, "as_sql"):
                sql, params = self.compile(val)
                values.append("%s = %s" % (qn(name), placeholder % sql))
                update_params.extend(params)
            elif val is not None:
                values.append("%s = %s" % (qn(name), placeholder))
                update_params.append(val)
            else:
                values.append("%s = NULL" % qn(name))

        # Replace "table"."field" to "field", clickhouse does not support that.
        table = qn(self.query.base_table)
        result = [
            "ALTER TABLE %s UPDATE" % table,
            ", ".join(values).replace(table + ".", ""),
        ]
        where, params = self.compile(self.query.where)
        where = where.replace(table + ".", "")

        if where:
            result.append("WHERE %s" % where)
        else:
            result.append("WHERE 1")

        params = (*update_params, *params)
        if hasattr(self.query, "setting_info") and self.query.setting_info:
            setting_sql, setting_params = self.connection.ops.settings_sql(
                **self.query.setting_info
            )
            result.append(setting_sql)
            params = (*params, *setting_params)
        return " ".join(result), params


class SQLAggregateCompiler(ClickhouseMixin, compiler.SQLAggregateCompiler):
    pass
