import type { OcrBlock } from "./layout.ts";

export interface OcrCheckpoint {
  bookId: string;
  sourceFingerprint: string;
  pageNumber: number;
  width: number;
  height: number;
  blocks: OcrBlock[];
  completedAt: string;
}

interface KeyValueBackend {
  get(key: string): Promise<unknown>;
  put(value: unknown, key: string): Promise<unknown>;
  delete(key: string): Promise<unknown>;
  getAllKeys(): Promise<unknown[]>;
}

function keyFor(bookId: string, fingerprint: string, pageNumber: number): string {
  return `${encodeURIComponent(bookId)}::${encodeURIComponent(fingerprint)}::${pageNumber}`;
}

function keyPrefix(bookId: string, fingerprint?: string): string {
  const bookPrefix = `${encodeURIComponent(bookId)}::`;
  return fingerprint === undefined
    ? bookPrefix
    : `${bookPrefix}${encodeURIComponent(fingerprint)}::`;
}

function isCheckpoint(value: unknown): value is OcrCheckpoint {
  return Boolean(
    value &&
      typeof value === "object" &&
      "bookId" in value &&
      typeof value.bookId === "string" &&
      "pageNumber" in value &&
      Number.isInteger(value.pageNumber),
  );
}

export class OcrCheckpointStore {
  private readonly backend: KeyValueBackend;

  constructor(backend: KeyValueBackend) {
    this.backend = backend;
  }

  async save(checkpoint: OcrCheckpoint): Promise<void> {
    if (!Number.isInteger(checkpoint.pageNumber) || checkpoint.pageNumber < 1) {
      throw new Error("OCR 页码必须是正整数。");
    }
    await this.backend.put(
      checkpoint,
      keyFor(checkpoint.bookId, checkpoint.sourceFingerprint, checkpoint.pageNumber),
    );
  }

  async get(
    bookId: string,
    sourceFingerprint: string,
    pageNumber: number,
  ): Promise<OcrCheckpoint | null> {
    const value = await this.backend.get(keyFor(bookId, sourceFingerprint, pageNumber));
    return isCheckpoint(value) ? value : null;
  }

  async list(bookId: string, sourceFingerprint: string): Promise<OcrCheckpoint[]> {
    const prefix = keyPrefix(bookId, sourceFingerprint);
    const keys = (await this.backend.getAllKeys())
      .filter((key): key is string => typeof key === "string" && key.startsWith(prefix));
    const checkpoints = await Promise.all(keys.map((key) => this.backend.get(key)));
    return checkpoints
      .filter(isCheckpoint)
      .sort((left, right) => left.pageNumber - right.pageNumber);
  }

  async clearPage(
    bookId: string,
    sourceFingerprint: string,
    pageNumber: number,
  ): Promise<void> {
    await this.backend.delete(keyFor(bookId, sourceFingerprint, pageNumber));
  }

  async clearBook(bookId: string): Promise<void> {
    const prefix = keyPrefix(bookId);
    const keys = (await this.backend.getAllKeys())
      .filter((key): key is string => typeof key === "string" && key.startsWith(prefix));
    await Promise.all(keys.map((key) => this.backend.delete(key)));
  }
}

export async function createBrowserOcrCheckpointStore(): Promise<OcrCheckpointStore> {
  const { openDB } = await import("idb");
  const database = await openDB("hearbooks-ocr", 1, {
    upgrade(db) {
      if (!db.objectStoreNames.contains("pages")) db.createObjectStore("pages");
    },
  });
  return new OcrCheckpointStore({
    get: (key) => database.get("pages", key),
    put: (value, key) => database.put("pages", value, key),
    delete: (key) => database.delete("pages", key),
    getAllKeys: () => database.getAllKeys("pages"),
  });
}
