import threading
from urllib.parse import urlparse
import logging
from weakref import WeakValueDictionary
from typing import Optional, Type, Any, List
from ..backends.base import DatabaseBackend
from ..backends.sqlite import SQLiteBackend
from ..backends.postgres import PostgresBackend
from ..utils.exceptions import ImproperlyConfigured
from ..orm.models import ModelMeta
from .pool import ConnectionPool
from ..utils.transaction import Transaction

logger = logging.getLogger(__name__)


class Database:
    def __init__(self, connection_uri: str, pool_size: int = 5):
        self.uri = connection_uri
        self.parsed_uri = urlparse(connection_uri)
        self.backend: DatabaseBackend = self._get_backend()
        self.backend.connect()
        # Identity map: {(model_class, pk): instance}
        # Using WeakValueDictionary to allow garbage collection
        self._identity_map: dict[tuple[Type, Any], Any] = WeakValueDictionary()
        self._local = threading.local()

        # For SQLite in-memory databases, we need to use the same connection
        # because each new connection creates a separate in-memory database
        self._is_sqlite_memory = self.parsed_uri.scheme == "sqlite" and (
            self.parsed_uri.path in ["/:memory:", ":memory:", ""]
        )

        if self._is_sqlite_memory:
            # For in-memory SQLite, use a single shared connection
            # Always use pool_size=1 for in-memory to avoid separate databases
            self.pool = ConnectionPool(lambda: self.backend, max_size=1, min_size=1)
        else:
            self.pool = ConnectionPool(
                self._get_backend, max_size=pool_size, min_size=1
            )

    def _get_backend(self) -> DatabaseBackend:
        scheme = self.parsed_uri.scheme

        match scheme:
            case "sqlite":
                path = self.parsed_uri.path
                if path.startswith("/"):
                    path = path[1:]
                return SQLiteBackend(path)
            case "postgres" | "postgresql":
                return PostgresBackend(self.uri)

            case _:
                raise ImproperlyConfigured(f"Unsupported database scheme:{scheme}")

    def register(self, model_cls):
        table_name = f'"{model_cls._table_name}"'
        fields_definitions = []

        for name, field in model_cls._fields.items():
            column_name = field.get_column_name()

            # Skip ManyToManyField as it doesn't create columns in the main table
            if (
                hasattr(field, "__class__")
                and field.__class__.__name__ == "ManyToManyField"
            ):
                continue

            field_type = self.backend.get_field_type(field)

            # Skip fields that don't have a SQL type (like ManyToManyField)
            if field_type is None:
                continue

            if name == "id":
                pk_constraint = self.backend.get_primary_key_constraint(
                    column_name, field_type
                )
                fields_definitions.append(pk_constraint)
                continue

            constraints = []
            if not field.nullable:
                constraints.append("NOT NULL")
            if field.unique:
                constraints.append("UNIQUE")

            # Add CHECK constraint for choices
            if hasattr(field, "choices") and field.choices is not None:
                check_constraint = self.backend.get_check_constraint(
                    column_name, field.choices
                )
                if check_constraint:
                    constraints.append(check_constraint)

            definition = f'"{column_name}" {field_type}'
            if constraints:
                definition += f" {' '.join(constraints)}"
            fields_definitions.append(definition)

        sql = (
            f"CREATE TABLE IF NOT EXISTS {table_name} ({', '.join(fields_definitions)})"
        )
        self.backend.execute(sql)
        self.backend.commit()
        logger.info(f"Registered model {model_cls.__name__} to table {table_name}")

        model_cls._db = self

        # Create through tables for ManyToManyFields
        for name, field in model_cls._fields.items():
            if (
                hasattr(field, "__class__")
                and field.__class__.__name__ == "ManyToManyField"
            ):
                try:
                    field.create_through_table(self)
                except Exception as e:
                    logger.warning(f"Failed to create through table for {name}: {e}")
                    # Continue with other fields even if one fails

    def create_all(self):
        for model_cls in ModelMeta.models:
            self.register(model_cls)
            print(f"Registered model {model_cls.__name__}")

    def execute(self, query: str, params=None):
        tx_conn = getattr(self._local, "connection", None)
        if tx_conn:
            cursor = tx_conn.execute(query, params)
            return cursor
        with self.pool.connection() as conn:
            cursor = conn.execute(query, params)
            # Only auto-commit if we're not in a transaction context
            # We're in a transaction if _local.connection exists
            conn.commit()
            # The backend execute method already returns a CursorWrapper
            return cursor

    def execute_scalar(self, query: str, params: List[Any] = None) -> Any:
        cursor = self.execute(query, params)
        result = cursor.fetchone()
        if result:
            return result[0]
        return None

    def commit(self):
        self.backend.commit()

    def close(self):
        self.backend.close()

    def transaction(self):
        return Transaction(self)

    def query(self, model_cls):
        from .query import Query

        return Query(model_cls, self)

    def get_from_identity_map(self, model_cls: Type, pk: Any) -> Optional[Any]:
        """
        Retrieve an instance from the identity map.

        Args:
            model_cls: The model class
            pk: Primary key value

        Returns:
            Model instance if found in identity map, None otherwise
        """
        key = (model_cls, pk)
        instance = self._identity_map.get(key)
        if instance:
            logger.debug(f"Retrieved {model_cls.__name__} id={pk} from identity map")
        return instance

    def add_to_identity_map(self, instance: Any) -> None:
        """
        Add an instance to the identity map.

        Args:
            instance: Model instance to add (must have id set)
        """
        if instance.id is not None:
            key = (instance.__class__, instance.id)
            self._identity_map[key] = instance
            logger.debug(
                f"Added {instance.__class__.__name__} id={instance.id} to identity map"
            )

    def remove_from_identity_map(self, instance: Any) -> None:
        """
        Remove an instance from the identity map.

        Args:
            instance: Model instance to remove
        """
        if instance.id is not None:
            key = (instance.__class__, instance.id)
            self._identity_map.pop(key, None)
            logger.debug(
                f"Removed {instance.__class__.__name__} id={instance.id} from identity map"
            )

    def clear_identity_map(self) -> None:
        """Clear all entries from the identity map."""
        self._identity_map.clear()
        logger.info("Identity map cleared")
