"""
Result comparison logic for validation -- Snowflake-native comparison.

Materializes both baseline and actual result sets into Snowflake transient tables,
then uses the data-validation framework's native ``MetadataExtractorSnowflake``
and ``SyncValidationExecutor`` to run schema, metrics, and row validation
entirely via SQL.
"""

from __future__ import annotations

import logging
from typing import Any, Optional

from snowflake.snowflake_data_validation.connector.connector_base import ConnectorBase
from snowflake.snowflake_data_validation.executer.sync_validation_executor import (
    SyncValidationExecutor,
)
from snowflake.snowflake_data_validation.snowflake.extractor.metadata_extractor_snowflake import (
    MetadataExtractorSnowflake,
)
from snowflake.snowflake_data_validation.snowflake.query.query_generator_snowflake import (
    QueryGeneratorSnowflake,
)
from snowflake.snowflake_data_validation.utils.constants import ValidationLevel
from snowflake.snowflake_data_validation.validation.validation_database_manager import (
    ValidationDatabaseManager,
)

from test_runner.common.config import TestRunnerConfig, ValidationConfiguration
from test_runner.validate.in_memory_comparator import InMemoryResultComparator

from snowflake.snowflake_data_validation.utils.context import Context

from .adapters.context_factory import build_context
from .adapters.model_factory import (
    build_table_column_metadata,
    build_table_configuration,
)
from .adapters.output_handler import LoggingOutputHandler
from test_runner.common.models import (
    MAX_ERROR_LENGTH,
    ColumnTypeMap,
    ComparisonResult,
    ValidationDifference,
    truncate,
)

from .transient_table_manager import (
    materialize_baseline_transient_table,
    materialize_actual_transient_table,
    materialize_baseline_output_params_table,
    materialize_actual_output_params_table,
    drop_transient_tables,
)


LOGGER = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Result comparator (Snowflake-native)
# ---------------------------------------------------------------------------

