import Dexie, { Table } from "dexie";
import {
  GameSession,
  AppSettings,
  SavedPlayer,
  Location,
  Profile,
  SyncState,
} from "../types";
import { ImportedGame } from "../types/gameImport";
import { generateId } from "../utils/id";

// When the sync engine applies records pulled/acked from the server, it flips
// this flag so the auto-dirty hooks don't re-mark those writes as dirty.
export const remoteApply = { active: false };

// The sync engine registers a (debounced) callback here so any local change to
// a synced table promptly schedules a push. Kept as a hook to avoid a circular
// import between db.ts and syncEngine.ts.
export const localChange = { notify: null as null | (() => void) };

export class BoardScoreDatabase extends Dexie {
  sessions!: Table<GameSession, string>;
  settings!: Table<AppSettings, number>;
  players!: Table<SavedPlayer, string>;
  locations!: Table<Location, string>;
  profiles!: Table<Profile, string>;
  syncState!: Table<SyncState, string>;
  importedGames!: Table<ImportedGame, [string, string]>;

  constructor() {
    super("BoardScoreDatabase");

    this.version(1).stores({
      sessions: "id, gameId, dateStart, status",
      settings: "id",
    });

    // Version 2: Added players table
    this.version(2).stores({
      sessions: "id, gameId, dateStart, status",
      settings: "id",
      players: "id, name, createdAt",
    });

    // Version 3: Added locations table, updatedAt tracking for future sync
    this.version(3)
      .stores({
        sessions: "id, gameId, dateStart, status, locationId, updatedAt",
        settings: "id",
        players: "id, name, createdAt, updatedAt",
        locations: "id, name, createdAt, updatedAt",
      })
      .upgrade(async (tx) => {
        await tx
          .table("sessions")
          .toCollection()
          .modify((session) => {
            session.updatedAt = session.dateEnd ?? session.dateStart;
          });
        await tx
          .table("players")
          .toCollection()
          .modify((player) => {
            player.updatedAt = player.createdAt;
          });
      });

    // Version 4: Added local profiles; scope existing data to a default profile
    this.version(4)
      .stores({
        sessions:
          "id, gameId, dateStart, status, locationId, updatedAt, profileId",
        settings: "id",
        players: "id, name, createdAt, updatedAt, profileId",
        locations: "id, name, createdAt, updatedAt, profileId",
        profiles: "id, name, createdAt, updatedAt",
      })
      .upgrade(async (tx) => {
        const now = Date.now();
        const defaultProfileId = generateId();

        await tx.table("profiles").add({
          id: defaultProfileId,
          name: "Moi",
          createdAt: now,
          updatedAt: now,
        });

        await tx
          .table("sessions")
          .toCollection()
          .modify((session) => {
            session.profileId = defaultProfileId;
          });
        await tx
          .table("players")
          .toCollection()
          .modify((player) => {
            player.profileId = defaultProfileId;
          });
        await tx
          .table("locations")
          .toCollection()
          .modify((location) => {
            location.profileId = defaultProfileId;
          });

        const settings = await tx.table("settings").get(1);
        if (settings) {
          await tx
            .table("settings")
            .update(1, { activeProfileId: defaultProfileId });
        } else {
          await tx.table("settings").add({
            id: 1,
            theme: "system",
            language: "fr",
            activeProfileId: defaultProfileId,
          });
        }
      });

    // Version 5: Sync bookkeeping (dirty flag + sync cursor per profile)
    this.version(5)
      .stores({
        sessions:
          "id, gameId, dateStart, status, locationId, updatedAt, profileId, dirty",
        settings: "id",
        players: "id, name, createdAt, updatedAt, profileId, dirty",
        locations: "id, name, createdAt, updatedAt, profileId, dirty",
        profiles: "id, name, createdAt, updatedAt",
        syncState: "profileId",
      })
      .upgrade(async (tx) => {
        // Mark all pre-existing records dirty so they push on the first sync.
        for (const table of ["sessions", "players", "locations"]) {
          await tx
            .table(table)
            .toCollection()
            .modify((row) => {
              row.dirty = 1;
            });
        }
      });

    // Version 6: imported game definitions (per profile)
    this.version(6).stores({
      sessions:
        "id, gameId, dateStart, status, locationId, updatedAt, profileId, dirty",
      settings: "id",
      players: "id, name, createdAt, updatedAt, profileId, dirty",
      locations: "id, name, createdAt, updatedAt, profileId, dirty",
      profiles: "id, name, createdAt, updatedAt",
      syncState: "profileId",
      importedGames: "[profileId+id], profileId, id, name",
    });
  }
}

export const db = new BoardScoreDatabase();

// ---- Auto-stamp updatedAt + dirty on every local write to synced tables ----
// Skipped while the sync engine is applying server data (remoteApply.active).
for (const table of [db.sessions, db.players, db.locations]) {
  table.hook("creating", (_primKey, obj: any) => {
    if (remoteApply.active) return;
    if (obj.updatedAt === undefined) obj.updatedAt = Date.now();
    if (obj.dirty === undefined) obj.dirty = 1;
    localChange.notify?.();
  });
  table.hook("updating", (modifications: any) => {
    if (remoteApply.active) return;
    // Don't clobber an explicit dirty/updatedAt already in this update.
    const extra: Record<string, unknown> = {};
    if (modifications.updatedAt === undefined) extra.updatedAt = Date.now();
    if (modifications.dirty === undefined) extra.dirty = 1;
    localChange.notify?.();
    return extra;
  });
}

// Initialize default profile + settings on a fresh install
db.on("populate", async () => {
  const now = Date.now();
  const defaultProfileId = generateId();

  await db.profiles.add({
    id: defaultProfileId,
    name: "Moi",
    createdAt: now,
    updatedAt: now,
  });

  await db.settings.add({
    id: 1,
    theme: "system",
    language: "fr",
    activeProfileId: defaultProfileId,
  });
});
