"use client";

/**
 * Dexie 数据库定义
 *
 * `messages.sessionId` 是当前聊天身份的缓存命名空间。所有消息读写必须通过
 * 该索引限定身份，不能退化为整表操作。
 *
 * 构造时 `dbName` 可注入，便于测试时每个用例用独立 DB 互不污染。
 */

import Dexie, { type Table, type Transaction } from "dexie";
import type { ChatMediaKind } from "@/data/schemas/chat";
import type {
  ChatImageData,
  ChatLockDetailData,
} from "@/data/schemas/chat";
import {
  buildChatMediaCacheKey,
  migrateLegacyChatConversationKey,
} from "@/lib/chat/chat_cache_keys";

export interface LocalMessageRow {
  /** Dexie 自增主键（数据库内部使用，不暴露给 LocalMessage 类）。 */
  dbId?: number;
  /** 消息 id（来自 ChatMessage.id）。 */
  id: string;
  role: string;
  type: string;
  content: string;
  createdAt: string;
  audioUrl?: string | null;
  image?: ChatImageData;
  lockDetail?: ChatLockDetailData;
  sessionId: string;
}

export interface LocalChatMediaRow {
  cacheKey: string;
  ownerKey: string;
  messageId: string;
  kind: ChatMediaKind;
  remoteUrl: string;
  /** v3+ stores raw bytes instead of Blob to avoid iOS WebKit Blob URL issues. */
  bytes?: ArrayBuffer;
  /** Legacy v2 field. Read-only fallback for users who already have cached media. */
  blob?: Blob;
  mimeType: string;
  byteSize: number;
  createdAt: number;
  updatedAt: number;
  lastAccessedAt: number;
}

export class LocalChatDB extends Dexie {
  messages!: Table<LocalMessageRow, number>;
  media!: Table<LocalChatMediaRow, string>;

  constructor(dbName: string = "cozsweet-chat") {
    super(dbName);
    this.version(1).stores({
      messages: "++dbId",
    });
    this.version(2).stores({
      messages: "++dbId",
      media: "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
    });
    this.version(3)
      .stores({
        messages: "++dbId, sessionId",
        media: "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
      })
      .upgrade(async (transaction) => {
        // v1/v2 always wrote an empty sessionId. Its owner cannot be recovered,
        // so retaining it would expose one identity's history to another.
        await transaction.table("messages").clear();
      });
    this.version(4)
      .stores({
        messages: "++dbId, sessionId",
        media: "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
      });
    this.version(5)
      .stores({
        messages: "++dbId, sessionId",
        media: "cacheKey, ownerKey, messageId, kind, remoteUrl, updatedAt, lastAccessedAt",
      })
      .upgrade(migrateLegacyChatCache);
  }
}

async function migrateLegacyChatCache(
  transaction: Transaction,
): Promise<void> {
  const messages = transaction.table<LocalMessageRow, number>("messages");
  await messages.toCollection().modify((message) => {
    message.sessionId = migrateLegacyChatConversationKey(message.sessionId);
  });

  const media = transaction.table<LocalChatMediaRow, string>("media");
  const rows = await media.toArray();
  const canonicalCacheKeys = new Set(
    rows.flatMap((row) => {
      const ownerKey = migrateLegacyChatConversationKey(row.ownerKey);
      const cacheKey = buildChatMediaCacheKey({
        ownerKey,
        messageId: row.messageId,
        kind: row.kind,
      });
      return ownerKey === row.ownerKey && cacheKey === row.cacheKey
        ? [cacheKey]
        : [];
    }),
  );

  for (const row of rows) {
    const ownerKey = migrateLegacyChatConversationKey(row.ownerKey);
    const cacheKey = buildChatMediaCacheKey({
      ownerKey,
      messageId: row.messageId,
      kind: row.kind,
    });
    if (ownerKey === row.ownerKey && cacheKey === row.cacheKey) continue;

    const existing = await media.get(cacheKey);
    await media.delete(row.cacheKey);
    if (existing && canonicalCacheKeys.has(cacheKey)) continue;
    if (existing && !isNewerMediaRow(row, existing)) continue;

    await media.put({
      ...row,
      cacheKey,
      ownerKey,
    });
  }
}

function isNewerMediaRow(
  candidate: LocalChatMediaRow,
  existing: LocalChatMediaRow,
): boolean {
  return (
    candidate.updatedAt > existing.updatedAt ||
    (candidate.updatedAt === existing.updatedAt &&
      candidate.lastAccessedAt > existing.lastAccessedAt) ||
    (candidate.updatedAt === existing.updatedAt &&
      candidate.lastAccessedAt === existing.lastAccessedAt &&
      candidate.createdAt > existing.createdAt)
  );
}
