"""Replay validation handler — compare deltas on the Snowflake target.

Integrates with the validate runner to perform delta-based comparison
when the baseline contains ``table_deltas`` (i.e. the procedure was
classified as ``modifies_data=True`` during capture).

The flow on the *validate* side:

1. Clone the target database (SnowflakeCloneIsolation) or begin txn
2. Snapshot affected tables (before)
3. Execute the converted procedure on Snowflake
4. Snapshot affected tables (after)
5. Compute actual deltas
6. Compare baseline deltas vs actual deltas (delegates to in_memory_comparator)
7. Drop the clone / rollback
"""

from __future__ import annotations

import logging
from typing import Any

from ..common.config import ReplayConfiguration
from ..common.models import BaselineData
from .delta_capture import compute_table_delta, snapshot_table
from .delta_comparator import compare_table_deltas
from ..common.database_executor import DatabaseExecutorFactory
from .isolation import SnowflakeCloneIsolation
from .models import TableConfig, delta_total_changes

logger = logging.getLogger(__name__)

_EMPTY_DELTA: dict[str, Any] = {
    "table": "",
    "strategy_used": "full_snapshot",
    "inserts": [],
    "deletes": [],
    "updates": [],
}


class DeltaComparisonResult:
    """Result of comparing baseline vs actual table deltas."""

    __slots__ = ("match", "match_type", "details")

    def __init__(
        self,
        match: bool,
        match_type: str,
        details: dict[str, dict[str, Any]],
    ) -> None:
        self.match = match
        self.match_type = match_type
        self.details = details

    def to_dict(self) -> dict[str, Any]:
        return {
            "match": self.match,
            "match_type": self.match_type,
            "details": self.details,
        }


def validate_deltas(
    connection: Any,
    baseline: BaselineData,
    replay_cfg: ReplayConfiguration,
    execute_fn: Any,
    database: str | None = None,
    procedure: str = "",
    params_hash: str = "",
    affected_tables: list[str] | None = None,
    clone_db: str | None = None,
    executor_factory: DatabaseExecutorFactory | None = None,
) -> DeltaComparisonResult:
    """Execute the procedure on Snowflake and compare deltas.

    Args:
        connection: Snowflake connection.
        baseline: Baseline with ``table_deltas`` from capture.
        replay_cfg: Replay configuration with thresholds.
        execute_fn: Callable accepting ``(database_override: str | None)``
            that executes the converted procedure on Snowflake.  When a
            clone database name is passed, the caller must rewrite its
            SQL to target the clone.
        database: Snowflake database name (for clone isolation).
        procedure: Procedure name (for clone suffix).
        params_hash: Parameter hash (for clone suffix).
        affected_tables: Fallback table list (from CUR) used when
            ``baseline.table_deltas`` is empty.  Enables detection of
            erroneous Snowflake mutations even when the source procedure
            produced no deltas.
        clone_db: Pre-created clone database name.  When provided, no
            new clone is created — the caller owns setup/teardown.
        executor_factory: The dialect's executor factory, used to create
            the appropriate isolation provider via
            ``executor_factory.create_isolation()``.

    Raises:
        ValueError: If *executor_factory* is ``None`` and isolation is
            needed (i.e. *clone_db* is not provided).
    """
    baseline_deltas_upper: dict[str, dict[str, Any]] = {}
    if baseline.table_deltas:
        baseline_deltas_upper = {
            _normalize_fqn(k): v for k, v in baseline.table_deltas.items()
        }

    tables_to_check = list(baseline_deltas_upper.keys())
    if not tables_to_check and affected_tables:
        tables_to_check = [_normalize_fqn(t) for t in affected_tables]

    if not tables_to_check:
        return DeltaComparisonResult(
            match=True, match_type="no_deltas", details={},
        )

    table_configs = _resolve_table_configs(
        tables_to_check,
        ignore_columns=replay_cfg.delta_ignore_columns,
    )
    col_meta_upper: dict[str, dict[str, list[str]]] | None = None
    if baseline.column_metadata:
        col_meta_upper = {_normalize_fqn(k): v for k, v in baseline.column_metadata.items()}
    _merge_column_metadata(table_configs, col_meta_upper)

    owns_isolation = clone_db is None
    isolation = None
    if owns_isolation:
        isolation = executor_factory.create_isolation(
            source_database=database or "",
            procedure=procedure,
            params_hash=params_hash,
        )

    result = DeltaComparisonResult(
        match=True, match_type="delta_match", details={},
    )

    try:
        if isolation is not None:
            isolation.setup(connection)
            if isinstance(isolation, SnowflakeCloneIsolation):
                clone_db = isolation.clone_database

        before_snapshots: dict[str, list[dict[str, Any]]] = {}
        for tc in table_configs:
            before_snapshots[tc.fqn] = snapshot_table(
                connection, tc.schema_name, tc.table_name,
                database=clone_db,
            )

        execute_fn(clone_db)

        for tc in table_configs:
            after = snapshot_table(
                connection, tc.schema_name, tc.table_name,
                database=clone_db,
            )
            tc.estimated_row_count = max(
                len(before_snapshots[tc.fqn]), len(after),
            )

            actual_delta = compute_table_delta(
                before_snapshots[tc.fqn], after, tc, replay_cfg,
            )

            baseline_delta = baseline_deltas_upper.get(tc.fqn, _EMPTY_DELTA)

            table_result = compare_table_deltas(
                baseline_delta, actual_delta, tc.fqn,
                ignore_columns=tc.ignore_columns,
            )
            result.details[tc.fqn] = table_result

            if not table_result["match"]:
                result.match = False
                result.match_type = "delta_mismatch"

    finally:
        if isolation is not None:
            isolation.teardown(connection)

    return result


