import Dexie, { type EntityTable } from "dexie";
import type { ConversionStatus, CurrencyCode, LedgerEntry, SyncOperation } from "@cents/domain";
import type { LedgerRecord, UserPreference } from "./ledgers";

export type SyncMetadata = {
  id: string;
  cursor: string;
  updatedAt: string;
};

export type LocalExchangeRate = {
  id: string;
  currency: CurrencyCode;
  requestedDate: string;
  effectiveDate: string;
  cnyPerUnit: string;
  status: Exclude<ConversionStatus, "pending">;
  updatedAt: string;
};

export type CentsDatabase = Dexie & {
  entries: EntityTable<LedgerEntry, "id">;
  syncOperations: EntityTable<SyncOperation, "id">;
  ledgers: EntityTable<LedgerRecord, "id">;
  userPreferences: EntityTable<UserPreference, "id">;
  syncMetadata: EntityTable<SyncMetadata, "id">;
  exchangeRates: EntityTable<LocalExchangeRate, "id">;
};

function configureDatabase(db: CentsDatabase) {
db.version(1).stores({
  entries: "id, ledgerId, occurredAt, updatedAt, deletedAt",
  syncOperations: "id, ledgerId, createdAt, syncedAt",
});

db.version(2).stores({
  entries: "id, ledgerId, occurredAt, updatedAt, deletedAt",
  syncOperations: "id, ledgerId, createdAt, syncedAt",
  ledgers: "id, updatedAt, archivedAt",
  userPreferences: "id, userId, key, updatedAt",
});

db.version(3).stores({
  entries: "id, ledgerId, occurredAt, updatedAt, deletedAt",
  syncOperations: "id, ledgerId, createdAt, syncedAt",
  ledgers: "id, updatedAt, archivedAt",
  userPreferences: "id, userId, key, updatedAt",
  syncMetadata: "id, updatedAt",
});

type LegacyEntry = LedgerEntry & { ledgerId?: string; ownerId?: string; ledgerIds?: string[] };
type LegacyOperation = SyncOperation & { ledgerId?: string; ledgerIds?: string[]; payload: LegacyEntry };

db.version(4).stores({
  entries: "id, ownerId, *ledgerIds, occurredAt, updatedAt, deletedAt",
  syncOperations: "id, *ledgerIds, createdAt, syncedAt",
  ledgers: "id, updatedAt, archivedAt",
  userPreferences: "id, userId, key, updatedAt",
  syncMetadata: "id, updatedAt",
}).upgrade(async (transaction) => {
  await transaction.table<LegacyEntry>("entries").toCollection().modify((entry) => {
    entry.ownerId ??= entry.createdBy;
    entry.ledgerIds = entry.ledgerIds?.length ? [...new Set(entry.ledgerIds)] : entry.ledgerId ? [entry.ledgerId] : [];
    delete entry.ledgerId;
  });
  await transaction.table<LegacyOperation>("syncOperations").toCollection().modify((operation) => {
    operation.payload.ownerId ??= operation.payload.createdBy;
    operation.payload.ledgerIds = operation.payload.ledgerIds?.length
      ? [...new Set(operation.payload.ledgerIds)]
      : operation.payload.ledgerId ? [operation.payload.ledgerId] : [];
    operation.ledgerIds = operation.ledgerIds?.length
      ? [...new Set(operation.ledgerIds)]
      : operation.ledgerId ? [operation.ledgerId] : operation.payload.ledgerIds;
    delete operation.payload.ledgerId;
    delete operation.ledgerId;
  });
});

db.version(5).stores({
  entries: "id, ownerId, *ledgerIds, occurredAt, updatedAt, deletedAt, conversionStatus",
  syncOperations: "id, *ledgerIds, createdAt, syncedAt",
  ledgers: "id, updatedAt, archivedAt",
  userPreferences: "id, userId, key, updatedAt",
  syncMetadata: "id, updatedAt",
  exchangeRates: "id, currency, requestedDate, updatedAt",
}).upgrade(async (transaction) => {
  const normalizeEntry = (entry: LegacyEntry) => {
    const date = localDate(entry.occurredAt);
    entry.exchangeRateDate ??= date;
    if (entry.currency === "CNY") {
      entry.baseCurrency = "CNY";
      entry.baseAmount = entry.amount;
      entry.exchangeRate = "1";
      entry.exchangeRateEffectiveDate = date;
      entry.exchangeRateSource = "system";
      entry.conversionStatus = "exact";
    } else if (!entry.conversionStatus || (entry.exchangeRate === "1" && entry.exchangeRateSource === "manual")) {
      entry.baseCurrency = "CNY";
      entry.baseAmount = null;
      entry.exchangeRate = null;
      entry.exchangeRateEffectiveDate = null;
      entry.exchangeRateSource = "system";
      entry.conversionStatus = "pending";
    }
  };
  await transaction.table<LegacyEntry>("entries").toCollection().modify(normalizeEntry);
  await transaction.table<LegacyOperation>("syncOperations").toCollection().modify((operation) => {
    normalizeEntry(operation.payload);
  });
});

db.version(6).stores({
  entries: "id, ownerId, *ledgerIds, reimbursementOfEntryId, occurredAt, updatedAt, deletedAt, conversionStatus",
  syncOperations: "id, *ledgerIds, createdAt, syncedAt",
  ledgers: "id, updatedAt, archivedAt",
  userPreferences: "id, userId, key, updatedAt",
  syncMetadata: "id, updatedAt",
  exchangeRates: "id, currency, requestedDate, updatedAt",
}).upgrade(async (transaction) => {
  await transaction.table<LegacyEntry>("entries").toCollection().modify((entry) => {
    entry.reimbursementOfEntryId ??= null;
  });
  await transaction.table<LegacyOperation>("syncOperations").toCollection().modify((operation) => {
    operation.payload.reimbursementOfEntryId ??= null;
  });
});
}

