# #region TargetSchemaValidation [C:4] [TYPE Module] [SEMANTICS translate, schema, validation, target-table]
# @BRIEF Проверка схемы целевой таблицы: запрос колонок через Superset SQL Lab,
#        сравнение с ожидаемыми (из build_columns), возврат diff.
# @LAYER Service
# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]
# @RELATION DEPENDS_ON -> [EXT:method:schemas.translate.TargetSchemaValidationRequest]
# @RELATION DEPENDS_ON -> [EXT:method:schemas.translate.TargetSchemaValidationResponse]
# @PRE Superset окружение доступно, target_database_id валиден.
# @POST Возвращает актуальные, ожидаемые, отсутствующие и лишние колонки.
# @SIDE_EFFECT Выполняет SQL-запрос через Superset SQL Lab.
# @RATIONALE C4 — оркестрация: вызов executor, парсинг ответа, diff и построение ответа.
# #endregion TargetSchemaValidation

import re
from typing import Any

from ...core.config_manager import ConfigManager
from ...core.logger import belief_scope, logger
from ...schemas.translate import (
    TargetSchemaColumnInfo,
    TargetSchemaValidationRequest,
    TargetSchemaValidationResponse,
)
from .superset_executor import SupersetSqlLabExecutor

_TABLE_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')


# #region _build_expected_columns [C:2] [TYPE Function]
# @BRIEF Собирает список ожидаемых колонок по конфигурации column mapping.
def _build_expected_columns(req: TargetSchemaValidationRequest) -> list[TargetSchemaColumnInfo]:
    """Определяет, какие колонки INSERT будет пытаться заполнить."""
    names: list[str] = []

    if req.target_key_cols:
        names.extend(req.target_key_cols)

    effective_target = req.target_column or req.translation_column
    if effective_target:
        names.append(effective_target)

    if req.target_language_column:
        names.append(req.target_language_column)

    if req.target_source_column:
        names.append(req.target_source_column)

    if req.target_source_language_column:
        names.append(req.target_source_language_column)

    names.append("context")
    names.append("is_original")

    seen: set[str] = set()
    deduped: list[str] = []
    for c in names:
        if c and c not in seen:
            deduped.append(c)
            seen.add(c)

    return [TargetSchemaColumnInfo(name=n) for n in deduped]
# #endregion _build_expected_columns