def _normalize_fqn(fqn: str) -> str:
    """Normalise a table FQN to uppercase ``SCHEMA.TABLE``, stripping any
    leading database prefix (e.g. ``WideWorldImporters.Warehouse.Colors``
    becomes ``WAREHOUSE.COLORS``).

    NOTE: This 2-part normalisation assumes single-database migrations.
    For multi-database setups where different databases can have identically
    named SCHEMA.TABLE, this will need to become 3-part with a database
    mapping layer (original DB → clone DB).  See SNOW-3232922.
    """
    parts = fqn.upper().split(".")
    return ".".join(parts[-2:]) if len(parts) >= 2 else fqn.upper()


def _resolve_table_configs(
    affected_tables: list[str],
    ignore_columns: list[str] | None = None,
) -> list[TableConfig]:
    """Parse ``SCHEMA.TABLE`` strings into :class:`TableConfig` objects."""
    configs: list[TableConfig] = []
    for fqn in affected_tables:
        parts = fqn.split(".")
        if len(parts) == 2:
            schema, table = parts
        elif len(parts) == 3:
            _, schema, table = parts
        else:
            schema, table = "", fqn
        configs.append(TableConfig(
            schema_name=schema.upper(),
            table_name=table.upper(),
            ignore_columns=list(ignore_columns) if ignore_columns else [],
        ))
    return configs


def _merge_column_metadata(
    table_configs: list[TableConfig],
    column_metadata: dict[str, dict[str, list[str]]] | None,
) -> None:
    """Union auto-detected platform-managed columns into each table's ignore list.

    Modifies *table_configs* in place.  Columns from
    ``column_metadata[fqn]["platform_managed_columns"]`` are merged
    (case-insensitive) with any manually configured ``ignore_columns``.
    """
    if not column_metadata:
        return
    for tc in table_configs:
        entry = column_metadata.get(tc.fqn)
        if not entry:
            continue
        existing = {c.upper() for c in tc.ignore_columns}
        for col in entry.get("platform_managed_columns", []):
            if col.upper() not in existing:
                tc.ignore_columns.append(col.upper())
                existing.add(col.upper())
