# #region Core.DbExecutor [C:3] [TYPE Module] [SEMANTICS database,execution,insert,direct]
# @defgroup Core Module group.
# @BRIEF Execute SQL directly against a target database using native drivers (asyncpg, clickhouse-connect).
#        Alternative to SupersetSqlLabExecutor — parallel path, NOT a replacement.
# @RELATION CALLS -> [Core.ConnectionService]
# @RELATION CALLS -> [Core.DatabaseConnection]
# @RATIONALE Separate executor isolates DB-specific logic from Translate.Orchestrator (C5).
# @REJECTED Embedding direct DB logic into Translate.Orchestrator was rejected — would bloat C5.
#            SQLAlchemy Core was rejected — asyncpg/clickhouse-connect provide better async performance.
#
# @NOTE This is a CORE SERVICE. ADR-0004's "No DB access for plugins" restriction applies to
#       subprocess plugins only, NOT to in-process core services.
# @see Core.SupersetSqlLabExecutor — parallel path for SQL Lab execution.

import time
from dataclasses import dataclass
from typing import Any


# #region DbExecutionResult [C:1] [TYPE DataClass] [SEMANTICS database,execution,result]
@dataclass
class DbExecutionResult:
    success: bool
    rows_affected: int = 0
    execution_time_ms: float = 0.0
    error: str | None = None
    db_version: str | None = None


# #endregion DbExecutionResult


# #region DbSchemaColumn [C:1] [TYPE DataClass] [SEMANTICS database,schema,column]
@dataclass
class DbSchemaColumn:
    name: str
    data_type: str | None = None
# #endregion DbSchemaColumn