const databaseCache = new Map<string, CentsDatabase>();
const databaseOpeners = new Map<string, Promise<CentsDatabase>>();

export async function getUserDb(userId: string) {
  const name = `cents-user-${userId}`;
  const cached = databaseCache.get(name);
  if (cached) return cached;
  const opening = databaseOpeners.get(name);
  if (opening) return opening;
  const promise = (async () => {
    const database = new Dexie(name) as CentsDatabase;
    configureDatabase(database);
    await database.open();
    await migrateLegacyUserData(database, userId);
    databaseCache.set(name, database);
    return database;
  })();
  databaseOpeners.set(name, promise);
  try {
    return await promise;
  } finally {
    databaseOpeners.delete(name);
  }
}

export function closeUserDb(userId: string) {
  const name = `cents-user-${userId}`;
  const database = databaseCache.get(name);
  database?.close();
  databaseCache.delete(name);
}

async function migrateLegacyUserData(database: CentsDatabase, userId: string) {
  const markerId = `${userId}:legacy-migration-v1`;
  if (await database.userPreferences.get(markerId)) return;
  const legacy = new Dexie("cents") as CentsDatabase;
  configureDatabase(legacy);
  try {
    await legacy.open();
    const [entries, operations, preferences, exchangeRates] = await Promise.all([
      legacy.entries.filter((entry) => entry.ownerId === userId).toArray(),
      legacy.syncOperations.filter((operation) => (operation.userId ?? operation.payload.updatedBy) === userId).toArray(),
      legacy.userPreferences.filter((preference) => preference.userId === userId).toArray(),
      legacy.exchangeRates.toArray(),
    ]);
    const operationEntries = await Promise.all(
      operations.map((operation) => legacy.entries.get(operation.entityId)),
    );
    const mergedEntries = new Map(entries.map((entry) => [entry.id, entry]));
    for (const entry of operationEntries) {
      if (entry) mergedEntries.set(entry.id, entry);
    }
    await database.transaction("rw", database.entries, database.syncOperations, database.userPreferences, database.exchangeRates, async () => {
      if (mergedEntries.size) await database.entries.bulkPut([...mergedEntries.values()]);
      if (operations.length) await database.syncOperations.bulkPut(operations);
      if (preferences.length) await database.userPreferences.bulkPut(preferences);
      if (exchangeRates.length) await database.exchangeRates.bulkPut(exchangeRates);
      await database.userPreferences.put({
        id: markerId,
        userId,
        key: "legacyMigration",
        value: "v1",
        updatedAt: new Date().toISOString(),
      });
    });
  } finally {
    legacy.close();
  }
}

function localDate(value: string) {
  const date = new Date(value);
  const pad = (part: number) => String(part).padStart(2, "0");
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
