import { ConvexReactClient } from 'convex/react';

import { api } from '@convex/_generated/api';

import { isDirty, useTaskStore, type LocalTask } from '../task-store';
import { currentOwnerId } from './owner';

const DRAIN_DEBOUNCE_MS = 400;
const ERROR_BACKOFF_MS = 5000;

/** Whether background sync is configured at all (a Convex URL is present). */
export function isSyncEnabled(): boolean {
  return !!process.env.EXPO_PUBLIC_CONVEX_URL;
}

/** The wire shape pushed to `pushTasks` — synced fields keyed by `clientId`. No `ownerId`
 *  (the server stamps it from the authenticated owner) and no local-only `syncedAt`. */
type WireTask = {
  clientId: string;
  title: string;
  status: LocalTask['status'];
  resistance: number;
  exposureCount: number;
  createdAt: number;
  updatedAt: number;
  lastDecision?: LocalTask['lastDecision'];
  completedAt?: number;
  deletedAt?: number;
};

function toWire(t: LocalTask): WireTask {
  // Omit undefined optionals — Convex args reject explicit `undefined` values.
  const wire: WireTask = {
    clientId: t.id,
    title: t.title,
    status: t.status,
    resistance: t.resistance,
    exposureCount: t.exposureCount,
    createdAt: t.createdAt,
    updatedAt: t.updatedAt,
  };
  if (t.lastDecision !== undefined) wire.lastDecision = t.lastDecision;
  if (t.completedAt !== undefined) wire.completedAt = t.completedAt;
  if (t.deletedAt !== undefined) wire.deletedAt = t.deletedAt;
  return wire;
}

let client: ConvexReactClient | null = null;
let cleanups: (() => void)[] = [];
let drainTimer: ReturnType<typeof setTimeout> | null = null;
let draining = false;
let firstPull = false;

/**
 * Start the Convex sync engine: mirror the local store to the account's server rows in the
 * background. **No-op when no `EXPO_PUBLIC_CONVEX_URL` is configured** — the app stays fully
 * local then, preserving the offline / no-account guarantee. Imperative (no ConvexProvider)
 * so it lives entirely behind the data layer. Returns a stop function.
 */
export function startConvexSync(): () => void {
  const url = process.env.EXPO_PUBLIC_CONVEX_URL;
  if (!url || client) return stopConvexSync;

  client = new ConvexReactClient(url, { unsavedChangesWarning: false });

  // Claim any un-owned local rows into the current account. In dogfood mode this is the
  // one-time startup migration; under auth it's the sign-in adoption moment.
  useTaskStore.getState().adoptOwner(currentOwnerId());

  // Pull: subscribe to the account's server rows; reconcile them in (server wins on
  // conflict, newer local edits are kept until they push).
  const watch = client.watchQuery(api.sync.getTasks, {});
  const unsubPull = watch.onUpdate(() => {
    const rows = watch.localQueryResult();
    if (rows === undefined) return; // query not loaded yet
    useTaskStore.getState().ingestServerRows(rows);
    if (!firstPull) {
      firstPull = true;
      // First successful pull. If both the account and the store are empty, this is a
      // brand-new account → seed the starters once (they then sync up). An existing
      // account's rows were ingested just above, so we never pollute it with starters.
      if (useTaskStore.getState().tasks.length === 0) useTaskStore.getState().ensureStarterTasks();
    }
  });

  // Push: drain dirty rows whenever the store changes (debounced), and once now.
  const unsubStore = useTaskStore.subscribe(() => scheduleDrain());
  cleanups = [unsubPull, unsubStore];
  scheduleDrain();

  return stopConvexSync;
}

export function stopConvexSync(): void {
  if (drainTimer) clearTimeout(drainTimer);
  drainTimer = null;
  cleanups.forEach((fn) => fn());
  cleanups = [];
  client?.close();
  client = null;
  firstPull = false;
}

function scheduleDrain(delayMs: number = DRAIN_DEBOUNCE_MS) {
  if (drainTimer) clearTimeout(drainTimer);
  drainTimer = setTimeout(() => void drain(), delayMs);
}

async function drain() {
  if (!client || draining) return;
  const dirty = useTaskStore.getState().tasks.filter(isDirty);
  if (dirty.length === 0) return;
  draining = true;
  let pushed = false;
  try {
    // pushTasks returns the authoritative server row per input; ingest confirms our pushes
    // (marks them clean) and picks up any newer version that beat us.
    const applied = await client.mutation(api.sync.pushTasks, { tasks: dirty.map(toWire) });
    useTaskStore.getState().ingestServerRows(applied);
    pushed = true;
  } catch {
    // A *rejected* push (validation, not-yet-deployed, etc.) — back off rather than hot-loop.
    // True offline leaves the mutation pending (not rejected), so it resolves on reconnect.
  } finally {
    draining = false;
    // Re-drain promptly after a successful push that left mid-flight edits; back off on failure.
    if (client && useTaskStore.getState().tasks.some(isDirty)) {
      scheduleDrain(pushed ? DRAIN_DEBOUNCE_MS : ERROR_BACKOFF_MS);
    }
  }
}
