# #region BatchInsertService [C:3] [TYPE Module] [SEMANTICS translate, insert, sqllab, direct_db, upsert, target]
# @defgroup Translate Module group.
# @BRIEF Insert successful translation records into target table via Superset SQL Lab OR direct DB.
#         Builds INSERT SQL (original + per-language rows), resolves backend dialect,
#         and executes via SupersetSqlLabExecutor or DbExecutor depending on job.insert_method.
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]
# @RELATION DEPENDS_ON -> [SQLGenerator], [SupersetSqlLabExecutor]
# @RELATION DEPENDS_ON -> [Core.DbExecutor], [Core.ConnectionService]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @PRE Batch has committed TranslationRecords with status SUCCESS. Job has target_table configured.
# @POST Per record, N+1 rows are INSERTED: 1 original + N translations.
# @SIDE_EFFECT HTTP call to Superset SQL Lab API OR native DB driver call. Writes to target database.
# @RATIONALE Extracted from BatchProcessingService (583 lines) to comply with INV_7.
#            Direct DB path added for ClickHouse and other native driver support per job.insert_method.
# @REJECTED Inline insert logic in process_batch — made the function 583 lines.

import json

from sqlalchemy.orm import Session, joinedload

from ...core.config_manager import ConfigManager
from ...core.connection_service import ConnectionService
from ...core.db_executor import DbExecutor
from ...core.logger import belief_scope, logger
from ...models.translate import TranslationJob, TranslationRecord
from .sql_generator import SQLGenerator, _normalize_timestamp_value
from .superset_executor import SupersetSqlLabExecutor


# #region insert_batch_to_target [C:3] [TYPE Function] [SEMANTICS translate, insert, orchestrate]
# @ingroup Translate
# @BRIEF Insert successful records from a single batch into the target table.
async def insert_batch_to_target(
    db: Session, config_manager: ConfigManager,
    job: TranslationJob, batch_id: str, run_id: str,
) -> None:
    """Insert successful batch records into the target table via Superset SQL Lab."""
    with belief_scope("BatchInsertService.insert_batch_to_target"):
        records = _fetch_batch_records(db, batch_id)
        if not records:
            return

        effective_target = job.target_column or job.translation_column
        primary_language = (job.target_languages or ["en"])[0]
        columns = _build_target_columns(job, effective_target)
        context_keys = _build_context_keys(job, effective_target)
        rows_for_sql = _build_insert_rows(records, job, effective_target, primary_language, context_keys)

        if not columns:
            columns = [effective_target or "translated_text"]
            rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records]

        dialect, executor, connection_id = await _resolve_insert_backend(config_manager, job, batch_id)
        if dialect is None:
            return

        # Use chunked INSERT to avoid SQL size limits (max 500 VALUES rows per statement)
        statements = SQLGenerator.generate_batch(
            dialect=dialect,
            target_schema=job.target_schema,
            target_table=job.target_table or "translated_data",
            columns=columns,
            rows=rows_for_sql,
            key_columns=job.target_key_cols,
            upsert_strategy=job.upsert_strategy or "MERGE",
            max_rows_per_statement=500,
        )
        total_inserted = 0
        for sql, chunk_count in statements:
            if sql is None:
                continue
            await _execute_insert_sql(executor, sql, batch_id, chunk_count, connection_id=connection_id)
            total_inserted += chunk_count
        logger.reason(f"Batch {batch_id[:12]} inserted {total_inserted} rows",
                      {"batch_id": batch_id, "rows": total_inserted, "chunks": len(statements)})
# #endregion insert_batch_to_target


# #region _fetch_batch_records [C:1] [TYPE Function]
def _fetch_batch_records(db: Session, batch_id: str) -> list[TranslationRecord]:
    """Fetch successful TranslationRecords for a batch with languages."""
    return (
        db.query(TranslationRecord)
        .options(joinedload(TranslationRecord.languages))
        .filter(
            TranslationRecord.batch_id == batch_id,
            TranslationRecord.status == "SUCCESS",
            TranslationRecord.target_sql.isnot(None),
        )
        .all()
    )
# #endregion _fetch_batch_records