# #region DbExecutor [C:3] [TYPE Class] [SEMANTICS database,execution,pool]
# @ingroup Core
# @BRIEF Direct database execution engine with connection pooling, dialect routing, error handling.
class DbExecutor:
    """Execute SQL directly against a target database.

    Connection pool lifecycle:
    - Pool created lazily on first execute_sql() call
    - Cached by connection_id
    - Recreated when connection config is updated (checked via updated_at timestamp)

    Dialect support:
    - postgresql: asyncpg driver, ``INSERT ... ON CONFLICT`` UPSERT
    - clickhouse: clickhouse-connect driver, plain INSERT
    - mysql: pymysql driver, ``INSERT ... ON DUPLICATE KEY UPDATE``
    """

    def __init__(self, connection_service: Any):
        """Initialize with a ConnectionService instance for resolving connection configs."""
        self._connection_service = connection_service
        self._pools: dict[str, Any] = {}  # connection_id -> pool/client
        self._pool_configs: dict[str, str] = {}  # connection_id -> updated_at timestamp

    # #region execute_sql [C:3] [TYPE Function] [SEMANTICS database,execution,execute]
    # @BRIEF Execute SQL against the target database via the configured connection.
    # @PRE DatabaseConnection exists, is valid, and the target database is reachable.
    # @POST SQL executed in a transaction. Result returned with rows_affected and timing.
    # @SIDE_EFFECT Writes data to target database. Opens/closes native driver connections.
    # @DATA_CONTRACT Input: (connection_id: str, sql: str) → Output: DbExecutionResult
    async def execute_sql(self, connection_id: str, sql: str) -> DbExecutionResult:
        """Execute SQL directly against the target database.

        Args:
            connection_id: UUID referencing a DatabaseConnection in GlobalSettings.
            sql: Dialect-appropriate SQL statement (INSERT/UPSERT).

        Returns:
            DbExecutionResult with success status, rows affected, timing, and error details.
        """
        conn = self._connection_service.get_connection(connection_id)
        if conn is None:
            return DbExecutionResult(
                success=False, error=f"Connection '{connection_id}' not found"
            )

        start = time.time()
        try:
            if conn.dialect == "postgresql":
                return await self._execute_pg(conn, sql, start)
            elif conn.dialect == "clickhouse":
                return self._execute_ch(conn, sql, start)
            elif conn.dialect == "mysql":
                return self._execute_mysql(conn, sql, start)
            else:
                return DbExecutionResult(
                    success=False, error=f"Unsupported dialect: {conn.dialect}"
                )
        except Exception as e:
            elapsed = int((time.time() - start) * 1000)
            return DbExecutionResult(
                success=False, error=str(e), execution_time_ms=elapsed
            )

    # #endregion execute_sql

    # #region _get_or_create_pool [C:2] [TYPE Function] [SEMANTICS database,pool,cache]
    # @BRIEF Get or create a connection pool for the given connection config.
    def _get_or_create_pool(self, conn_id: str, config_key: str) -> Any:
        if conn_id in self._pools and self._pool_configs.get(conn_id) == config_key:
            return self._pools[conn_id]
        return None

    def _set_pool(self, conn_id: str, pool: Any, config_key: str) -> None:
        self._pools[conn_id] = pool
        self._pool_configs[conn_id] = config_key

    # #endregion _get_or_create_pool

    # #region dialect executors [C:2] [TYPE Function] [SEMANTICS database,dialect,pg]
    # @BRIEF PostgreSQL execution via asyncpg with connection pooling.
    async def _execute_pg(self, conn: Any, sql: str, start: float) -> DbExecutionResult:
        try:
            import asyncpg  # type: ignore[import-untyped]
        except ImportError:
            return DbExecutionResult(
                success=False,
                error="asyncpg is not installed. Add to requirements.txt.",
            )

        config_key = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        pool = self._get_or_create_pool(conn.id, config_key)

        if pool is None:
            pool = await asyncpg.create_pool(
                host=conn.host,
                port=conn.port,
                user=conn.username,
                password=conn.password,
                database=conn.database,
                min_size=1,
                max_size=conn.pool_size,
            )
            self._set_pool(conn.id, pool, config_key)

        async with pool.acquire() as pg_conn:
            # asyncpg execute returns status string like "INSERT 0 5"
            status = await pg_conn.execute(sql)
            elapsed = int((time.time() - start) * 1000)
            # Parse rows affected from status string
            rows = 0
            if status:
                parts = status.split()
                if len(parts) >= 2:
                    try:
                        rows = int(parts[-1])
                    except ValueError:
                        rows = 0
            return DbExecutionResult(
                success=True,
                rows_affected=rows,
                execution_time_ms=elapsed,
            )

    # #endregion _execute_pg

    # #region _execute_ch [C:2] [TYPE Function] [SEMANTICS database,dialect,clickhouse]
    # @BRIEF ClickHouse execution via clickhouse-connect.
    def _execute_ch(self, conn: Any, sql: str, start: float) -> DbExecutionResult:
        try:
            import clickhouse_connect  # type: ignore[import-untyped]
        except ImportError:
            return DbExecutionResult(
                success=False,
                error="clickhouse-connect is not installed. Add to requirements.txt.",
            )

        config_key = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        client = self._get_or_create_pool(conn.id, config_key)

        if client is None:
            client = clickhouse_connect.get_client(
                host=conn.host,
                port=conn.port,
                username=conn.username,
                password=conn.password,
                database=conn.database,
                connect_timeout=30,
            )
            self._set_pool(conn.id, client, config_key)

        result = client.command(sql)
        elapsed = int((time.time() - start) * 1000)
        return DbExecutionResult(
            success=True,
            rows_affected=result if isinstance(result, int) else 0,
            execution_time_ms=elapsed,
        )

    # #endregion _execute_ch

    # #region _execute_mysql [C:2] [TYPE Function] [SEMANTICS database,dialect,mysql]
    # @BRIEF MySQL execution via pymysql.
    def _execute_mysql(self, conn: Any, sql: str, start: float) -> DbExecutionResult:
        try:
            import pymysql  # type: ignore[import-untyped]
        except ImportError:
            return DbExecutionResult(
                success=False,
                error="pymysql is not installed. Add to requirements.txt.",
            )

        config_key = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        pool_conn = self._get_or_create_pool(conn.id, config_key)

        if pool_conn is None:
            pool_conn = pymysql.connect(
                host=conn.host,
                port=conn.port,
                user=conn.username,
                password=conn.password,
                database=conn.database,
                connect_timeout=30,
                autocommit=True,
            )
            self._set_pool(conn.id, pool_conn, config_key)

        with pool_conn.cursor() as cursor:
            rows = cursor.execute(sql)
            elapsed = int((time.time() - start) * 1000)
            return DbExecutionResult(
                success=True,
                rows_affected=rows,
                execution_time_ms=elapsed,
            )

    # #endregion _execute_mysql

    # #region fetch_schema [C:3] [TYPE Function] [SEMANTICS database,schema,fetch,columns]
    # @BRIEF Fetch table schema (column names + types) from a direct DB connection.
    # @PRE connection_id must reference a valid saved connection.
    # @POST Returns list of DbSchemaColumn or None on failure.
    # @RELATION CALLS -> [Core.ConnectionService]
    async def fetch_schema(
        self, connection_id: str, schema: str, table: str, config_manager: Any
    ) -> list[DbSchemaColumn] | None:
        """Query target table columns directly via native driver."""
        from src.core.connection_service import ConnectionService

        service = ConnectionService(config_manager)
        conn = service.get_connection(connection_id)
        if not conn:
            return None

        safe_schema = schema.replace("'", "''")
        safe_table = table.replace("'", "''")

        # Dialect-specific schema query
        if conn.dialect == "clickhouse":
            sql = (
                f"SELECT name, type FROM system.columns "
                f"WHERE database = '{safe_schema}' AND table = '{safe_table}' "
                f"ORDER BY position"
            )
            return self._fetch_ch(conn, sql)

        if conn.dialect == "postgresql":
            sql = (
                f"SELECT column_name AS name, data_type AS type "
                f"FROM information_schema.columns "
                f"WHERE table_schema = '{safe_schema}' AND table_name = '{safe_table}' "
                f"ORDER BY ordinal_position"
            )
            return await self._fetch_pg(conn, sql)

        if conn.dialect == "mysql":
            sql = (
                f"SELECT column_name AS name, data_type AS type "
                f"FROM information_schema.columns "
                f"WHERE table_schema = '{safe_schema}' AND table_name = '{safe_table}' "
                f"ORDER BY ordinal_position"
            )
            return self._fetch_mysql(conn, sql)

        return None

    # #endregion fetch_schema

    # #region _fetch_pg [C:2] [TYPE Function] [SEMANTICS database,postgresql,fetch,schema]
    async def _fetch_pg(self, conn: Any, sql: str) -> list[DbSchemaColumn] | None:
        try:
            import asyncpg  # type: ignore[import-untyped]
        except ImportError:
            return None

        pool_cfg = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        pool = self._get_or_create_pool(conn.id, pool_cfg)
        if pool is None:
            pool = await asyncpg.create_pool(
                host=conn.host, port=conn.port,
                user=conn.username, password=conn.password,
                database=conn.database, min_size=1, max_size=conn.pool_size,
            )
            self._set_pool(conn.id, pool, pool_cfg)
        async with pool.acquire() as pg_conn:
            rows = await pg_conn.fetch(sql)
            return [DbSchemaColumn(name=r["name"], data_type=r["type"]) for r in rows]

    # #endregion _fetch_pg

    # #region _fetch_ch [C:2] [TYPE Function] [SEMANTICS database,clickhouse,fetch,schema]
    def _fetch_ch(self, conn: Any, sql: str) -> list[DbSchemaColumn] | None:
        try:
            import clickhouse_connect  # type: ignore[import-untyped]
        except ImportError:
            return None

        pool_cfg = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        client = self._get_or_create_pool(conn.id, pool_cfg)
        if client is None:
            client = clickhouse_connect.get_client(
                host=conn.host, port=conn.port,
                username=conn.username, password=conn.password,
                database=conn.database, connect_timeout=30,
            )
            self._set_pool(conn.id, client, pool_cfg)

        rows = client.query(sql).result_rows
        return [DbSchemaColumn(name=r[0], data_type=r[1]) for r in rows] if rows else []

    # #endregion _fetch_ch

    # #region _fetch_mysql [C:2] [TYPE Function] [SEMANTICS database,mysql,fetch,schema]
    def _fetch_mysql(self, conn: Any, sql: str) -> list[DbSchemaColumn] | None:
        try:
            import pymysql  # type: ignore[import-untyped]
        except ImportError:
            return None

        pool_cfg = f"{conn.host}:{conn.port}/{conn.database}@{conn.updated_at}"
        db = self._get_or_create_pool(conn.id, pool_cfg)
        if db is None:
            db = pymysql.connect(
                host=conn.host, port=conn.port,
                user=conn.username, password=conn.password,
                database=conn.database, connect_timeout=30, autocommit=True,
            )
            self._set_pool(conn.id, db, pool_cfg)

        with db.cursor() as cursor:
            cursor.execute(sql)
            rows = cursor.fetchall()
            return [DbSchemaColumn(name=r[0], data_type=r[1]) for r in rows] if rows else []

    # #endregion _fetch_mysql


# #endregion DbExecutor
# #endregion Core.DbExecutor
