import type { ConversationMessage, ConversationRecord } from "../types";
import { slugifyTitle } from "../utils/time";
import { imageDB } from "../utils/imageDB";

const STORAGE_KEY = "kisera_cinema_conversations_v1";
const CONVERSATION_STORE_DB_NAME = "kisera_cottage_conversation_store_v1";
const CONVERSATION_STORE_NAME = "kv";
const CONVERSATION_STORE_LIST_ID = "conversations";
const CONVERSATION_UPDATE_EVENT = "kisera-cottage-conversations-updated";
const MAX_STORED_CONVERSATIONS = 120;

export type ConversationImportConflictMode = "merge" | "copy";

export interface ConversationImportInspection {
  total: number;
  identical: number;
  divergentSameId: number;
  reusableEquivalent: number;
  newRecords: number;
}

export interface ConversationImportReport {
  added: number;
  merged: number;
  copied: number;
  reusedIdentical: number;
  idMap: Record<string, string>;
}

function now() {
  return new Date().toISOString();
}

function createId(prefix = "chat") {
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}

function isQuotaExceeded(error: unknown) {
  const event = error as { name?: string; message?: string };
  return event?.name === "QuotaExceededError" || String(event?.message || "").toLowerCase().includes("quota");
}

function toIso(value: unknown, fallback = Date.now()): string {
  const time = typeof value === "number"
    ? value < 1_000_000_000_000 ? value * 1000 : value
    : typeof value === "string"
      ? new Date(value).getTime()
      : fallback;
  return new Date(Number.isFinite(time) && time > 0 ? time : fallback).toISOString();
}

function normalizeRole(role: unknown): ConversationMessage["role"] {
  const value = String(role || "").trim().toLowerCase();
  if (value === "user" || value === "human") return "user";
  return "companion";
}

function dataUrlSize(dataUrl: string) {
  const base64 = dataUrl.split(",")[1] || "";
  return Math.max(0, Math.round((base64.length * 3) / 4));
}

function isImageDbRef(value = "") {
  return value.startsWith("indexeddb://");
}

function normalizeAttachment(raw: any, index: number): NonNullable<ConversationMessage["attachments"]>[number] | null {
  if (!raw || typeof raw !== "object") return null;
  const rawType = String(raw.type || "").toLowerCase();
  const dataUrl = typeof raw.dataUrl === "string"
    ? raw.dataUrl
    : typeof raw.content === "string" && (raw.content.startsWith("data:") || isImageDbRef(raw.content))
      ? raw.content
      : "";
  const text = typeof raw.text === "string"
    ? raw.text
    : typeof raw.content === "string" && !raw.content.startsWith("data:") && !raw.content.startsWith("indexeddb://")
      ? raw.content
      : "";

  if (rawType === "image" || dataUrl.startsWith("data:image")) {
    const mimeType = dataUrl.match(/^data:([^;,]+);/)?.[1] || String(raw.mimeType || "image/jpeg");
    return {
      id: String(raw.id || `import-image-${Date.now()}-${index}`),
      type: "image",
      name: String(raw.name || raw.metadata?.source || `image-${index + 1}.jpg`),
      mimeType,
      size: Number(raw.size) || (dataUrl ? dataUrlSize(dataUrl) : 0),
      dataUrl: dataUrl || undefined,
      text: typeof raw.text === "string" ? raw.text : undefined,
    };
  }

  if (rawType === "file" || text.trim()) {
    return {
      id: String(raw.id || `import-file-${Date.now()}-${index}`),
      type: "file",
      name: String(raw.name || `file-${index + 1}.txt`),
      mimeType: String(raw.mimeType || "text/plain"),
      size: Number(raw.size) || text.length,
      text,
    };
  }

  return null;
}

function normalizeConversationMessage(raw: any, index: number): ConversationMessage | null {
  if (!raw || typeof raw !== "object") return null;
  const text = String(raw.text ?? raw.content ?? "").trim();
  const attachments = Array.isArray(raw.attachments)
    ? raw.attachments.map((attachment: any, attachmentIndex: number) => normalizeAttachment(attachment, attachmentIndex)).filter(Boolean)
    : [];
  if (!text && attachments.length === 0) return null;
  const timestamp = raw.createdAt ?? raw.timestamp ?? raw.time;
  return {
    id: String(raw.id || `import-message-${Date.now()}-${index}`),
    role: normalizeRole(raw.role),
    text,
    createdAt: toIso(timestamp, Date.now() + index),
    attachments: attachments.length ? attachments as ConversationMessage["attachments"] : undefined,
    modelUsed: typeof raw.modelUsed === "string" ? raw.modelUsed : undefined,
    tokenCount: typeof raw.tokenCount === "number" ? raw.tokenCount : undefined,
    thoughts: typeof raw.thoughts === "string" ? raw.thoughts : undefined,
    thoughtsTranslated: typeof raw.thoughtsTranslated === "string" ? raw.thoughtsTranslated : undefined,
  };
}