# #region _build_target_columns [C:1] [TYPE Function]
def _build_target_columns(job: TranslationJob, effective_target: str | None) -> list[str]:
    """Build the list of column names for the target INSERT."""
    columns: list[str] = []
    if job.target_key_cols:
        columns.extend(job.target_key_cols)
    if effective_target:
        columns.append(effective_target)
    if job.target_language_column:
        columns.append(job.target_language_column)
    if job.target_source_column:
        columns.append(job.target_source_column)
    if job.target_source_language_column:
        columns.append(job.target_source_language_column)
    columns.append("context")
    columns.append("is_original")
    seen: set[str] = set()
    deduped: list[str] = []
    for c in columns:
        if c and c not in seen:
            deduped.append(c)
            seen.add(c)
    return deduped
# #endregion _build_target_columns


# #region _build_context_keys [C:1] [TYPE Function]
def _build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:
    """Build context keys for JSON bundled column."""
    context_keys = list(job.context_columns or [])
    if (
        job.translation_column
        and job.translation_column != effective_target
        and job.translation_column not in context_keys
    ):
        context_keys.append(job.translation_column)
    return context_keys
# #endregion _build_context_keys


# #region _build_insert_rows [C:3] [TYPE Function]
def _build_insert_rows(
    records: list[TranslationRecord], job: TranslationJob,
    effective_target: str | None, primary_language: str, context_keys: list[str],
) -> list[dict[str, object]]:
    """Build row dicts for the INSERT SQL statement."""
    rows_for_sql: list[dict[str, object]] = []
    for rec in records:
        source_data = rec.source_data or {}
        detected_src_lang = "und"
        if rec.languages and len(rec.languages) > 0:
            detected_src_lang = rec.languages[0].source_language_detected or "und"

        context_data: dict[str, str] = {}
        for key in context_keys:
            val = source_data.get(key)
            context_data[key] = str(val) if val is not None else ""

        base_row: dict[str, object] = {}
        if job.target_key_cols:
            for k in job.target_key_cols:
                raw = source_data.get(k)
                if raw is not None:
                    normalized = _normalize_timestamp_value(raw)
                    base_row[k] = normalized if normalized else raw
                else:
                    base_row[k] = None
        if job.target_source_column:
            base_row[job.target_source_column] = rec.source_sql or ""
        if job.target_source_language_column:
            base_row[job.target_source_language_column] = detected_src_lang
        base_row["context"] = json.dumps(context_data, ensure_ascii=False)

        original_row = dict(base_row)
        if effective_target:
            original_row[effective_target] = rec.source_sql or ""
        if job.target_language_column:
            original_row[job.target_language_column] = detected_src_lang
        original_row["is_original"] = 1
        rows_for_sql.append(original_row)

        if rec.languages and len(rec.languages) > 0:
            for lang in rec.languages:
                if lang.language_code == detected_src_lang:
                    continue
                trans_row = dict(base_row)
                trans_value = lang.final_value or lang.translated_value or ""
                if effective_target:
                    trans_row[effective_target] = trans_value
                if job.target_language_column:
                    trans_row[job.target_language_column] = lang.language_code
                trans_row["is_original"] = 0
                rows_for_sql.append(trans_row)
        else:
            fallback = dict(base_row)
            if effective_target:
                fallback[effective_target] = rec.target_sql or ""
            if job.target_language_column:
                fallback[job.target_language_column] = primary_language
            fallback["is_original"] = 0
            rows_for_sql.append(fallback)
    return rows_for_sql
# #endregion _build_insert_rows


