"""Refactored DataBase class with improved architecture and security."""
import reprlib
from collections.abc import Iterable

import pandas as pd
from sqlalchemy import create_engine

from sangreal_db.orm import SangrealSession
from .dialects import DialectFactory
from .exceptions import ValidationError
from .table_registry import TableRegistry


class DataBase:
    """
    Enhanced DataBase class for easy-to-use ORM operations with improved security and architecture.

    This class provides a simplified interface for database operations with:
    - Lazy table reflection for improved performance
    - Case-insensitive table and column access
    - Multi-database support with dialect-specific optimizations
    - Comprehensive error handling and validation
    - Security fixes preventing malicious attribute injection

    Args:
        bind: SQLAlchemy engine or connection string
        schema: Optional database schema name (default: None)

    Raises:
        DatabaseConnectionError: If database connection fails
        ValidationError: If parameters are invalid

    Example:
        # Using connection string
        db = DataBase('sqlite:///example.db')

        # Using existing engine
        engine = create_engine('postgresql://user:pass@localhost/db')
        db = DataBase(engine)

        # Access tables with case-insensitive names
        users = db.users  # or db.Users, or db.USERS
    """

    def __init__(self, bind, schema=None):
        if bind is None:
            # Allow initialization without binding for delayed setup
            self._bind = None
            self._schema = None
            self._session = None
            self._table_registry = None
            self._insert_strategy = None
            return

        # Normalize schema parameter
        if schema == 'None':
            schema = None

        # Create engine if string provided
        if isinstance(bind, str):
            try:
                bind = create_engine(bind)
            except Exception as e:
                raise ValidationError(f"Failed to create database engine from '{bind}': {str(e)}") from e

        self._bind = bind
        self._schema = schema
        self._session = SangrealSession(self._bind)

        # Initialize table registry
        self._table_registry = TableRegistry(self._bind, schema)

        # Initialize database-specific insert strategy
        dialect_name = self._bind.dialect.name
        self._insert_strategy = DialectFactory.get_strategy(dialect_name)

        # Set up dynamic attribute access for available tables
        self._setup_table_attributes()

    def _setup_table_attributes(self):
        """Set up dynamic attributes for available tables without security risks."""
        if self._table_registry is None:
            return

        for table_name in self._table_registry.available_tables:
            # Create placeholders for lazy loading - use None instead of 'None' string
            setattr(self, table_name, None)
            # Set up case variants for convenience (original behavior)
            setattr(self, table_name.lower(), None)
            setattr(self, table_name.upper(), None)

    def inject(self, bind, schema=None):
        """
        Reinitialize the DataBase with a new bind.

        Args:
            bind: SQLAlchemy engine or connection string
            schema: Optional database schema name
        """
        self.__init__(bind, schema)

    def __getattribute__(self, name):
        """Handle dynamic table access with lazy loading and security fixes."""
        # First, try normal attribute access, but avoid infinite recursion
        try:
            value = object.__getattribute__(self, name)

            # Check if this is a special internal attribute to avoid recursion
            if name.startswith('_'):
                return value

            # If the attribute is None and we have a table registry, try to load it
            if (value is None and
                hasattr(self, '_table_registry') and
                self._table_registry and
                hasattr(self._table_registry, 'get_table')):
                try:
                    # Lazy load the table (will try case variants automatically)
                    table_class = self._table_registry.get_table(name)
                    # Use object.__setattr__ to avoid recursion
                    object.__setattr__(self, name, table_class)
                    # Update case variants as well (using object.__setattr__ to avoid recursion)
                    object.__setattr__(self, name.lower(), table_class)
                    object.__setattr__(self, name.upper(), table_class)
                    return table_class
                except Exception:
                    # If table loading fails, return None
                    return None
            return value

        except AttributeError:
            # If the attribute doesn't exist at all, check if it's a table
            if (hasattr(self, '_table_registry') and
                self._table_registry and
                hasattr(self._table_registry, 'get_table')):
                try:
                    table_class = self._table_registry.get_table(name)
                    object.__setattr__(self, name, table_class)
                    # Also set up case variants (using object.__setattr__ to avoid recursion)
                    object.__setattr__(self, name.lower(), table_class)
                    object.__setattr__(self, name.upper(), table_class)
                    return table_class
                except Exception:
                    pass

            # Raise a more informative error
            available_tables = []
            if hasattr(self, '_table_registry') and self._table_registry:
                available_tables = getattr(self._table_registry, 'available_tables', [])

            raise AttributeError(
                f"'{name}' is not a valid table name. "
                f"Available tables: {reprlib.repr(available_tables)}"
            )

    def __getattr__(self, name):
        """Fallback attribute access for better error messages."""
        available_tables = []
        if self._table_registry:
            available_tables = self._table_registry.available_tables

        raise AttributeError(
            f"'{name}' is not a valid table name or attribute. "
            f"Available tables: {reprlib.repr(available_tables)}"
        )

    def __repr__(self):
        """Return a string representation of the DataBase object."""
        if self._bind is None:
            return f"<{type(self).__name__} (unbound)>"
        return str(self._bind).replace(
            type(self._bind).__name__,
            type(self).__name__)

    @property
    def bind(self):
        """Get the underlying SQLAlchemy engine."""
        return self._bind

    @property
    def schema(self):
        """Get the database schema name."""
        return self._schema

    @property
    def tables(self):
        """Get a list of all available table names."""
        if self._table_registry is None:
            return []
        return self._table_registry.available_tables

    def reflect(self):
        """
        Reflect all tables in the database and set up dynamic attributes.

        This method pre-loads all table metadata, which can be useful for
        development environments but may impact performance for large databases.
        """
        if self._table_registry is None:
            return

        reflected_tables = self._table_registry.reflect_all_tables()

        # Update instance attributes with reflected tables
        for table_name, table_class in reflected_tables.items():
            setattr(self, table_name, table_class)

    def query(self, *columns):
        """
        Create a SQLAlchemy query object.

        Args:
            *columns: Optional columns to query

        Returns:
            SQLAlchemy Query object with Sangreal enhancements

        Example:
            # Query all users
            result = db.query(db.users)

            # Query specific columns
            result = db.query(db.users.name, db.users.email)

            # Convert to DataFrame
            df = db.query(db.users).to_df()
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")
        return self._session.query(*columns)

    def update(self, t_obj):
        """
        Update or insert objects into the database.

        Args:
            t_obj: Single ORM object or iterable of ORM objects

        Example:
            # Update single object
            user = db.users(id=1)
            user.name = 'New Name'
            db.update(user)

            # Update multiple objects
            users = [db.users(id=i, name=f'User {i}') for i in range(1, 4)]
            db.update(users)
            db.commit()
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")

        if isinstance(t_obj, Iterable):
            self._session.add_all(t_obj)
        else:
            self._session.add(t_obj)

    def insert(self, table, insert_obj, ignore=True, index=None):
        """
        Insert bulk data into the database with dialect-specific optimizations.

        Args:
            table: SQLAlchemy table class (e.g., db.users)
            insert_obj: DataFrame or list of dictionaries to insert
            ignore: Whether to ignore duplicate key errors (default: True)
            index: Index name for Oracle ignore functionality (auto-detected if None)

        Raises:
            ValidationError: If input data format is invalid
            RuntimeError: If database is not connected

        Returns:
            Result object from SQLAlchemy execute

        Example:
            # Insert DataFrame
            df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [25, 30]})
            db.insert(db.users, df)

            # Insert list of dictionaries
            data = [{'name': 'Charlie', 'age': 35}, {'name': 'Diana', 'age': 28}]
            db.insert(db.users, data)

            # Insert without ignoring duplicates
            db.insert(db.users, data, ignore=False)
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")

        # Validate and convert input data
        if isinstance(insert_obj, pd.DataFrame):
            if insert_obj.empty:
                raise ValidationError('The input DataFrame is empty, please check!')
            insert_obj = insert_obj.to_dict('records')
        elif not isinstance(insert_obj, list):
            raise ValidationError(
                f"The insert_obj must be a pandas DataFrame or list of dicts, "
                f"got {type(insert_obj)} with value: {reprlib.repr(insert_obj)}"
            )

        # Validate dialect-specific parameters
        if self._insert_strategy:
            self._insert_strategy.validate_bulk_insert(ignore=ignore, table=table, index=index)

        # Get dialect-specific ignore clause
        ignore_str = ""
        if self._insert_strategy:
            ignore_str = self._insert_strategy.get_ignore_clause(
                ignore=ignore, table=table, index=index
            )

        # Execute the insert with appropriate prefix
        insert_statement = table.__table__.insert()
        if ignore_str:
            insert_statement = insert_statement.prefix_with(ignore_str)

        return self._session.execute(insert_statement.values(insert_obj))

    def delete(self, t_obj):
        """
        Delete an object from the database.

        Args:
            t_obj: ORM object to delete

        Returns:
            SQLAlchemy delete result

        Example:
            user = db.query(db.users).filter(db.users.id == 1).first()
            db.delete(user)
            db.commit()
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")
        return self._session.delete(t_obj)

    def close(self):
        """Close the database session."""
        if self._session is not None:
            return self._session.close()

    def merge(self, t_obj):
        """
        Merge an object into the current session (replace/merge operation).

        Args:
            t_obj: Single ORM object or iterable of ORM objects

        Returns:
            Merged ORM object

        Example:
            # Merge single object
            user = db.users(id=1, name='Updated Name')
            merged_user = db.merge(user)

            # Merge multiple objects
            users = [db.users(id=i, name=f'User {i}') for i in range(1, 4)]
            merged = [db.merge(user) for user in users]
            db.commit()
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")

        if isinstance(t_obj, Iterable):
            for t in t_obj:
                self._session.merge(t)
            return
        else:
            return self._session.merge(t_obj)

    def commit(self):
        """Commit the current transaction."""
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")
        return self._session.commit()

    def flush(self, objects=None):
        """
        Flush pending changes to the database.

        Args:
            objects: Optional objects to flush (default: all pending changes)
        """
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")
        return self._session.flush(objects=objects)

    def rollback(self):
        """Roll back the current transaction."""
        if self._session is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")
        return self._session.rollback()

    def refresh(self):
        """Refresh the database connection and table metadata."""
        if self._bind is not None and self._schema is not None:
            self.__init__(self._bind, self._schema)

    def create_all(self, tables=None, checkfirst=True):
        """
        Create all tables defined in the metadata.

        Args:
            tables: Optional list of tables to create (default: all)
            checkfirst: Whether to check for table existence first (default: True)

        Example:
            db.create_all()  # Create all tables
            db.create_all(checkfirst=True)  # Only create if they don't exist
        """
        if self._bind is None:
            raise RuntimeError("Database not connected. Call inject() to set up connection.")

        # Use the table registry's metadata for consistency
        if self._table_registry:
            self._table_registry._metadata.create_all(
                bind=self._bind,
                tables=tables,
                checkfirst=checkfirst
            )
            self.refresh()
        else:
            raise RuntimeError("Table registry not initialized.")


# Maintain backward compatibility with the original interface
    @property
    def metadata(self):
        """Access to the underlying metadata for backward compatibility."""
        if self._table_registry:
            return self._table_registry._metadata
        return None

    @staticmethod
    def _get_tables(bind, schema=None):
        """
        Get table names from database (backwards compatibility method).

        Args:
            bind: SQLAlchemy engine or connection
            schema: Optional database schema name

        Returns:
            List of table names
        """
        from sqlalchemy.engine import reflection
        insp = reflection.Inspector.from_engine(bind=bind)
        tables = insp.get_table_names(schema=schema)
        return tables


if __name__ == '__main__':
    pass