export function normalizeConversationRecord(raw: any, index = 0): ConversationRecord | null {
  if (!raw || typeof raw !== "object") return null;
  const messages = Array.isArray(raw.messages)
    ? raw.messages.map((message: any, messageIndex: number) => normalizeConversationMessage(message, messageIndex)).filter(Boolean)
    : [];
  const createdAt = toIso(raw.createdAt ?? raw.startTime, Date.now() + index);
  const updatedAt = toIso(raw.updatedAt ?? raw.lastModified ?? raw.endTime ?? raw.createdAt, Date.now() + index);
  const linkedWatchTitle = typeof raw.linkedWatchTitle === "string"
    ? raw.linkedWatchTitle
    : typeof raw.watchTitle === "string"
      ? raw.watchTitle
      : undefined;
  const linkedWatchRecordId = typeof raw.linkedWatchRecordId === "string"
    ? raw.linkedWatchRecordId
    : typeof raw.watchRecordId === "string"
      ? raw.watchRecordId
      : undefined;

  return {
    id: String(raw.id || `import-session-${Date.now()}-${index}`),
    title: String(raw.title || raw.name || `导入窗口 ${index + 1}`),
    createdAt,
    updatedAt,
    messages: messages as ConversationRecord["messages"],
    linkedWatchTitle,
    linkedWatchRecordId,
  };
}

function normalizeConversationList(conversations: ConversationRecord[]): ConversationRecord[] {
  return conversations
    .map((conversation, index) => normalizeConversationRecord(conversation, index))
    .filter(Boolean)
    .slice(0, MAX_STORED_CONVERSATIONS) as ConversationRecord[];
}

async function prepareAttachmentForStorage(attachment: NonNullable<ConversationMessage["attachments"]>[number]) {
  if (attachment.type === "image" && attachment.dataUrl?.startsWith("data:image")) {
    const id = await imageDB.saveImage(attachment.dataUrl);
    return {
      ...attachment,
      dataUrl: `indexeddb://${id}`,
    };
  }
  return attachment;
}

async function prepareConversationForStorage(conversation: ConversationRecord): Promise<ConversationRecord> {
  return {
    ...conversation,
    messages: await Promise.all(conversation.messages.map(async (message) => ({
      ...message,
      attachments: message.attachments?.length
        ? await Promise.all(message.attachments.map(prepareAttachmentForStorage))
        : undefined,
    }))),
  };
}

async function hydrateAttachment(attachment: NonNullable<ConversationMessage["attachments"]>[number]) {
  if (attachment.type !== "image" || !attachment.dataUrl || !isImageDbRef(attachment.dataUrl)) return attachment;
  const id = attachment.dataUrl.replace("indexeddb://", "");
  const dataUrl = await imageDB.getImage(id);
  return dataUrl ? { ...attachment, dataUrl } : attachment;
}

async function hydrateConversation(conversation: ConversationRecord): Promise<ConversationRecord> {
  return {
    ...conversation,
    messages: await Promise.all(conversation.messages.map(async (message) => ({
      ...message,
      attachments: message.attachments?.length
        ? await Promise.all(message.attachments.map(hydrateAttachment))
        : undefined,
    }))),
  };
}

export async function prepareConversationRecordsForStorage(conversations: ConversationRecord[]): Promise<ConversationRecord[]> {
  const normalized = normalizeConversationList(conversations);
  return Promise.all(normalized.map(prepareConversationForStorage));
}

export async function hydrateConversationRecords(conversations: ConversationRecord[]): Promise<ConversationRecord[]> {
  return Promise.all(normalizeConversationList(conversations).map(hydrateConversation));
}

function readLegacyConversations(): ConversationRecord[] {
  try {
    if (typeof localStorage === "undefined") return [];
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? normalizeConversationList(parsed as ConversationRecord[]) : [];
  } catch {
    return [];
  }
}

let conversationCache: ConversationRecord[] = readLegacyConversations();
let persistentConversationStoreLoaded = false;
let persistentConversationStoreLoading: Promise<void> | null = null;
let conversationStoreOpenPromise: Promise<IDBDatabase | null> | null = null;
let conversationStorePersistRunning = false;
let conversationStorePersistQueued = false;
let conversationStoreWriteSeq = 0;

