"""
Database logging handler implementation.

Handles logging to database tables with batching, performance optimization,
and critical error flush mechanism.
"""

import logging
import signal
from datetime import datetime
from typing import Any

import sqlalchemy

from ...config import LogConfig
from .config import DatabaseHandlerConfig


class DatabaseHandler(logging.Handler):
    """
    Database logging handler.

    Handles logging to database tables with batching and custom data mapping.
    """

    def __init__(
        self,
        lg: Any,
        handler_config: DatabaseHandlerConfig,
        log_config: LogConfig,
        lifecycle_manager: Any = None,
    ) -> None:
        """
        Initialize database handler.

        Args:
            lg: Logger instance for error logging
            handler_config: Database handler configuration
            log_config: Logger configuration
            lifecycle_manager: Optional lifecycle manager for shutdown registration
        """
        super().__init__()
        self._lg = lg
        self.handler_config = handler_config
        self.log_config = log_config
        self.batch: list[dict[str, Any]] = []
        self.last_flush = datetime.now()

        # Performance optimizations: cache SQL statements and metadata
        self._sql_cache: dict[tuple, str] = {}  # Cache prepared statements
        self._table_metadata: Any = None  # Cache table metadata for bulk operations

        # Set handler level with proper resolution
        level = handler_config.level or log_config.level
        if isinstance(level, str):
            level = getattr(logging, level.upper(), logging.INFO)
        self.setLevel(level)

        # Register with lifecycle manager if provided
        if lifecycle_manager and hasattr(lifecycle_manager, "register_db_handler"):
            lifecycle_manager.register_db_handler(self)

    def emit(self, record: logging.LogRecord) -> None:
        """Emit a log record to the database."""
        try:
            # Map record to database row
            row_data = self.handler_config.data_mapper(record)

            # Check if this is a critical error requiring immediate flush
            if self._is_critical_error(record):
                self._critical_flush(row_data)
            else:
                # Normal batching behavior
                self.batch.append(row_data)
                if self._should_flush_batch():
                    self._flush_batch()

        except Exception:
            self.handleError(record)

    def _should_flush_batch(self) -> bool:
        """Check if the batch should be flushed based on size or time."""
        return len(self.batch) >= self.handler_config.batch_size or (
            self.handler_config.flush_interval > 0
            and (datetime.now() - self.last_flush).total_seconds()
            >= self.handler_config.flush_interval
        )

    def _is_critical_error(self, record: logging.LogRecord) -> bool:
        """Check if this log record contains critical error information."""
        if not self.handler_config.critical_flush_enabled:
            return False

        # Check if record has exception information
        if hasattr(record, "exc_info") and record.exc_info:
            return True

        # Check if 'extra' dict contains trigger fields
        if hasattr(record, "extra") and record.extra:
            for field in self.handler_config.critical_trigger_fields:
                if field in record.extra:
                    return True

        return False

    def _critical_flush(self, row_data: dict[str, Any]) -> None:
        """Immediately flush critical error to database."""
        try:
            # Use a timeout to prevent hanging during app crash

            def timeout_handler(signum: int, frame: Any) -> None:
                raise TimeoutError("Critical flush timeout")

            # Set timeout
            old_handler = signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(int(self.handler_config.critical_flush_timeout))

            try:
                # Immediate database write
                with self.handler_config.db_interface.session() as session:
                    self._insert_single_record(session, row_data)
                    session.commit()

            finally:
                # Restore signal handler
                signal.alarm(0)
                signal.signal(signal.SIGALRM, old_handler)

        except Exception:
            # Fallback to console if database flush fails
            if self.handler_config.fallback_to_console:
                self._lg.critical(f"CRITICAL ERROR (DB flush failed): {row_data}")
            raise

    def _get_insert_sql(self, columns_tuple: tuple) -> str:
        """Get cached INSERT SQL statement for given columns."""
        if columns_tuple not in self._sql_cache:
            columns = list(columns_tuple)
            column_names = ", ".join(columns)
            placeholders = ", ".join([f":{col}" for col in columns])
            table_name = self.handler_config.table_name
            self._sql_cache[columns_tuple] = (
                f"INSERT INTO {table_name} ({column_names}) VALUES ({placeholders})"
            )
        return self._sql_cache[columns_tuple]

    def _insert_single_record(self, session: Any, row_data: dict[str, Any]) -> None:
        """Insert a single record into the database table."""
        if not row_data:
            return

        # Use cached SQL statement
        columns_tuple = tuple(sorted(row_data.keys()))
        insert_sql = self._get_insert_sql(columns_tuple)
        session.execute(sqlalchemy.text(insert_sql), row_data)

    def _flush_batch(self) -> None:
        """Flush the current batch to the database."""
        if not self.batch:
            return

        try:
            # Get database session
            with self.handler_config.db_interface.session() as session:
                # Insert batch data
                self._insert_batch(session, self.batch)
                session.commit()

        except Exception as e:
            # Log error but don't raise to avoid infinite recursion
            self._lg.error("database logging error", extra={"exception": e})
        finally:
            self.batch.clear()
            self.last_flush = datetime.now()

    def _get_table_metadata(self, session: Any) -> Any:
        """Get cached table metadata for bulk operations."""
        if self._table_metadata is None:
            try:
                metadata = sqlalchemy.MetaData()
                self._table_metadata = sqlalchemy.Table(
                    self.handler_config.table_name, metadata, autoload_with=session.bind
                )
            except Exception:
                # If we can't get metadata, we'll fall back to executemany
                self._table_metadata = False
        return self._table_metadata

    def _insert_batch(self, session: Any, batch_data: list[dict[str, Any]]) -> None:
        """Insert batch data using optimized bulk operations."""
        if not batch_data:
            return

        try:
            # Try to use SQLAlchemy bulk operations for best performance
            table = self._get_table_metadata(session)
            if table is not False:
                # Use bulk_insert_mappings for maximum performance
                session.bulk_insert_mappings(table.__class__, batch_data)
                return
        except Exception:
            # Fall back to executemany if bulk operations fail
            pass

        # Fallback: Use executemany which is still much faster than individual executes
        if batch_data:
            columns_tuple = tuple(sorted(batch_data[0].keys()))
            insert_sql = self._get_insert_sql(columns_tuple)

            # Use executemany for better performance than individual execute calls
            session.execute(sqlalchemy.text(insert_sql), batch_data)

    def close(self) -> None:
        """Close the handler and flush any remaining data."""
        self._flush_batch()
        super().close()
