import { db, remoteApply, localChange } from "../database/db";
import { apiJson, getAccessToken } from "../lib/apiClient";
import { useProfileStore } from "../stores/profileStore";
import { GameSession, Location, SavedPlayer } from "../types";

interface PullResponse {
  serverTime: number;
  locations: any[];
  players: any[];
  sessions: any[];
}

interface PushResponse {
  serverTime: number;
  applied: {
    locations: { id: string; updatedAt: number }[];
    players: { id: string; updatedAt: number }[];
    sessions: { id: string; updatedAt: number }[];
  };
}

let syncing = false;
const listeners = new Set<(state: SyncStatus) => void>();

export type SyncStatus = "idle" | "syncing" | "error";
let currentStatus: SyncStatus = "idle";

export function onSyncStatus(cb: (state: SyncStatus) => void): () => void {
  listeners.add(cb);
  cb(currentStatus);
  return () => listeners.delete(cb);
}

function setStatus(s: SyncStatus) {
  currentStatus = s;
  listeners.forEach((cb) => cb(s));
}

// ---- Mapping: server rows -> local records (scoped to the active profile) ----

function toLocation(r: any, profileId: string): Location {
  return {
    id: r.id,
    name: r.name,
    address: r.address ?? undefined,
    createdAt: Number(r.createdAt),
    updatedAt: Number(r.updatedAt),
    deletedAt: r.deletedAt ? Number(r.deletedAt) : undefined,
    profileId,
    dirty: 0,
  };
}

function toPlayer(r: any, profileId: string): SavedPlayer {
  return {
    id: r.id,
    name: r.name,
    avatar: r.avatar ?? undefined,
    createdAt: Number(r.createdAt),
    updatedAt: Number(r.updatedAt),
    deletedAt: r.deletedAt ? Number(r.deletedAt) : undefined,
    profileId,
    dirty: 0,
  };
}

function toSession(r: any, profileId: string): GameSession {
  return {
    id: r.id,
    gameId: r.gameId,
    dateStart: Number(r.dateStart),
    dateEnd: r.dateEnd ? Number(r.dateEnd) : undefined,
    players: r.players ?? [],
    rounds: r.rounds ?? [],
    status: r.status,
    options: r.options ?? {},
    winnerIds: r.winnerIds ?? undefined,
    locationId: r.locationId ?? undefined,
    updatedAt: Number(r.updatedAt),
    deletedAt: r.deletedAt ? Number(r.deletedAt) : undefined,
    profileId,
    dirty: 0,
  };
}

// ---- Push local (dirty) records, then clear their dirty flag on ack ----

async function pushDirty(profileId: string): Promise<void> {
  const [locations, players, sessions] = await Promise.all([
    db.locations
      .where("profileId")
      .equals(profileId)
      .and((r) => r.dirty === 1)
      .toArray(),
    db.players
      .where("profileId")
      .equals(profileId)
      .and((r) => r.dirty === 1)
      .toArray(),
    db.sessions
      .where("profileId")
      .equals(profileId)
      .and((r) => r.dirty === 1)
      .toArray(),
  ]);

  if (
    locations.length === 0 &&
    players.length === 0 &&
    sessions.length === 0
  ) {
    return;
  }

  const res = await apiJson<PushResponse>("/sync", {
    method: "POST",
    body: JSON.stringify({ locations, players, sessions }),
  });

  remoteApply.active = true;
  try {
    await db.transaction(
      "rw",
      db.locations,
      db.players,
      db.sessions,
      async () => {
        for (const a of res.applied.locations)
          await db.locations.update(a.id, { updatedAt: a.updatedAt, dirty: 0 });
        for (const a of res.applied.players)
          await db.players.update(a.id, { updatedAt: a.updatedAt, dirty: 0 });
        for (const a of res.applied.sessions)
          await db.sessions.update(a.id, { updatedAt: a.updatedAt, dirty: 0 });
      },
    );
  } finally {
    remoteApply.active = false;
  }
}

// ---- Pull remote changes since the stored cursor and apply them locally ----

async function pullSince(profileId: string): Promise<void> {
  const state = await db.syncState.get(profileId);
  const since = state?.lastSyncedAt ?? 0;

  const res = await apiJson<PullResponse>(`/sync?since=${since}`, {
    method: "GET",
  });

  remoteApply.active = true;
  try {
    await db.transaction(
      "rw",
      db.locations,
      db.players,
      db.sessions,
      db.syncState,
      async () => {
        for (const r of res.locations)
          await db.locations.put(toLocation(r, profileId));
        for (const r of res.players)
          await db.players.put(toPlayer(r, profileId));
        for (const r of res.sessions)
          await db.sessions.put(toSession(r, profileId));
        await db.syncState.put({ profileId, lastSyncedAt: res.serverTime });
      },
    );
  } finally {
    remoteApply.active = false;
  }
}

// ---- Public entry point ----

export async function runSync(): Promise<void> {
  if (syncing) return;
  if (!getAccessToken()) return; // not logged in
  const profileId = useProfileStore.getState().activeProfileId;
  if (!profileId) return;

  syncing = true;
  setStatus("syncing");
  try {
    await pushDirty(profileId);
    await pullSince(profileId);
    setStatus("idle");
  } catch (err) {
    console.error("[sync] failed", err);
    setStatus("error");
  } finally {
    syncing = false;
  }
}

// ---- Triggers: reconnection, tab focus, and a periodic heartbeat ----

let started = false;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;

// Push shortly after any local change (coalesces bursts of edits).
function scheduleSync() {
  if (debounceTimer) clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => void runSync(), 1500);
}

export function startSyncTriggers() {
  if (started) return;
  started = true;

  localChange.notify = scheduleSync;
  window.addEventListener("online", () => void runSync());
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") void runSync();
  });
  setInterval(() => void runSync(), 60_000);
}