class ResultComparator:
    """Compare baseline and actual result sets using Snowflake-native validation.

    Materializes both sides into transient tables and delegates to
    ``SyncValidationExecutor`` for schema, metrics, and row comparison via SQL.
    """

    def __init__(
        self,
        config: TestRunnerConfig,
        connector: ConnectorBase,
        code_unit_name: str = "",
    ) -> None:
        self._config = config
        self._connector = connector
        self._code_unit_name = code_unit_name
        self._database = config.validation_database
        self._context = build_context(config)

        vc = config.validation_configuration or ValidationConfiguration()
        self._table_config = build_table_configuration(
            code_unit_name,
            schema_validation=False,
            metrics_validation=vc.metrics_validation,
            row_validation=vc.row_validation,
            max_failed_rows_number=vc.max_failed_rows_number,
        )

    def compare(
        self,
        rendered_sql: str,
        params_hash: str,
        fetch_stmts: list[str] | None = None,
        outputs_sql: str | None = None,
        outputs_only: bool = False,
    ) -> ComparisonResult:
        """Run the comparison using Snowflake temp tables.

        When *outputs_only* is ``True`` (baseline has no result sets, only
        output params), the result-set materialization is skipped entirely.

        Otherwise performs two independent validations when *outputs_sql*
        is provided:
        1. Result set comparison (main proc output)
        2. Output parameter comparison (via anonymous block)

        Both are run independently; the result includes failures from either.
        """
        tables_to_drop: list[str] = []

        try:
            result: ComparisonResult | None = None

            # --- Result set comparison (skip when baseline has no result sets) ---
            if not outputs_only:
                target_table = materialize_actual_transient_table(
                    self._connector,
                    self._database,
                    rendered_sql,
                    self._code_unit_name,
                    params_hash,
                    fetch_stmts=fetch_stmts,
                )
                tables_to_drop.append(target_table)

                target_col_types_precise = self._get_column_types_from_table(
                    target_table, include_precision=True,
                )

                source_table = materialize_baseline_transient_table(
                    self._connector,
                    self._database,
                    self._code_unit_name,
                    params_hash,
                    target_col_types=target_col_types_precise,
                )
                tables_to_drop.append(source_table)

                result = self._compare_tables(source_table, target_table)

            # --- Output parameter comparison ---
            if outputs_sql:
                outputs_result = self._compare_output_params(
                    params_hash, outputs_sql, tables_to_drop,
                )
                if outputs_result is not None:
                    if result is not None:
                        result = self._merge_results(result, outputs_result)
                    else:
                        result = outputs_result

            if result is None:
                return ComparisonResult(
                    match=True, match_type="no_data",
                )

            return result

        except Exception as exc:
            LOGGER.warning("Comparison failed: %s", exc)
            return ComparisonResult(
                match=False,
                match_type="error",
                differences=[
                    ValidationDifference(type="ERROR", detail=truncate(str(exc), MAX_ERROR_LENGTH)),
                ],
            )
        finally:
            drop_transient_tables(self._connector, *tables_to_drop)

    @staticmethod
    def _merge_results(
        result_set_result: ComparisonResult,
        output_params_result: ComparisonResult,
    ) -> ComparisonResult:
        """Merge result set and output parameter comparison results.

        Both validations are independent; the merged result reports
        failures from either side.
        """
        if result_set_result.match and output_params_result.match:
            return result_set_result

        merged_diffs: list[ValidationDifference | str] = list(result_set_result.differences)
        for diff in output_params_result.differences:
            if isinstance(diff, ValidationDifference):
                merged_diffs.append(ValidationDifference(
                    level=diff.level, type=diff.type,
                    detail=diff.detail, source="output_params",
                ))
            else:
                merged_diffs.append(diff)

        if not result_set_result.match:
            return ComparisonResult(
                match=result_set_result.match,
                match_type=result_set_result.match_type,
                match_level=result_set_result.match_level,
                differences=merged_diffs,
            )

        return ComparisonResult(
            match=False,
            match_level=output_params_result.match_level,
            match_type=f"output_params_{output_params_result.match_type or 'mismatch'}",
            differences=merged_diffs,
        )

    def _compare_output_params(
        self,
        params_hash: str,
        outputs_sql: str,
        tables_to_drop: list[str],
    ) -> ComparisonResult | None:
        """Compare output parameter values between baseline and actual."""
        source_outputs = materialize_baseline_output_params_table(
            self._connector,
            self._database,
            self._code_unit_name,
            params_hash,
        )
        if source_outputs is None:
            return None
        tables_to_drop.append(source_outputs)

        target_outputs = materialize_actual_output_params_table(
            self._connector,
            self._database,
            outputs_sql,
            self._code_unit_name,
            params_hash,
        )
        tables_to_drop.append(target_outputs)

        return self._compare_tables(source_outputs, target_outputs)

    def _compare_tables(
        self,
        source_table: str,
        target_table: str,
    ) -> ComparisonResult:
        """Run the data-validation comparison on two tables.

        Creates a fresh context for each comparison so that the
        ``LoggingOutputHandler`` state is isolated between calls.
        """
        context = build_context(self._config)

        query_generator = QueryGeneratorSnowflake()

        source_extractor = MetadataExtractorSnowflake(
            connector=self._connector,
            query_generator=query_generator,
        )
        target_extractor = MetadataExtractorSnowflake(
            connector=self._connector,
            query_generator=query_generator,
        )

        source_col_types = self._get_column_types_from_table(source_table)
        target_col_types = self._get_column_types_from_table(target_table)

        all_columns = list(source_col_types.keys())
        index_columns = all_columns[:1] if all_columns else []

        source_metadata = build_table_column_metadata(
            source_col_types,
            self._get_row_count(source_table),
            self._code_unit_name,
        )
        target_metadata = build_table_column_metadata(
            target_col_types,
            self._get_row_count(target_table),
            self._code_unit_name,
        )

        table_config = build_table_configuration(
            self._code_unit_name,
            schema_validation=self._table_config.validation_configuration.schema_validation,
            metrics_validation=self._table_config.validation_configuration.metrics_validation,
            row_validation=self._table_config.validation_configuration.row_validation,
            source_table=source_table,
            target_table=target_table,
            database=self._database,
            index_column_list=index_columns,
            max_failed_rows_number=self._table_config.max_failed_rows_number or 100,
            column_selection_list=all_columns,
        )

        executor = SyncValidationExecutor(
            source_extractor=source_extractor,
            target_extractor=target_extractor,
            context=context,
            db_manager=ValidationDatabaseManager(),
        )

        executor.execute_validation_levels(
            validation_config=table_config.validation_configuration,
            table_configuration=table_config,
            source_table_column_metadata=source_metadata,
            target_table_column_metadata=target_metadata,
        )

        return self._collect_result(context)

    def _get_column_types_from_table(
        self, table_name: str, *, include_precision: bool = False,
    ) -> ColumnTypeMap:
        """Query INFORMATION_SCHEMA.COLUMNS for the temp table's column types.

        When *include_precision* is True, NUMBER columns include precision
        and scale (e.g. ``NUMBER(12,2)``) — useful for casting.
        When False (default), returns bare type names that the validation
        framework expects.
        """
        parts = table_name.split(".")
        db_name = parts[-3] if len(parts) >= 3 else self._database
        schema_name = parts[-2].upper() if len(parts) >= 2 else "VALIDATION"
        tbl_name = parts[-1].upper()

        try:
            rows = self._connector.execute_query(f"""\
SELECT COLUMN_NAME, DATA_TYPE, NUMERIC_PRECISION, NUMERIC_SCALE
FROM {db_name}.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '{tbl_name}'
  AND TABLE_SCHEMA = '{schema_name}'
ORDER BY ORDINAL_POSITION""")
            result = {}
            for row in rows:
                values = list(row.values())
                col_name = values[0].upper()
                data_type = values[1].upper()
                if include_precision:
                    precision = values[2]
                    scale = values[3]
                    if data_type == "NUMBER" and precision is not None and scale is not None:
                        data_type = f"NUMBER({precision},{scale})"
                result[col_name] = data_type
            return result
        except Exception:
            return {}

    def _get_row_count(self, table_name: str) -> int:
        """Get the row count of a temp table."""
        try:
            rows = self._connector.execute_query(
                f"SELECT COUNT(*) FROM {table_name}"
            )
            if rows:
                row = rows[0]
                return int(list(row.values())[0])
            return 0
        except Exception:
            return 0

    @staticmethod
    def _collect_result(context: Context) -> ComparisonResult:
        """Collect validation results from the context's output handler."""
        handler = context.output_handler
        if isinstance(handler, LoggingOutputHandler) and handler.has_failures:
            return ComparisonResult(
                match=False,
                match_level=handler.failed_level or "",
                match_type=_match_type_from_level(handler.failed_level),
                differences=list(handler.differences[:10]),
            )

        return ComparisonResult(
            match=True,
            match_level=ValidationLevel.ROW_VALIDATION.value,
            match_type="data_match",
        )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def compare_results(
    config: TestRunnerConfig,
    connector: ConnectorBase,
    rendered_sql: str,
    params_hash: str,
    code_unit_name: str,
    baseline: Optional[Any] = None,
    dump_baseline_and_actual: bool = False,
    fetch_stmts: list[str] | None = None,
    outputs_sql: str | None = None,
    outputs_only: bool = False,
) -> ComparisonResult:
    """Compare baseline and actual result sets.

    Routes to ``InMemoryResultComparator`` when baseline row count is within
    ``in_memory_row_threshold``; otherwise uses the Snowflake transient-table path.

    When *outputs_only* is ``True`` the baseline has no result sets (the
    procedure only sets output parameters), so the result-set comparison
    is skipped and only output parameters are compared.

    When *outputs_sql* is provided, also compares output parameter values
    after the main result set comparison.
    """
    threshold = 0
    vc = config.validation_configuration
    if vc is not None:
        threshold = vc.in_memory_row_threshold

    row_count = 0
    if baseline is not None and getattr(baseline, "row_counts", None):
        # Compare/materialize logic uses the last result set for output-table paths.
        row_count = baseline.row_counts[-1]

    if outputs_only:
        LOGGER.info(
            "Baseline has no result sets for %s — comparing output params only",
            code_unit_name,
        )

    if not outputs_only and threshold > 0 and row_count > threshold:
        LOGGER.info(
            "Using transient comparator for %s (row_count=%d, threshold=%d)",
            code_unit_name, row_count, threshold,
        )
        comparator = ResultComparator(config, connector, code_unit_name)
        return comparator.compare(
            rendered_sql,
            params_hash,
            fetch_stmts=fetch_stmts,
            outputs_sql=outputs_sql,
            outputs_only=outputs_only,
        )

    LOGGER.info(
        "Using in-memory comparator for %s (row_count=%s, threshold=%d, outputs_only=%s)",
        code_unit_name,
        row_count,
        threshold,
        outputs_only,
    )
    comparator = InMemoryResultComparator(config, connector, code_unit_name)
    return comparator.compare(
        rendered_sql,
        params_hash,
        dump_baseline_and_actual=dump_baseline_and_actual,
        fetch_stmts=fetch_stmts,
        outputs_sql=outputs_sql,
        outputs_only=outputs_only,
    )


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _match_type_from_level(level: str | None) -> str:
    if level == ValidationLevel.SCHEMA_VALIDATION.value:
        return "schema_mismatch"
    if level == ValidationLevel.METRICS_VALIDATION.value:
        return "metrics_mismatch"
    if level == ValidationLevel.ROW_VALIDATION.value:
        return "data_mismatch"
    return "unknown"


def errors_match(baseline_error: str, actual_error: str) -> bool:
    """Check whether two error messages represent the same error type."""
    if not baseline_error or not actual_error:
        return baseline_error == actual_error
    b_prefix = baseline_error.strip().upper().split(":")[0]
    a_prefix = actual_error.strip().upper().split(":")[0]
    return b_prefix == a_prefix
