"""Full-snapshot delta capture strategy.

Uses DuckDB EXCEPT to diff before/after snapshots — reusing the same
DuckDB-based comparison approach as ``in_memory_comparator``.

Best suited for small tables (below ``small_table_threshold``) where
PK discovery is unnecessary.
"""

from __future__ import annotations

import logging
from typing import Any

import duckdb
import pandas as pd

from ..models import DeltaStrategy, RowDict, TableConfig, make_delta
from . import DeltaCaptureStrategy

logger = logging.getLogger(__name__)


class FullSnapshotStrategy(DeltaCaptureStrategy):
    """Content-based diff using DuckDB EXCEPT."""

    @property
    def name(self) -> DeltaStrategy:
        return DeltaStrategy.FULL_SNAPSHOT

    def compute_delta(
        self,
        before: list[RowDict],
        after: list[RowDict],
        table_cfg: TableConfig,
    ) -> dict[str, Any]:
        columns = _resolve_columns(before or after, table_cfg)
        if not columns:
            return make_delta(table_cfg.fqn, self.name)

        conn = duckdb.connect()
        try:
            _register(conn, "before_snap", before, columns)
            _register(conn, "after_snap", after, columns)

            col_list = ", ".join(f'"{c}"' for c in columns)

            inserts = conn.execute(
                f"SELECT {col_list} FROM after_snap EXCEPT SELECT {col_list} FROM before_snap"
            ).fetchdf().to_dict(orient="records")

            deletes = conn.execute(
                f"SELECT {col_list} FROM before_snap EXCEPT SELECT {col_list} FROM after_snap"
            ).fetchdf().to_dict(orient="records")
        finally:
            conn.close()

        logger.debug(
            "full_snapshot delta for %s: +%d -%d",
            table_cfg.fqn, len(inserts), len(deletes),
        )
        return make_delta(
            table_cfg.fqn, self.name,
            inserts=inserts, deletes=deletes,
        )


def _resolve_columns(
    sample_rows: list[RowDict],
    table_cfg: TableConfig,
) -> list[str]:
    if not sample_rows:
        return []
    ignore = {c.upper() for c in table_cfg.ignore_columns}
    return sorted(c for c in sample_rows[0] if c.upper() not in ignore)


def _register(
    conn: duckdb.DuckDBPyConnection,
    name: str,
    rows: list[RowDict],
    columns: list[str],
) -> None:
    sanitized = [
        {k: (v.hex() if isinstance(v, (bytes, bytearray)) else v) for k, v in row.items()}
        for row in rows
    ] if rows else []
    df = pd.DataFrame(sanitized, columns=columns) if sanitized else pd.DataFrame(columns=columns)
    for col in df.columns:
        if df[col].dtype == "float64" and df[col].dropna().apply(float.is_integer).all():
            df[col] = df[col].astype("Int64")
    conn.register(name, df)
