import Dexie, { type EntityTable } from 'dexie';
import type { Campaign } from '@/lib/schemas/campaign';
import type { Character } from '@/lib/schemas/character';
import type { Encounter } from '@/lib/schemas/encounter';
import type { DiceRoll } from '@/lib/schemas/dice';
import type { Note, Npc, Quest, Calendar, BattleMap, Homebrew } from '@/lib/schemas/world';
import type { SessionLogEntry } from '@/lib/schemas/sessionLog';
import { characterDefaults } from '@/lib/schemas/character';

/**
 * Single Dexie instance for the whole app. Dexie wraps IndexedDB transactions
 * correctly (commits before resolving), which removes the silent-data-loss
 * class of bugs the old hand-rolled adapter shipped with.
 *
 * Schema versions are additive: bump `version(n)` and add a `.upgrade()` when
 * the shape changes — never edit a past version in place.
 */
export class TtrpgDatabase extends Dexie {
  campaigns!: EntityTable<Campaign, 'id'>;
  characters!: EntityTable<Character, 'id'>;
  encounters!: EntityTable<Encounter, 'id'>;
  diceRolls!: EntityTable<DiceRoll, 'id'>;
  notes!: EntityTable<Note, 'id'>;
  npcs!: EntityTable<Npc, 'id'>;
  quests!: EntityTable<Quest, 'id'>;
  calendars!: EntityTable<Calendar, 'campaignId'>;
  maps!: EntityTable<BattleMap, 'id'>;
  homebrew!: EntityTable<Homebrew, 'id'>;
  sessionLog!: EntityTable<SessionLogEntry, 'id'>;

  constructor() {
    super('ttrpg-manager');
    this.version(1).stores({
      campaigns: 'id, updatedAt',
      characters: 'id, campaignId, kind',
      encounters: 'id, campaignId, status',
      diceRolls: 'id, campaignId, createdAt',
    });

    // v2 — Phase 1 character depth: backfill new fields on existing characters.
    this.version(2).stores({}).upgrade(async (tx) => {
      await tx
        .table('characters')
        .toCollection()
        .modify((c: Record<string, unknown>) => {
          for (const [key, value] of Object.entries(characterDefaults())) {
            if (c[key] === undefined) c[key] = value;
          }
        });
    });

    // v3 — Phase 3 combat depth: backfill the encounter event log.
    this.version(3).stores({}).upgrade(async (tx) => {
      await tx
        .table('encounters')
        .toCollection()
        .modify((e: Record<string, unknown>) => {
          if (e.log === undefined) e.log = [];
        });
    });

    // v4 — Phase 5 worldbuilding: notes, NPCs, quests, calendar.
    this.version(4).stores({
      notes: 'id, campaignId',
      npcs: 'id, campaignId',
      quests: 'id, campaignId, status',
      calendars: 'campaignId',
    });

    // v5 — Phase 7 maps/VTT.
    this.version(5).stores({ maps: 'id, campaignId' });

    // v6 — Phase 9 homebrew content.
    this.version(6).stores({ homebrew: 'id, campaignId, [campaignId+kind]' });

    // v7 — Phase 13 assistant: encounters gain optional partyLevelsSnapshot +
    // outcome. Both optional, so existing rows stay valid; no backfill needed.
    this.version(7).stores({});

    // v8 — Phase 17 VTT overhaul: maps gain drawings/gridUnit/gridType/fogVersion;
    // tokens gain size/kind/conditions/gmOnly. All defaulted, so backfill is
    // belt-and-suspenders (repo re-parses on save too).
    this.version(8).stores({}).upgrade(async (tx) => {
      await tx
        .table('maps')
        .toCollection()
        .modify((m: Record<string, unknown>) => {
          if (m.drawings === undefined) m.drawings = [];
          if (m.gridUnit === undefined) m.gridUnit = { feet: 5 };
          if (m.gridType === undefined) m.gridType = 'square';
          if (m.fogVersion === undefined) m.fogVersion = 1;
          const tokens = Array.isArray(m.tokens) ? (m.tokens as Record<string, unknown>[]) : [];
          for (const t of tokens) {
            if (t.size === undefined) t.size = 1;
            if (t.kind === undefined) t.kind = 'npc';
            if (t.conditions === undefined) t.conditions = [];
            if (t.gmOnly === undefined) t.gmOnly = false;
          }
        });
    });

    // v9 — Phase 20: tokens gain optional combatantId (live HP from an encounter
    // combatant). Optional field — no backfill needed; existing rows parse fine.
    this.version(9).stores({});

    // v10 — Phase 2: characters gain `portrait`, map tokens gain `image`
    // (both optional data URLs). No backfill needed.
    this.version(10).stores({});

    // v11 — Phase 3: maps gain walls/doors/lights (UVTT import + dynamic vision).
    // Backfill empty arrays so code can read them on pre-existing maps.
    this.version(11).stores({}).upgrade(async (tx) => {
      await tx.table('maps').toCollection().modify((m: Record<string, unknown>) => {
        if (m.walls === undefined) m.walls = [];
        if (m.doors === undefined) m.doors = [];
        if (m.lights === undefined) m.lights = [];
      });
    });

    // v12 — Phase 4: maps gain dynamicVision + sightRadiusFeet.
    this.version(12).stores({}).upgrade(async (tx) => {
      await tx.table('maps').toCollection().modify((m: Record<string, unknown>) => {
        if (m.dynamicVision === undefined) m.dynamicVision = false;
        if (m.sightRadiusFeet === undefined) m.sightRadiusFeet = 60;
      });
    });

    // v13 — P14b: persisted live-session recap (rolls + chat) per campaign.
    this.version(13).stores({ sessionLog: 'id, campaignId, ts' });

    // v14 — V2 rules kernel: characters gain a top-level `concentration` slot and
    // each spell entry gains snapshotted mechanics (concentration/save/damage).
    // All defaulted, so this backfill is belt-and-suspenders (repos re-parse on save).
    this.version(14).stores({}).upgrade(async (tx) => {
      await tx.table('characters').toCollection().modify((c: Record<string, unknown>) => {
        if (c.concentration === undefined) c.concentration = null;
        if (c.equippedArmor === undefined) c.equippedArmor = null;
        const sc = c.spellcasting as { spells?: Record<string, unknown>[] } | undefined;
        for (const s of sc?.spells ?? []) {
          if (s.concentration === undefined) s.concentration = false;
          if (s.save === undefined) s.save = null;
          if (s.damage === undefined) s.damage = [];
        }
      });
    });
  }
}

export const db = new TtrpgDatabase();
