"""Sync outbox for crash-safe change propagation.

The outbox pattern ensures that no write is lost between local commit
and sync cycle. Every change that needs to reach the cluster is written
to _sync_outbox atomically (same transaction as the data write). The
sync engine drains the outbox rather than scanning _sync_log.

Flow:
  1. App writes data + outbox entry in one transaction
  2. Sync scheduler periodically drains outbox → pushes to remote
  3. On success, outbox entry is marked delivered
  4. On crash recovery, undelivered outbox entries are replayed

This replaces the "hope the next delta scan catches it" approach
with guaranteed at-least-once delivery.
"""
from __future__ import annotations

from typing import Any

from greymatter_core.db import now_sql


def create_outbox_table(db) -> None:
    """Create the _sync_outbox table if it doesn't exist."""
    db.execute("""
        CREATE TABLE IF NOT EXISTS _sync_outbox (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            table_name TEXT NOT NULL,
            row_id TEXT NOT NULL,
            operation TEXT NOT NULL DEFAULT 'upsert',
            created_at TEXT DEFAULT (datetime('now')),
            delivered_at TEXT DEFAULT NULL,
            attempts INTEGER DEFAULT 0,
            last_error TEXT DEFAULT NULL
        )
    """)
    db.execute(
        "CREATE INDEX IF NOT EXISTS idx_outbox_pending "
        "ON _sync_outbox(delivered_at) WHERE delivered_at IS NULL"
    )


def enqueue(db, table_name: str, row_id: str, operation: str = "upsert") -> int:
    """Add a change to the outbox. Returns the outbox entry ID.

    Call this in the SAME transaction as the data write to guarantee
    the outbox entry exists if and only if the data write committed.
    """
    result = db.execute(
        "INSERT INTO _sync_outbox (table_name, row_id, operation) "
        "VALUES (?, ?, ?) RETURNING id",
        (table_name, row_id, operation),
    )
    if result and isinstance(result, list) and result[0].get("id"):
        return result[0]["id"]
    # Fallback for SQLite versions without RETURNING
    row = db.execute_one(
        "SELECT MAX(id) AS id FROM _sync_outbox "
        "WHERE table_name = ? AND row_id = ?",
        (table_name, row_id),
    )
    return row["id"] if row else 0


def get_pending(db, limit: int = 500) -> list[dict[str, Any]]:
    """Fetch undelivered outbox entries, oldest first.

    Returns list of dicts with: id, table_name, row_id, operation, attempts.
    """
    return db.execute(
        "SELECT id, table_name, row_id, operation, attempts "
        "FROM _sync_outbox "
        "WHERE delivered_at IS NULL "
        "ORDER BY id ASC LIMIT ?",
        (limit,),
    )


def mark_delivered(db, outbox_ids: list[int]) -> int:
    """Mark outbox entries as successfully delivered.

    Args:
        db: Database connection.
        outbox_ids: List of outbox entry IDs to mark.

    Returns:
        Number of entries marked.
    """
    if not outbox_ids:
        return 0
    placeholders = ",".join("?" for _ in outbox_ids)
    db.execute(
        f"UPDATE _sync_outbox SET delivered_at = {now_sql(db)} "
        f"WHERE id IN ({placeholders})",
        tuple(outbox_ids),
    )
    return len(outbox_ids)


def mark_failed(db, outbox_id: int, error: str) -> None:
    """Record a delivery failure for retry."""
    db.execute(
        "UPDATE _sync_outbox SET attempts = attempts + 1, last_error = ? "
        "WHERE id = ?",
        (error, outbox_id),
    )


def cleanup_delivered(db, retain_hours: int = 24) -> int:
    """Remove delivered outbox entries older than retain_hours.

    Returns number of entries removed.
    """
    if hasattr(db, "dialect") and db.dialect == "crdb":
        cutoff_expr = f"now() - INTERVAL '{retain_hours} hours'"
    else:
        cutoff_expr = f"datetime('now', '-{retain_hours} hours')"

    count_row = db.execute_one(
        f"SELECT COUNT(*) AS cnt FROM _sync_outbox "
        f"WHERE delivered_at IS NOT NULL "
        f"AND delivered_at < {cutoff_expr}",
    )
    count = count_row["cnt"] if count_row else 0
    if count > 0:
        db.execute(
            f"DELETE FROM _sync_outbox "
            f"WHERE delivered_at IS NOT NULL "
            f"AND delivered_at < {cutoff_expr}",
        )
    return count


def get_stats(db) -> dict[str, int]:
    """Get outbox statistics for monitoring."""
    pending = db.execute_one(
        "SELECT COUNT(*) AS cnt FROM _sync_outbox WHERE delivered_at IS NULL"
    )
    failed = db.execute_one(
        "SELECT COUNT(*) AS cnt FROM _sync_outbox "
        "WHERE delivered_at IS NULL AND attempts > 0"
    )
    delivered = db.execute_one(
        "SELECT COUNT(*) AS cnt FROM _sync_outbox WHERE delivered_at IS NOT NULL"
    )
    return {
        "pending": pending["cnt"] if pending else 0,
        "failed": failed["cnt"] if failed else 0,
        "delivered": delivered["cnt"] if delivered else 0,
    }