function canUseConversationIndexedDB(): boolean {
  return typeof indexedDB !== "undefined";
}

function dispatchConversationUpdate(): void {
  if (typeof window !== "undefined") {
    window.dispatchEvent(new CustomEvent(CONVERSATION_UPDATE_EVENT));
  }
}

function openConversationStore(): Promise<IDBDatabase | null> {
  if (!canUseConversationIndexedDB()) return Promise.resolve(null);
  if (conversationStoreOpenPromise) return conversationStoreOpenPromise;
  conversationStoreOpenPromise = new Promise((resolve) => {
    try {
      const request = indexedDB.open(CONVERSATION_STORE_DB_NAME, 1);
      request.onupgradeneeded = () => {
        const db = request.result;
        if (!db.objectStoreNames.contains(CONVERSATION_STORE_NAME)) {
          db.createObjectStore(CONVERSATION_STORE_NAME);
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => {
        conversationStoreOpenPromise = null;
        resolve(null);
      };
      request.onblocked = () => resolve(null);
    } catch {
      conversationStoreOpenPromise = null;
      resolve(null);
    }
  });
  return conversationStoreOpenPromise;
}

async function conversationIdbGet<T>(key: string): Promise<T | null> {
  const db = await openConversationStore();
  if (!db) return null;
  return new Promise((resolve) => {
    try {
      const transaction = db.transaction(CONVERSATION_STORE_NAME, "readonly");
      const request = transaction.objectStore(CONVERSATION_STORE_NAME).get(key);
      request.onsuccess = () => resolve((request.result as T) ?? null);
      request.onerror = () => resolve(null);
      transaction.onabort = () => resolve(null);
    } catch {
      resolve(null);
    }
  });
}

async function conversationIdbSet(key: string, value: unknown): Promise<boolean> {
  const db = await openConversationStore();
  if (!db) return false;
  return new Promise((resolve) => {
    try {
      const transaction = db.transaction(CONVERSATION_STORE_NAME, "readwrite");
      transaction.oncomplete = () => resolve(true);
      transaction.onerror = () => resolve(false);
      transaction.onabort = () => resolve(false);
      transaction.objectStore(CONVERSATION_STORE_NAME).put(value, key);
    } catch {
      resolve(false);
    }
  });
}

function removeLegacyConversationStorageIfNeeded(): void {
  try {
    if (typeof localStorage !== "undefined") localStorage.removeItem(STORAGE_KEY);
  } catch {
    // Best effort only.
  }
}

async function persistConversationCacheToIndexedDB(): Promise<void> {
  if (conversationStorePersistRunning) {
    conversationStorePersistQueued = true;
    return;
  }
  conversationStorePersistRunning = true;
  try {
    const snapshot = normalizeConversationList(conversationCache);
    const ok = await conversationIdbSet(CONVERSATION_STORE_LIST_ID, snapshot);
    if (ok) {
      removeLegacyConversationStorageIfNeeded();
    } else {
      try {
        if (typeof localStorage !== "undefined") {
          localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
        }
      } catch (error) {
        console.warn("[Conversations] Failed to persist fallback localStorage copy:", error);
      }
    }
  } finally {
    conversationStorePersistRunning = false;
    if (conversationStorePersistQueued) {
      conversationStorePersistQueued = false;
      void persistConversationCacheToIndexedDB();
    }
  }
}

export async function ensureConversationStoreReady(): Promise<void> {
  if (persistentConversationStoreLoaded) return;
  if (persistentConversationStoreLoading) return persistentConversationStoreLoading;
  persistentConversationStoreLoading = (async () => {
    const loadSeq = conversationStoreWriteSeq;
    const legacy = readLegacyConversations();
    conversationCache = legacy;
    const idbConversations = await conversationIdbGet<ConversationRecord[]>(CONVERSATION_STORE_LIST_ID);

    if (conversationStoreWriteSeq !== loadSeq) {
      persistentConversationStoreLoaded = true;
      return;
    }

    if (Array.isArray(idbConversations)) {
      conversationCache = normalizeConversationList(idbConversations as ConversationRecord[]);
      removeLegacyConversationStorageIfNeeded();
    } else if (legacy.length > 0) {
      conversationCache = legacy;
      await persistConversationCacheToIndexedDB();
    }

    persistentConversationStoreLoaded = true;
    dispatchConversationUpdate();
  })().finally(() => {
    persistentConversationStoreLoading = null;
  });
  return persistentConversationStoreLoading;
}

void ensureConversationStoreReady();

export function subscribeConversationStore(listener: () => void): () => void {
  if (typeof window === "undefined") return () => {};
  const handler = () => listener();
  window.addEventListener(CONVERSATION_UPDATE_EVENT, handler);
  return () => window.removeEventListener(CONVERSATION_UPDATE_EVENT, handler);
}

function readConversations(): ConversationRecord[] {
  void ensureConversationStoreReady();
  return normalizeConversationList(conversationCache);
}

function writeConversations(conversations: ConversationRecord[]) {
  const normalized = normalizeConversationList(conversations);
  conversationStoreWriteSeq += 1;
  conversationCache = normalized;
  if (canUseConversationIndexedDB()) {
    void persistConversationCacheToIndexedDB();
    dispatchConversationUpdate();
    return;
  }
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized));
    dispatchConversationUpdate();
    return;
  } catch (error) {
    if (!isQuotaExceeded(error)) throw error;
  }

  throw new Error("浏览器本地存储空间不足。图片需要先写入 IndexedDB 后再导入，请使用当前版本重新导入；如果仍失败，请先删除部分旧窗口后重试。");
}

