"""
Abstract base classes for database executors and factories.

Each database dialect (SQL Server, Oracle, Snowflake, ...) implements
:class:`DatabaseExecutorFactory` to provide:

- A dialect-specific connection config builder
- An executor that can connect, execute SQL, and capture result sets

SQL literal formatting is a separate concern provided by
:class:`LiteralFormatter`.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, Optional

from .models import TestCaseResult, ValidationSteps
from .database_dialect import DatabaseDialect


# ---------------------------------------------------------------------------
# Literal formatter (ISP: separated from factory)
# ---------------------------------------------------------------------------

class LiteralFormatter(ABC):
    """Format Python values as SQL literals for a specific dialect.

    Separated from :class:`DatabaseExecutorFactory` so that runners can
    depend on the formatting capability without pulling in executor
    creation logic (Interface Segregation Principle).
    """

    @abstractmethod
    def format_literal(self, value: Any) -> str:
        """Format a Python value as a SQL literal in this dialect."""


# ---------------------------------------------------------------------------
# Executor
# ---------------------------------------------------------------------------

class DatabaseExecutor(ABC):
    """Executes SQL and captures result sets.

    Instances are created already connected by
    :meth:`DatabaseExecutorFactory.create_executor`.
    """

    @abstractmethod
    def close(self) -> None:
        """Close the database connection."""

    @abstractmethod
    def execute(
        self,
        sql: str,
        steps: ValidationSteps | None = None,
        fetch_stmts: list[str] | None = None,
    ) -> TestCaseResult:
        """Execute *sql* and return captured result sets.

        *steps* provides optional metadata (``capture`` directives,
        ``output_parameters``) that dialect-specific executors can use
        for post-execution result collection.

        *fetch_stmts* is a list of pre-rendered SQL statements produced
        by ``render_output_fetch_statements()`` — e.g.
        ``FETCH ALL FROM cursor`` or ``SELECT * FROM IDENTIFIER('table')``.
        The executor runs each one in sequence after the CALL.
        """

    @property
    def connector(self) -> Optional[Any]:
        """Access the underlying database connector, if available.

        Subclasses that wrap a ``ConnectorBase`` should override this.
        Returns ``None`` by default so callers don't need ``hasattr`` checks.
        """
        return None

    def execute_statement(self, sql: str) -> None:
        """Execute a DDL/DML statement without returning results.

        Optional -- raises ``NotImplementedError`` by default.
        """
        raise NotImplementedError(
            f"{type(self).__name__} does not support execute_statement"
        )

    def __enter__(self) -> DatabaseExecutor:
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        self.close()


# ---------------------------------------------------------------------------
# Factory
# ---------------------------------------------------------------------------

class DatabaseExecutorFactory(ABC):
    """Abstract factory for creating dialect-specific executors.

    Implementations are registered in the DI container and resolved at
    runtime based on the configured dialect.

    Also acts as a :class:`LiteralFormatter` for its dialect so that
    runners can obtain both the executor and the formatter from the
    same factory.
    """

    @property
    @abstractmethod
    def dialect(self) -> DatabaseDialect:
        """The database dialect this factory handles."""

    @abstractmethod
    def build_config(self, raw: dict[str, Any]) -> Any:
        """Parse a raw YAML connection dict into a typed config."""

    @abstractmethod
    def create_executor(self, config: Any) -> DatabaseExecutor:
        """Create a new executor for the given dialect-specific config."""

    @abstractmethod
    def create_literal_formatter(self) -> LiteralFormatter:
        """Create a literal formatter for this dialect."""

    def create_isolation(self, **kwargs: Any) -> Any:
        """Create an isolation provider for this dialect.

        The default returns :class:`TransactionIsolation`.
        Dialects with different transaction semantics should override.
        """
        from test_runner.replay.isolation import TransactionIsolation
        return TransactionIsolation()

    def create_introspector(self) -> Any:
        """Create a source introspector for this dialect.

        The default returns :class:`DefaultIntrospector` which uppercases
        identifiers and performs no column introspection.
        """
        from test_runner.replay.source_introspector import DefaultIntrospector
        return DefaultIntrospector()
