"""Background sync process for uploading local data to Supabase.

Runs as a separate OS process (multiprocessing.Process). Reads pending
rows from the local SQLite database, batch-uploads via Supabase RPC
functions, and deletes the rows after confirmed upload. SQLite WAL mode
enables concurrent access from both the main process (writer) and this
sync process (reader + deleter).

Communication with the main process is via the SQLite database itself:
  - Pending rows: any row in configs/metric_points tables
  - Closing signal: sync_state['closing'] = '1'
  - Status: run_meta['status'] (read at close time, sent via update_run_status)
"""

from __future__ import annotations

import logging
import multiprocessing
import sqlite3
import time
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional

from supabase import create_client, Client

logger = logging.getLogger("goodseed.sync")


def _get_connection(db_path: str) -> sqlite3.Connection:
    """Open SQLite in WAL mode for concurrent access."""
    conn = sqlite3.connect(db_path, check_same_thread=False)
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=5000")
    conn.row_factory = sqlite3.Row
    return conn


class _SyncWorker:
    """Internal worker that runs inside the sync process."""

    def __init__(
        self,
        db_path: str,
        project: str,
        run_name: str,
        experiment_name: Optional[str],
        supabase_url: str,
        supabase_key: str,
        sync_interval: float = 0.5,
        batch_size: int = 5000,
    ):
        self.db_path = db_path
        self.project = project
        self.run_name = run_name
        self.experiment_name = experiment_name
        self.supabase_url = supabase_url
        self.supabase_key = supabase_key
        self.sync_interval = sync_interval
        self.batch_size = batch_size

        self._conn: Optional[sqlite3.Connection] = None
        self._client: Optional[Client] = None
        self._run_uuid: Optional[str] = None
        self._backoff: float = sync_interval
        self._max_backoff: float = 30.0

    def run(self) -> None:
        """Main sync loop. Runs until closing signal received and all data synced."""
        self._conn = _get_connection(self.db_path)
        self._client = create_client(self.supabase_url, self.supabase_key)

        try:
            while True:
                closing = self._is_closing()
                try:
                    synced_any = self._sync_pass()
                    if synced_any:
                        self._backoff = self.sync_interval
                except Exception:
                    logger.exception("Sync error")
                    self._backoff = min(self._backoff * 2, self._max_backoff)

                if closing:
                    # Final drain: keep syncing until nothing left
                    try:
                        while self._sync_pass():
                            pass
                    except Exception:
                        logger.exception("Error during final drain")

                    # Update run status on server
                    try:
                        self._update_run_status()
                    except Exception:
                        logger.exception("Error updating run status")
                    break

                time.sleep(self._backoff)
        finally:
            if self._conn:
                self._conn.close()

    def _is_closing(self) -> bool:
        row = self._conn.execute(
            "SELECT value FROM sync_state WHERE key = 'closing'"
        ).fetchone()
        return row is not None and row["value"] == "1"

    def _ensure_run(self) -> str:
        if self._run_uuid:
            return self._run_uuid
        result = self._client.rpc(
            "ensure_run",
            {
                "p_project": self.project,
                "p_run_name": self.run_name,
                "p_experiment_name": self.experiment_name,
            },
        ).execute()
        self._run_uuid = result.data
        return self._run_uuid

    def _sync_pass(self) -> bool:
        """Run one sync cycle. Returns True if any data was synced."""
        synced = False
        synced |= self._sync_configs()
        synced |= self._sync_metrics()
        return synced

    def _sync_configs(self) -> bool:
        rows = self._conn.execute(
            "SELECT rowid, path, type_tag, value, updated_at FROM configs LIMIT ?",
            (self.batch_size,),
        ).fetchall()
        if not rows:
            return False

        run_uuid = self._ensure_run()
        configs_json = [
            {
                "path": row["path"],
                "type_tag": row["type_tag"],
                "value": row["value"],
                "updated_at": row["updated_at"],
            }
            for row in rows
        ]
        self._client.rpc(
            "batch_upsert_configs",
            {"p_run_id": run_uuid, "p_configs": configs_json},
        ).execute()

        # Delete synced rows by rowid
        rowids = [row["rowid"] for row in rows]
        placeholders = ",".join("?" * len(rowids))
        self._conn.execute(
            f"DELETE FROM configs WHERE rowid IN ({placeholders})",
            rowids,
        )
        self._conn.commit()
        return True

    def _sync_metrics(self) -> bool:
        rows = self._conn.execute(
            """SELECT rowid, path, step, y, ts FROM metric_points
               ORDER BY path, step LIMIT ?""",
            (self.batch_size,),
        ).fetchall()
        if not rows:
            return False

        run_uuid = self._ensure_run()

        # Group by path for the append_metric_points RPC (one call per path)
        by_path: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
        for row in rows:
            by_path[row["path"]].append({
                "step": row["step"],
                "y": row["y"],
                "ts": row["ts"],
            })

        for path, points in by_path.items():
            self._client.rpc(
                "append_metric_points",
                {
                    "p_run_id": run_uuid,
                    "p_path": path,
                    "p_points": points,
                },
            ).execute()

        # Delete synced rows by rowid
        rowids = [row["rowid"] for row in rows]
        placeholders = ",".join("?" * len(rowids))
        self._conn.execute(
            f"DELETE FROM metric_points WHERE rowid IN ({placeholders})",
            rowids,
        )
        self._conn.commit()
        return True

    def _update_run_status(self) -> None:
        """Read status from local meta and send to server."""
        row = self._conn.execute(
            "SELECT value FROM run_meta WHERE key = 'status'"
        ).fetchone()
        status = row["value"] if row else "finished"
        run_uuid = self._ensure_run()
        self._client.rpc(
            "update_run_status",
            {"p_run_id": run_uuid, "p_status": status},
        ).execute()


def _sync_process_entry(
    db_path: str,
    project: str,
    run_name: str,
    experiment_name: Optional[str],
    supabase_url: str,
    supabase_key: str,
) -> None:
    """Entry point for the sync child process."""
    worker = _SyncWorker(
        db_path=db_path,
        project=project,
        run_name=run_name,
        experiment_name=experiment_name,
        supabase_url=supabase_url,
        supabase_key=supabase_key,
    )
    worker.run()


def start_sync_process(
    db_path: Path,
    project: str,
    run_name: str,
    experiment_name: Optional[str],
    supabase_url: str,
    supabase_key: str,
) -> multiprocessing.Process:
    """Spawn and start the background sync process."""
    ctx = multiprocessing.get_context("spawn")
    p = ctx.Process(
        target=_sync_process_entry,
        args=(str(db_path), project, run_name, experiment_name, supabase_url, supabase_key),
        daemon=True,
        name=f"goodseed-sync-{run_name}",
    )
    p.start()
    return p