function conversationFingerprint(conversation: ConversationRecord): string {
  return JSON.stringify({
    title: conversation.title,
    linkedWatchTitle: conversation.linkedWatchTitle || "",
    linkedWatchRecordId: conversation.linkedWatchRecordId || "",
    messages: conversation.messages,
  });
}

function mergeConversationMessages(current: ConversationMessage[], incoming: ConversationMessage[]): ConversationMessage[] {
  const messages = new Map<string, ConversationMessage>();
  [...current, ...incoming].forEach((message) => {
    const key = message.id || `${message.role}:${message.createdAt}:${message.text}`;
    const previous = messages.get(key);
    messages.set(key, previous ? {
      ...message,
      ...previous,
      attachments: previous.attachments?.length ? previous.attachments : message.attachments,
    } : message);
  });
  return Array.from(messages.values()).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
}

export function listConversations(): ConversationRecord[] {
  return readConversations().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

export function exportConversationRecords(): ConversationRecord[] {
  return readConversations();
}

export function inspectConversationImport(incoming: ConversationRecord[]): ConversationImportInspection {
  const current = readConversations();
  const normalizedIncoming = normalizeConversationList(incoming);
  const byId = new Map(current.map((conversation) => [conversation.id, conversation]));
  const currentFingerprints = new Set(current.map(conversationFingerprint));
  const inspection: ConversationImportInspection = {
    total: normalizedIncoming.length,
    identical: 0,
    divergentSameId: 0,
    reusableEquivalent: 0,
    newRecords: 0,
  };

  normalizedIncoming.forEach((conversation) => {
    const sameId = byId.get(conversation.id);
    const fingerprint = conversationFingerprint(conversation);
    if (sameId) {
      if (conversationFingerprint(sameId) === fingerprint) inspection.identical += 1;
      else inspection.divergentSameId += 1;
      return;
    }
    if (currentFingerprints.has(fingerprint)) inspection.reusableEquivalent += 1;
    else inspection.newRecords += 1;
  });

  return inspection;
}

export function importConversationRecords(
  incoming: ConversationRecord[],
  conflictMode: ConversationImportConflictMode = "merge",
): ConversationImportReport {
  const current = readConversations();
  const normalizedIncoming = normalizeConversationList(incoming);
  const next = [...current];
  const report: ConversationImportReport = {
    added: 0,
    merged: 0,
    copied: 0,
    reusedIdentical: 0,
    idMap: {},
  };

  normalizedIncoming.forEach((conversation) => {
    const sameIdIndex = next.findIndex((item) => item.id === conversation.id);
    const fingerprint = conversationFingerprint(conversation);
    if (sameIdIndex >= 0) {
      const existing = next[sameIdIndex];
      if (conversationFingerprint(existing) === fingerprint) {
        report.reusedIdentical += 1;
        report.idMap[conversation.id] = existing.id;
        return;
      }
      if (conflictMode === "copy") {
        const copy = {
          ...conversation,
          id: createId("import"),
          title: `${conversation.title} (导入副本)`,
        };
        next.unshift(copy);
        report.idMap[conversation.id] = copy.id;
        report.copied += 1;
        return;
      }
      next[sameIdIndex] = {
        ...existing,
        ...conversation,
        id: existing.id,
        createdAt: existing.createdAt < conversation.createdAt ? existing.createdAt : conversation.createdAt,
        updatedAt: existing.updatedAt > conversation.updatedAt ? existing.updatedAt : conversation.updatedAt,
        messages: mergeConversationMessages(existing.messages, conversation.messages),
      };
      report.merged += 1;
      report.idMap[conversation.id] = existing.id;
      return;
    }

    const equivalent = next.find((item) => conversationFingerprint(item) === fingerprint);
    if (equivalent) {
      report.reusedIdentical += 1;
      report.idMap[conversation.id] = equivalent.id;
      return;
    }

    next.unshift(conversation);
    report.idMap[conversation.id] = conversation.id;
    report.added += 1;
  });

  writeConversations(next);
  return report;
}

export async function importConversationRecordsAsync(
  incoming: ConversationRecord[],
  conflictMode: ConversationImportConflictMode = "merge",
): Promise<ConversationImportReport> {
  await ensureConversationStoreReady();
  const prepared = await prepareConversationRecordsForStorage(incoming);
  return importConversationRecords(prepared, conflictMode);
}

export function getConversation(id: string): ConversationRecord | undefined {
  return readConversations().find((conversation) => conversation.id === id);
}

export function createConversation(title = "新的对话"): ConversationRecord {
  const createdAt = now();
  const conversation: ConversationRecord = {
    id: createId(),
    title,
    createdAt,
    updatedAt: createdAt,
    messages: [],
  };
  writeConversations([conversation, ...readConversations()]);
  return conversation;
}

export function saveConversation(next: ConversationRecord): ConversationRecord {
  const conversations = readConversations();
  const updated: ConversationRecord = {
    ...next,
    updatedAt: now(),
  };
  writeConversations([updated, ...conversations.filter((item) => item.id !== next.id)]);
  return updated;
}

export function renameConversation(id: string, title: string): ConversationRecord | undefined {
  const current = getConversation(id);
  if (!current) return undefined;
  return saveConversation({ ...current, title: title.trim() || current.title });
}

export function renameWatchConversationLink(oldRecordId: string, oldTitle: string, newRecordId: string, newTitle: string): void {
  const conversations = readConversations();
  const oldTitleKey = slugifyTitle(oldTitle || "");
  let changed = false;
  const next = conversations.map((conversation) => {
    const matchesRecord = conversation.linkedWatchRecordId === oldRecordId;
    const matchesTitle = Boolean(conversation.linkedWatchTitle && slugifyTitle(conversation.linkedWatchTitle) === oldTitleKey);
    if (!matchesRecord && !matchesTitle) return conversation;
    changed = true;
    return {
      ...conversation,
      title: conversation.title.includes(oldTitle) ? conversation.title.replace(oldTitle, newTitle) : conversation.title,
      linkedWatchTitle: newTitle,
      linkedWatchRecordId: newRecordId,
      updatedAt: now(),
    };
  });
  if (changed) writeConversations(next);
}

export function deleteConversation(id: string): void {
  writeConversations(readConversations().filter((conversation) => conversation.id !== id));
}

export function appendConversationMessages(
  conversationId: string,
  messages: Array<Omit<ConversationMessage, "id" | "createdAt">>,
): ConversationRecord | undefined {
  const current = getConversation(conversationId);
  if (!current) return undefined;
  const stampedMessages = messages.map((message) => ({
    ...message,
    id: createId(message.role),
    createdAt: now(),
  }));
  return saveConversation({
    ...current,
    messages: [...current.messages, ...stampedMessages],
  });
}

export function replaceConversationMessages(conversationId: string, messages: ConversationMessage[]): ConversationRecord | undefined {
  const current = getConversation(conversationId);
  if (!current) return undefined;
  return saveConversation({ ...current, messages });
}

export function findWatchConversation(title: string, recordId?: string): ConversationRecord | undefined {
  const watchKey = slugifyTitle(title || "untitled-movie");
  const conversations = readConversations();
  return conversations.find(
    (conversation) =>
      (recordId && conversation.linkedWatchRecordId === recordId)
      || (conversation.linkedWatchTitle && slugifyTitle(conversation.linkedWatchTitle) === watchKey),
  );
}

export function getOrCreateWatchConversation(title: string, recordId?: string): ConversationRecord {
  const existing = findWatchConversation(title, recordId);
  if (existing) return existing;

  const conversations = readConversations();
  const createdAt = now();
  const conversation: ConversationRecord = {
    id: createId("movie"),
    title: title ? `观影 · ${title}` : "观影对话",
    createdAt,
    updatedAt: createdAt,
    messages: [],
    linkedWatchTitle: title,
    linkedWatchRecordId: recordId,
  };
  writeConversations([conversation, ...conversations]);
  return conversation;
}
