from .fields import Field, IntegerField
from .query import QuerySet
import logging

logger = logging.getLogger(__name__)


class ModelMeta(type):
    models = []

    def __new__(cls, name, bases, attrs):
        if "id" not in attrs:
            attrs["id"] = IntegerField(nullable=True)
        fields = {}
        for key, value in list(attrs.items()):
            if isinstance(value, Field):
                fields[key] = value
        attrs["_fields"] = fields
        attrs["_table_name"] = name.lower()
        new_class = super().__new__(cls, name, bases, attrs)
        new_class._db = None

        if bases:
            cls.models.append(new_class)

        return new_class


class Model(metaclass=ModelMeta):
    def __init__(self, **kwargs):
        for name, field in self._fields.items():
            if name in kwargs:
                value = kwargs[name]
            else:
                value = field.default
                if callable(value):
                    value = value()
            setattr(self, name, value)

    @classmethod
    def create(cls, **kwargs) -> "Model":
        """Create a new instance, save it to the database and return it."""
        instance = cls(**kwargs)
        instance.save()
        return instance

    @classmethod
    def bulk_create(cls, instances: list["Model"]) -> list["Model"]:
        """
        Insert multiple records in a single query.

        Args:
            instances: List of model instances to create

        Returns:
            List of created instances with IDs populated

        Example:
            users = [User(name="Alice"), User(name="Bob"), User(name="Charlie")]
            User.bulk_create(users)
        """
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )

        if not instances:
            return []

        # Validate all instances are of the correct type
        for instance in instances:
            if not isinstance(instance, cls):
                raise TypeError(
                    f"All instances must be of type {cls.__name__}, got {type(instance).__name__}"
                )
            if instance.id is not None:
                raise ValueError(
                    "bulk_create() cannot be used with instances that already have an id"
                )

        db_interface = cls._db
        table_name = f'"{cls._table_name}"'

        # Get columns (excluding id and ManyToManyFields)
        raw_columns = [
            cls._fields[col].get_column_name()
            for col in cls._fields.keys()
            if col != "id" and cls._fields[col].get_column_name() is not None
        ]
        columns_str = ", ".join([f'"{col}"' for col in raw_columns])

        # Prepare all values for batch insert
        all_values = []
        for instance in instances:
            values = [
                cls._fields[col].to_sql_value(instance)
                for col in cls._fields.keys()
                if col != "id" and cls._fields[col].get_column_name() is not None
            ]
            all_values.extend(values)

        # Build placeholders for multiple rows
        placeholder = db_interface.backend.placeholder_char
        num_columns = len(raw_columns)
        single_row_placeholders = ", ".join([placeholder] * num_columns)
        all_placeholders = ", ".join([f"({single_row_placeholders})"] * len(instances))

        # Build INSERT statement
        backend_name = db_interface.backend.__class__.__name__
        sql = f"INSERT INTO {table_name} ({columns_str}) VALUES {all_placeholders}"

        # Add RETURNING clause for PostgreSQL
        if backend_name == "PostgresBackend":
            sql = f"{sql} RETURNING id"

        # Execute through db_interface to respect transaction context
        cursor = db_interface.execute(sql, all_values)

        # Let backend extract the inserted IDs
        returned_ids = db_interface.backend.get_bulk_insert_ids(cursor, len(instances))

        for instance, returned_id in zip(instances, returned_ids):
            instance.id = returned_id
            db_interface.add_to_identity_map(instance)
        logger.info(f"Bulk inserted {len(instances)} {cls._table_name} records")
        return instances

    @classmethod
    def bulk_update(
        cls, instances: list["Model"], fields: list[str] | None = None
    ) -> list["Model"]:
        """
        Update multiple records efficiently using CASE/WHEN statements.

        Args:
            instances: List of model instances to update (must have IDs)
            fields: List of field names to update. If None, updates all non-ID fields.

        Returns:
            List of updated instances

        Example:
            users = User.filter(active=True).all()
            for user in users:
                user.status = "verified"
            User.bulk_update(users, fields=["status"])
        """
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )

        if not instances:
            return []

        # Validate all instances
        for instance in instances:
            if not isinstance(instance, cls):
                raise TypeError(
                    f"All instances must be of type {cls.__name__}, got {type(instance).__name__}"
                )
            if instance.id is None:
                raise ValueError(
                    "bulk_update() requires all instances to have an id. Use bulk_create() for new records."
                )

        db_interface = cls._db
        table_name = f'"{cls._table_name}"'

        # Determine which fields to update
        if fields is None:
            # Update all fields except id and ManyToManyFields
            update_fields = [
                name
                for name in cls._fields.keys()
                if name != "id" and cls._fields[name].get_column_name() is not None
            ]
        else:
            # Validate provided fields
            for field_name in fields:
                if field_name not in cls._fields:
                    raise ValueError(
                        f"Field '{field_name}' does not exist in {cls.__name__}"
                    )
                if field_name == "id":
                    raise ValueError("Cannot update 'id' field")
                if cls._fields[field_name].get_column_name() is None:
                    raise ValueError(
                        f"Cannot update field '{field_name}' as it has no database column (e.g., ManyToManyField)"
                    )
            update_fields = fields

        if not update_fields:
            logger.warning("No fields to update in bulk_update()")
            return instances

        # Build CASE/WHEN statements for each field
        placeholder = db_interface.backend.placeholder_char
        case_statements = []
        all_values = []

        for field_name in update_fields:
            column_name = cls._fields[field_name].get_column_name()
            # Build CASE WHEN id = ? THEN ? WHEN id = ? THEN ? ... END
            when_clauses = []
            for instance in instances:
                when_clauses.append(f"WHEN id = {placeholder} THEN {placeholder}")
                all_values.append(instance.id)
                all_values.append(cls._fields[field_name].to_sql_value(instance))

            case_sql = f'"{column_name}" = CASE {" ".join(when_clauses)} END'
            case_statements.append(case_sql)

        # Build WHERE clause with all IDs
        id_placeholders = ", ".join([placeholder] * len(instances))
        id_values = [instance.id for instance in instances]

        # Construct UPDATE statement
        set_clause = ", ".join(case_statements)
        sql = f"UPDATE {table_name} SET {set_clause} WHERE id IN ({id_placeholders})"

        # Combine all values: CASE values + WHERE values
        all_values.extend(id_values)

        # Execute update
        db_interface.execute(sql, all_values)

        logger.info(
            f"Bulk updated {len(instances)} {cls._table_name} records ({len(update_fields)} fields)"
        )
        return instances

    @classmethod
    def get_or_create(cls, defaults: dict = None, **kwargs) -> tuple["Model", bool]:
        """Lookup an object with the given kwargs, creating one if necessary.
            Returns a tuple of (object, created), where created is a boolean
            specifying whether an object was created.
        Args:
            defaults: Dictionary of default values to use if creating a new object.
            **kwargs: Lookup parameters to find the existing record.
        """
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )

        instance = cls.filter(**kwargs).first()
        if instance:
            return instance, False
        params = {**kwargs}
        if defaults:
            params.update(defaults)
        try:
            with cls._db.transaction():
                new_instance = cls(**params)
                new_instance.save()
                return new_instance, True
        except Exception as e:
            instance = cls.filter(**kwargs).first()
            if instance:
                logger.info(f"Race condition handled for {cls} with lookup {kwargs}")
                return instance, False
            raise e

    @classmethod
    def update_or_create(cls, defaults: dict = None, **kwargs) -> tuple["Model", bool]:
        """Look up an object with the given kwargs, updating it with defaults if found,
        or creating a new one if necessary.
        Returns a tuple of (object, created), where created is a boolean
        specifying whether an object was created.
        """
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )

        defaults = defaults or {}
        instance = cls.filter(**kwargs).first()
        if instance:
            for key, value in defaults.items():
                setattr(instance, key, value)
            instance.save()
            return instance, False
        params = {**kwargs, **defaults}
        try:
            with cls._db.transaction():
                new_instance = cls(**params)
                new_instance.save()
                return new_instance, True
        except Exception:
            instance = cls.filter(**kwargs).first()
            if not instance:
                raise

            for key, value in defaults.items():
                setattr(instance, key, value)
            instance.save()
            return instance, False

    def save(self, db_interface=None):
        db_interface = db_interface or self._db
        if db_interface is None:
            raise RuntimeError(
                f"Model {self.__class__.__name__} is not registered to any database."
            )
        table_name = f'"{self._table_name}"'

        if self.id is not None:
            raw_columns = [
                self._fields[col].get_column_name()
                for col in self._fields.keys()
                if col != "id" and self._fields[col].get_column_name() is not None
            ]
            values = [
                self._fields[col].to_sql_value(self)
                for col in self._fields.keys()
                if col != "id" and self._fields[col].get_column_name() is not None
            ]
            set_clause = ", ".join(
                [
                    f'"{col}" = {db_interface.backend.placeholder_char}'
                    for col in raw_columns
                ]
            )
            values.append(self.id)

            sql = f"UPDATE {table_name} SET {set_clause} WHERE id = {db_interface.backend.placeholder_char}"
            db_interface.execute(sql, values)
            logger.info(
                f"Updated {self._table_name} with id={self.id}, values: {values[:-1]}"
            )
        else:
            raw_columns = [
                self._fields[col].get_column_name()
                for col in self._fields.keys()
                if col != "id" and self._fields[col].get_column_name() is not None
            ]
            values = [
                self._fields[col].to_sql_value(self)
                for col in self._fields.keys()
                if col != "id" and self._fields[col].get_column_name() is not None
            ]
            placeholders = [db_interface.backend.placeholder_char for _ in values]

            sql = f"INSERT INTO {table_name} ({', '.join(raw_columns)}) VALUES ({', '.join(placeholders)})"

            cursor = db_interface.execute(sql, values)
            if hasattr(cursor, "lastrowid") and cursor.lastrowid:
                self.id = cursor.lastrowid
                # Add to identity map after getting ID
                db_interface.add_to_identity_map(self)

            logger.info(f"Inserted {self._table_name} with values: {values}")

    @classmethod
    def get(cls, pk: int) -> "Model":
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )

        # Check identity map first
        instance = cls._db.get_from_identity_map(cls, pk)
        if instance is not None:
            return instance

        # Load from database
        table_name = f'"{cls._table_name}"'
        columns = ", ".join(
            [f'"{field.get_column_name()}"' for field in cls._fields.values()]
        )

        sql = f"SELECT {columns} FROM {table_name} WHERE id = {cls._db.backend.placeholder_char}"
        cursor = cls._db.backend.execute(sql, [pk])
        row = cursor.fetchone()

        if row:
            data = {}
            for idx, (field_name, field) in enumerate(cls._fields.items()):
                data[field_name] = row[idx]
            instance = cls(**data)
            # Add to identity map
            cls._db.add_to_identity_map(instance)
            return instance
        return None

    @classmethod
    def objects(cls) -> QuerySet:
        if cls._db is None:
            raise RuntimeError(
                f"Model {cls.__name__} is not registered to any database."
            )
        return QuerySet(cls, cls._db)

    @classmethod
    def all(cls) -> QuerySet:
        return cls.objects()

    @classmethod
    def filter(cls, *args, **kwargs) -> QuerySet:
        return cls.all().filter(*args, **kwargs)

    @classmethod
    def exclude(cls, *args, **kwargs) -> QuerySet:
        return cls.all().exclude(*args, **kwargs)

    def delete(self):
        """
        Delete this model instance and handle cascade operations.

        Returns:
            int: Number of deleted records (1 if successful, 0 if record not found)
        """
        if self.id is None:
            logger.warning(
                f"Attempting to delete {self.__class__.__name__} instance without an ID"
            )
            return 0

        if self._db is None:
            raise RuntimeError(
                f"Model {self.__class__.__name__} is not registered to any database."
            )

        # Handle cascade operations before deletion
        self._handle_cascade_deletion()

        # Delete the instance
        table_name = f'"{self._table_name}"'
        sql = f"DELETE FROM {table_name} WHERE id = {self._db.backend.placeholder_char}"
        cursor = self._db.execute(sql, [self.id])

        # Remove from identity map
        self._db.remove_from_identity_map(self)

        deleted_count = cursor.rowcount if hasattr(cursor, "rowcount") else 1
        if deleted_count > 0:
            logger.info(f"Deleted {self._table_name} with id={self.id}")

        return deleted_count

    def _handle_cascade_deletion(self):
        """
        Handle cascade operations for foreign keys pointing to this instance.
        """
        from .fields import ForeignKey, CASCADE, SET_NULL, SET_DEFAULT, PROTECT

        # Find all models that have foreign keys pointing to this model
        for model_class in ModelMeta.models:
            for field_name, field in model_class._fields.items():
                if isinstance(field, ForeignKey) and field.to == self.__class__:
                    # Find all instances that reference this object
                    referencing_instances = model_class.filter(
                        **{field_name: self.id}
                    ).all()

                    if referencing_instances and field.on_delete is PROTECT:
                        raise ValueError(
                            f"Cannot delete {self.__class__.__name__} instance (id={self.id}) "
                            f"because it is referenced by {model_class.__name__} instances "
                            f"with PROTECT constraint on field '{field_name}'"
                        )

                    for ref_instance in referencing_instances:
                        if field.on_delete is CASCADE:
                            # Recursively delete the referencing instance
                            ref_instance.delete()

                        elif field.on_delete is SET_NULL:
                            # Set the foreign key field to NULL
                            setattr(ref_instance, field_name, None)
                            ref_instance.save()

                        elif field.on_delete is SET_DEFAULT:
                            # Set the foreign key field to its default value
                            default_value = field.default
                            if callable(default_value):
                                default_value = default_value()
                            setattr(ref_instance, field_name, default_value)
                            ref_instance.save()
