"""Full sync: copy all rows from source to target database.

Used when a node first joins a cluster and needs a complete snapshot.
Reads all rows from each syncable table, batches them, and writes
to the target via INSERT ... ON CONFLICT DO UPDATE (upsert).
"""
from __future__ import annotations

from greymatter_core.db import now_sql
from greymatter_core.sync import SYNC_TABLES
from greymatter_core.sync.models import SyncResult
from greymatter_core.sync.tracking import _get_pk_col, get_sync_version


DEFAULT_BATCH_SIZE = 500


def full_sync(
    source,
    target,
    *,
    batch_size: int = DEFAULT_BATCH_SIZE,
    tables: list[str] | None = None,
) -> SyncResult:
    """Copy all rows from source DB to target DB.

    Args:
        source: Source database (implements Database Protocol).
        target: Target database.
        batch_size: Number of rows per INSERT batch.
        tables: Tables to sync (default: all SYNC_TABLES).

    Returns:
        SyncResult with row counts and any errors.
    """
    sync_tables = tables or SYNC_TABLES
    result = SyncResult()

    for table in sync_tables:
        try:
            pushed = _sync_table(source, target, table, batch_size)
            result.rows_pushed += pushed
            if pushed > 0:
                result.tables_synced += 1
        except Exception as e:
            result.errors.append(f"{table}: {e}")

    # Copy sync state (version counter) so target knows where to resume delta sync
    try:
        source_version = get_sync_version(source)
        target.execute(
            f"UPDATE _sync_state SET value = ?, updated_at = {now_sql(target)} "
            "WHERE key = ?",
            (str(source_version), "sync_version"),
        )
    except Exception as e:
        result.errors.append(f"_sync_state: {e}")

    return result


def _sync_table(source, target, table: str, batch_size: int) -> int:
    """Sync a single table. Returns number of rows pushed."""
    pk_col = _get_pk_col(table)
    rows = source.execute(f"SELECT * FROM {table}")

    if not rows:
        return 0

    total = 0
    for batch_start in range(0, len(rows), batch_size):
        batch = rows[batch_start : batch_start + batch_size]
        for row in batch:
            _upsert_row(target, table, pk_col, dict(row))
        total += len(batch)

    return total


def _upsert_row(target, table: str, pk_col: str, row: dict) -> None:
    """Insert or update a single row in the target database."""
    columns = list(row.keys())
    placeholders = ", ".join("?" for _ in columns)
    col_list = ", ".join(columns)

    # Build SET clause for conflict update (exclude PK)
    update_cols = [c for c in columns if c != pk_col]
    if update_cols:
        set_clause = ", ".join(f"{c} = excluded.{c}" for c in update_cols)
        sql = (
            f"INSERT INTO {table} ({col_list}) VALUES ({placeholders}) "
            f"ON CONFLICT({pk_col}) DO UPDATE SET {set_clause}"
        )
    else:
        sql = (
            f"INSERT INTO {table} ({col_list}) VALUES ({placeholders}) "
            f"ON CONFLICT({pk_col}) DO NOTHING"
        )

    values = tuple(row[c] for c in columns)
    target.execute(sql, values)