# #region _resolve_insert_backend [C:2] [TYPE Function] [SEMANTICS translate, insert, backend, dispatch]
# @ingroup Translate
# @BRIEF Resolve the database backend and SQL executor for batch insert.
#         Checks job.insert_method — direct_db routes to DbExecutor, otherwise SupersetSqlLabExecutor.
# @PRE config_manager is valid. job has target_table configured.
# @POST Returns (dialect, executor, connection_id). connection_id is None for sqllab path.
# @RELATION DEPENDS_ON -> [Core.ConnectionService]
# @RELATION DEPENDS_ON -> [Core.DbExecutor]
# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]
# @RATIONALE Direct DB path added for ClickHouse and other native driver support.
#            Previously always used Superset SQL Lab, ignoring job.insert_method.
async def _resolve_insert_backend(
    config_manager: ConfigManager, job: TranslationJob, batch_id: str,
) -> tuple[str | None, SupersetSqlLabExecutor | DbExecutor | None, str | None]:
    """Resolve the database backend and SQL executor for batch insert.

    Returns:
        Tuple of (dialect, executor, connection_id):
        - direct_db: dialect from connection, DbExecutor, job.connection_id
        - sqllab:    dialect from Superset backend, SupersetSqlLabExecutor, None
        - failure:   None, None, None (logging already emitted)
    """
    # ── Direct DB path ──
    if getattr(job, "insert_method", None) == "direct_db":
        if not job.connection_id:
            raise ValueError(
                f"Batch {batch_id}: insert_method=direct_db but connection_id is missing"
            )
        conn_service = ConnectionService(config_manager)
        conn = conn_service.get_connection(job.connection_id)
        if not conn:
            raise ValueError(
                f"Batch {batch_id}: connection_id='{job.connection_id}' not found in global settings"
            )
        logger.reason("Batch insert using direct DB connection",
                      {"batch_id": batch_id, "dialect": conn.dialect, "connection": conn.name})
        executor: SupersetSqlLabExecutor | DbExecutor = DbExecutor(conn_service)
        return conn.dialect, executor, job.connection_id

    # ── Superset SQL Lab path (default) ──
    try:
        env_id = job.environment_id or job.source_dialect or ""
        executor = SupersetSqlLabExecutor(config_manager, env_id)
        await executor.resolve_database_id(target_database_id=job.target_database_id)
        real_backend = executor.get_database_backend()
    except Exception as e:
        logger.explore("Failed to resolve database backend for batch insert",
                       {"batch_id": batch_id, "error": str(e)})
        return None, None, None
    dialect = real_backend or job.database_dialect or job.target_dialect or "postgresql"
    return dialect, executor, None
# #endregion _resolve_insert_backend


# #region _generate_insert_sql [C:1] [TYPE Function]
def _generate_insert_sql(
    dialect: str, job: TranslationJob, columns: list[str],
    rows_for_sql: list[dict[str, object]], batch_id: str = "",
) -> tuple[str | None, int]:
    """Generate INSERT SQL using SQLGenerator."""
    try:
        sql, row_count = SQLGenerator.generate(
            dialect=dialect, target_schema=job.target_schema,
            target_table=job.target_table or "translated_data",
            columns=columns, rows=rows_for_sql,
            key_columns=job.target_key_cols,
            upsert_strategy=job.upsert_strategy or "MERGE",
        )
        return sql, row_count
    except ValueError as e:
        logger.explore("SQL generation failed for batch", {"batch_id": batch_id, "error": str(e)})
        return None, 0
# #endregion _generate_insert_sql


# #region _execute_insert_sql [C:2] [TYPE Function] [SEMANTICS translate, insert, execute, dispatch]
# @ingroup Translate
# @BRIEF Execute INSERT SQL via either Superset SQL Lab or direct DB (DbExecutor).
# @PRE executor is a valid initialized executor. sql is dialect-appropriate.
# @POST SQL executed. Status logged (non-fatal — batch processing continues).
# @SIDE_EFFECT Writes to target DB or calls Superset API.
# @RATIONALE Branch on executor type: DbExecutor uses execute_sql(connection_id),
#            SupersetSqlLabExecutor uses execute_and_poll(). connection_id discriminates the path.
async def _execute_insert_sql(
    executor: SupersetSqlLabExecutor | DbExecutor, sql: str,
    batch_id: str, row_count: int, connection_id: str | None = None,
) -> None:
    """Execute the INSERT SQL via Superset SQL Lab or direct DB."""
    try:
        if isinstance(executor, DbExecutor):
            result = await executor.execute_sql(connection_id, sql)  # type: ignore[arg-type]
            status = "success" if result.success else "failed"
            if not result.success:
                logger.explore("Direct DB insert failed for batch",
                               {"batch_id": batch_id, "error": result.error},
                               error=result.error or "Unknown DB error")
        else:
            # Superset SQL Lab path
            result = await executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)
            status = result.get("status", "unknown")
    except Exception as e:
        logger.explore("SQL submission failed for batch", {"batch_id": batch_id, "error": str(e)})
        return
    logger.reason(f"Chunk inserted for batch {batch_id[:12]}",
                  {"batch_id": batch_id, "rows": row_count, "status": status})
# #endregion _execute_insert_sql
# #endregion BatchInsertService