# #region _extract_columns_from_rows [C:2] [TYPE Function]
# @BRIEF Извлекает список колонок таблицы из data-строк результата SQL Lab.
#
# Для PostgreSQL (information_schema.columns) каждая строка содержит:
#   {"column_name": "id", "data_type": "integer", "is_nullable": "YES"}
#
# Для ClickHouse (DESCRIBE TABLE) каждая строка содержит:
#   {"name": "id", "type": "Int32"}
#
# Функция ищет имя колонки по нескольким возможным ключам и возвращает
# унифицированный список TargetSchemaColumnInfo.
def _extract_columns_from_rows(data_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """
    Извлекает информацию о колонках таблицы из data-строк SQL Lab ответа.
    """
    columns_info: list[dict[str, Any]] = []

    for row in data_rows:
        if not isinstance(row, dict):
            continue

        # Имя колонки: ищем по всем возможным ключам (PG, CH, разные версии Superset)
        col_name = (
            row.get("column_name")  # PostgreSQL information_schema
            or row.get("name")       # ClickHouse DESCRIBE TABLE / MySQL SHOW COLUMNS
            or row.get("Field")      # MySQL SHOW COLUMNS
        )
        if not col_name:
            continue

        # Тип данных
        col_type = (
            row.get("data_type")  # PG information_schema
            or row.get("type")     # ClickHouse DESCRIBE TABLE
            or row.get("Type")     # MySQL SHOW COLUMNS
        )

        # Nullable
        is_nullable = row.get("is_nullable")  # PG
        if isinstance(is_nullable, str):
            is_nullable = is_nullable.upper() == "YES"
        elif not isinstance(is_nullable, bool):
            is_nullable = True  # ClickHouse не возвращает is_nullable, считаем True

        columns_info.append({
            "name": str(col_name),
            "type": str(col_type) if col_type else None,
            "is_nullable": is_nullable,
        })

    return columns_info
# #endregion _extract_columns_from_rows


# #region _parse_sqllab_result [C:2] [TYPE Function]
# @BRIEF Извлекает data-строки из ответа Superset SQL Lab.
#   Поддерживает 3 формата: sync mode, async polling, get_query_results.
def _parse_sqllab_result(result: dict[str, Any]) -> tuple[list[dict[str, Any]], bool]:
    """
    Извлекает строки данных из ответа SQL Lab.
    Returns (data_rows, table_exists).
    """
    if not result:
        return [], False

    raw = result.get("raw_response", result)
    if not raw:
        return [], False

    data_rows = None

    # Формат 1: прямой ответ с data
    if "data" in raw and isinstance(raw["data"], list):
        data_rows = raw["data"]

    # Формат 2: вложенный result (async polling)
    elif "result" in raw and isinstance(raw["result"], dict):
        res = raw["result"]
        data_rows = res.get("data") if isinstance(res.get("data"), list) else None

    # Формат 3: ключ results (get_query_results)
    elif "results" in raw and isinstance(raw["results"], dict):
        res = raw["results"]
        if "data" in res and isinstance(res["data"], list):
            data_rows = res["data"]
        elif isinstance(res.get("result"), dict):
            sub = res["result"]
            data_rows = sub.get("data") if isinstance(sub.get("data"), list) else None

    # Формат 4: results — список строк (execute_and_poll sync mode возвращает так)
    elif "results" in raw and isinstance(raw["results"], list):
        data_rows = raw["results"]

    if data_rows is None:
        return [], False

    table_exists = len(data_rows) > 0

    # Нормализуем формат строк: если data_rows — список списков, а не словарей
    # (бывает в некоторых версиях Superset), конвертируем через columns metadata
    if data_rows and not isinstance(data_rows[0], dict):
        columns_raw = raw.get("columns") or (raw.get("result") or {}).get("columns") or []
        col_names = []
        for c in columns_raw:
            cn = c.get("name") or c.get("column_name") or ""
            if cn:
                col_names.append(cn)
        if col_names:
            normalized = []
            for row in data_rows:
                if isinstance(row, list) and len(row) == len(col_names):
                    normalized.append(dict(zip(col_names, row)))
            data_rows = normalized

    return data_rows, table_exists
# #endregion _parse_sqllab_result


# #region validate_target_table_schema [C:4] [TYPE Function] [SEMANTICS translate, schema, validate, orchestrate]
# @BRIEF Основная функция: проверяет схему целевой таблицы через Superset SQL Lab.
# @PRE Superset окружение и target_database_id валидны.
# @POST Возвращает TargetSchemaValidationResponse с diff-анализом.
# @SIDE_EFFECT Выполняет SQL-запрос к information_schema через Superset SQL Lab API.
# @RELATION DEPENDS_ON -> [_build_expected_columns]
# @RELATION DEPENDS_ON -> [_extract_columns_from_rows]
# @RELATION DEPENDS_ON -> [_parse_sqllab_result]
# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]
def validate_target_table_schema(
    req: TargetSchemaValidationRequest,
    config_manager: ConfigManager,
) -> TargetSchemaValidationResponse:
    """
    Проверяет схему целевой таблицы:
    1. Вычисляет ожидаемые колонки
    2. Запрашивает актуальные через Superset SQL Lab
    3. Сравнивает и возвращает diff
    """
    with belief_scope("validate_target_table_schema"):
        # 1. Ожидаемые колонки
        expected = _build_expected_columns(req)
        expected_names = {c.name for c in expected}
        logger.reason("Built expected columns",
                      extra={"payload": {"count": len(expected), "columns": [c.name for c in expected]}})

        # 2. Валидация имён (SQL injection protection)
        if req.target_table and not _TABLE_NAME_RE.match(req.target_table):
            logger.explore("Invalid target table name rejected",
                          extra={"payload": {"table": req.target_table},
                                 "error": "Invalid characters in table name"})
            return TargetSchemaValidationResponse(
                table_exists=False,
                error=f"Invalid target table name: '{req.target_table}'. Only alphanumeric and underscore allowed.",
                expected_columns=expected, actual_columns=[],
                missing_columns=expected, extra_columns=[], all_present=False,
            )
        if req.target_schema and not _TABLE_NAME_RE.match(req.target_schema):
            logger.explore("Invalid target schema name rejected",
                          extra={"payload": {"schema": req.target_schema},
                                 "error": "Invalid characters in schema name"})
            return TargetSchemaValidationResponse(
                table_exists=False,
                error=f"Invalid target schema name: '{req.target_schema}'. Only alphanumeric and underscore allowed.",
                expected_columns=expected, actual_columns=[],
                missing_columns=expected, extra_columns=[], all_present=False,
            )

        # 3. Запрос через Superset SQL Lab
        target_ref = f"{req.target_schema or 'public'}.{req.target_table}"
        logger.reason("Querying target table schema",
                      extra={"payload": {"table": target_ref, "env": req.environment_id, "db_id": req.target_database_id}})

        try:
            executor = SupersetSqlLabExecutor(config_manager, req.environment_id)
            executor.resolve_database_id(target_database_id=req.target_database_id)
            backend = executor.get_database_backend() or ""
            db_name = executor.get_database_name() or "unknown"

            safe_schema = (req.target_schema or "public").replace("'", "''")
            safe_table = req.target_table.replace("'", "''")

            # Нормализуем backend через маппинг (как в get_dialect_from_database)
            # чтобы правильно обработать "clickhousedb" → "clickhouse",
            # "greenplum" → "postgresql" и другие варианты
            _backend_normalize = {
                "clickhouse": "clickhouse",
                "clickhousedb": "clickhouse",
                "postgresql": "postgresql",
                "greenplum": "postgresql",
                "mysql": "mysql",
                "mssql": "mssql",
                "sqlite": "sqlite",
                "oracle": "oracle",
                "snowflake": "snowflake",
                "bigquery": "bigquery",
                "redshift": "redshift",
                "presto": "presto",
                "trino": "trino",
                "druid": "druid",
                "hive": "hive",
                "spark": "spark",
                "databricks": "databricks",
            }
            normalized_backend = _backend_normalize.get(backend.lower().strip(), backend.lower().strip())
            # ClickHouse использует system.columns, всё остальное — information_schema.columns
            is_clickhouse = normalized_backend == "clickhouse"
            if is_clickhouse:
                sql = (
                    f"SELECT name, type, default_expression AS data_default "
                    f"FROM system.columns "
                    f"WHERE database = '{safe_schema}' "
                    f"AND table = '{safe_table}' "
                    f"ORDER BY position"
                )
            else:
                sql = (
                    f"SELECT column_name, data_type, is_nullable "
                    f"FROM information_schema.columns "
                    f"WHERE table_schema = '{safe_schema}' "
                    f"AND table_name = '{safe_table}' "
                    f"ORDER BY ordinal_position"
                )

            logger.reason("Executing SQL Lab query",
                          extra={"payload": {"sql": sql[:200], "backend": backend, "database_name": db_name}})

            result = executor.execute_and_poll(sql=sql, max_polls=15, poll_interval_seconds=1.0)
            status = result.get("status", "")

            if status == "failed":
                error_msg = result.get("error_message", "SQL Lab query failed")
                logger.explore("SQL Lab query failed",
                              extra={"payload": {"table": target_ref}, "error": error_msg})
                return TargetSchemaValidationResponse(
                    table_exists=False, error=f"Superset SQL Lab error: {error_msg}",
                    expected_columns=expected, actual_columns=[],
                    missing_columns=expected, extra_columns=[], all_present=False,
                    database_name=db_name, database_backend=backend,
                )

            # Парсим data-строки результата (реальные колонки таблицы)
            data_rows, table_exists = _parse_sqllab_result(result)
            actual_cols_raw = _extract_columns_from_rows(data_rows)

            actual_cols = []
            for col in actual_cols_raw:
                actual_cols.append(TargetSchemaColumnInfo(
                    name=col["name"],
                    data_type=col.get("type"),
                    is_nullable=col.get("is_nullable", True),
                ))

            actual_names = {c.name for c in actual_cols}

            # 4. Diff
            missing = [col for col in expected if col.name not in actual_names]
            extra = [col for col in actual_cols if col.name not in expected_names]

            logger.reflect("Schema check complete",
                          extra={"payload": {
                              "table": target_ref, "table_exists": table_exists,
                              "expected": len(expected), "found": len(actual_cols),
                              "missing": len(missing), "extra": len(extra),
                              "all_present": len(missing) == 0,
                              "actual_columns": [c.name for c in actual_cols],
                          }})

            return TargetSchemaValidationResponse(
                table_exists=table_exists,
                expected_columns=expected,
                actual_columns=actual_cols,
                missing_columns=missing,
                extra_columns=extra,
                all_present=len(missing) == 0,
                database_name=db_name, database_backend=backend,
            )

        except Exception as e:
            logger.explore("Target schema validation failed",
                          extra={"payload": {"table": target_ref, "env": req.environment_id},
                                 "error": str(e)})
            return TargetSchemaValidationResponse(
                table_exists=False, error=f"Failed to validate target schema: {e}",
                expected_columns=expected, actual_columns=[],
                missing_columns=expected, extra_columns=[], all_present=False,
                database_name=db_name, database_backend=backend,
            )
# #endregion validate_target_table_schema
# #endregion TargetSchemaValidation
