import { applyStoreDiff, get, getAll, put, remove } from "./db";
import { jobs } from "./jobs";
import { decorateIndexItems } from "./renderComponents";
import type { IndexItem, Job, JobContext } from "./types";
import { VectorWorkerManager } from "./worker/vectorWorkerManager";
import { loadDynamicItems } from "../utils/dynamicItems";
import { getVectorizedItemIds, pruneOrphanVectorEmbeddings } from "./utils";
import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion";
import { resetSearchIndexes } from "./resetIndexes";
import { isIndexingPaused } from "./indexingPause";

import { verboseDebug } from '@/utils/verboseLog';
const META_STORE = "meta";
const LOCK_KEY = "bsq-indexer-lock";
const HEARTBEAT_INTERVAL = 10000;
const LOCK_TIMEOUT = 20000;
const LOCK_ACQUIRE_TIMEOUT = 5000;

let schemaCheckPromise: Promise<void> | null = null;

async function ensureSchemaCurrent(): Promise<void> {
  if (schemaCheckPromise) return schemaCheckPromise;
  schemaCheckPromise = (async () => {
    let storedRaw: string | null = null;
    try {
      storedRaw = localStorage.getItem(SCHEMA_VERSION_KEY);
    } catch {
      return;
    }
    const stored = storedRaw ? parseInt(storedRaw, 10) : 0;
    if (stored === INDEX_SCHEMA_VERSION) return;

    console.warn(
      `[Indexer] Schema version changed (${stored} -> ${INDEX_SCHEMA_VERSION}); resetting structured + vector indexes.`,
    );

    try {
      await resetSearchIndexes();
    } catch (e) {
      console.warn("[Indexer] Failed to reset search indexes:", e);
    }

    try {
      localStorage.setItem(SCHEMA_VERSION_KEY, String(INDEX_SCHEMA_VERSION));
    } catch {
      /* ignore */
    }
  })();
  return schemaCheckPromise;
}

export { ensureSchemaCurrent };

async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
  const rec = await get(META_STORE, `progress:${jobId}`);
  return rec?.progress as T | undefined;
}

async function saveProgress<T = any>(jobId: string, progress: T): Promise<void> {
  await put(META_STORE, { progress }, `progress:${jobId}`);
}

let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
let isIndexingActive = false;

function shouldRun(job: Job, lastRun?: number): boolean {
  const now = Date.now();

  if (job.frequency === "pageLoad") return true;
  if (!lastRun) return true;

  if (job.frequency.type === "interval") {
    return now - lastRun >= job.frequency.ms;
  }

  if (job.frequency.type === "expiry") {
    return now - lastRun >= job.frequency.afterMs;
  }

  return false;
}

function getLastRunMeta(jobId: string): Promise<number | undefined> {
  return get(META_STORE, jobId).then((rec) => rec?.lastRun);
}

function indexItemStorageKey(item: IndexItem): string {
  return JSON.stringify({
    id: item.id,
    text: item.text,
    category: item.category,
    content: item.content,
    dateAdded: item.dateAdded,
    metadata: item.metadata,
    actionId: item.actionId,
    renderComponentId: item.renderComponentId,
  });
}

function indexItemsEqual(a: IndexItem, b: IndexItem): boolean {
  return indexItemStorageKey(a) === indexItemStorageKey(b);
}

async function diffAndStoreItems(
  targetStore: string,
  items: IndexItem[],
): Promise<void> {
  const validItems = items.filter((i) => i && i.id);
  if (validItems.length !== items.length) {
    console.warn(
      `[Indexer] Filtered out ${items.length - validItems.length} invalid items before storing in '${targetStore}'.`,
    );
  }

  const existing = (await getAll(targetStore)) as IndexItem[];
  const existingMap = new Map(
    existing.filter((i) => i?.id).map((i) => [i.id, i]),
  );
  const newMap = new Map(validItems.map((i) => [i.id, i]));

  const puts: Array<{ key: string; value: IndexItem }> = [];
  const removeKeys: string[] = [];

  for (const [id, item] of newMap) {
    const prev = existingMap.get(id);
    if (!prev || !indexItemsEqual(prev, item)) {
      puts.push({ key: id, value: item });
    }
  }

  for (const id of existingMap.keys()) {
    if (!newMap.has(id)) {
      removeKeys.push(id);
    }
  }

  if (puts.length > 0 || removeKeys.length > 0) {
    await applyStoreDiff(targetStore, puts, removeKeys);
  }
}

async function updateLastRunMeta(jobId: string): Promise<void> {
  await put(META_STORE, { jobId, lastRun: Date.now() }, jobId);
}

async function tryClaimLock(lockId: string): Promise<boolean> {
  localStorage.setItem(LOCK_KEY, lockId);
  await new Promise((resolve) => setTimeout(resolve, 50));
  if (localStorage.getItem(LOCK_KEY) === lockId) {
    isIndexingActive = true;
    return true;
  }
  return false;
}

async function acquireLock(): Promise<boolean> {
  if (isIndexingActive) {
    verboseDebug("[Indexer] Already indexing in this tab");
    return false;
  }

  const lockId = `${Date.now()}-${Math.random()}`;
  const startTime = Date.now();

  while (Date.now() - startTime < LOCK_ACQUIRE_TIMEOUT) {
    const currentLock = localStorage.getItem(LOCK_KEY);
    const currentTime = Date.now();

    if (!currentLock) {
      if (await tryClaimLock(lockId)) return true;
    } else {
      try {
        const [timestamp] = currentLock.split("-");
        const lockTime = parseInt(timestamp, 10);
        if (isNaN(lockTime) || currentTime - lockTime > LOCK_TIMEOUT) {
          if (await tryClaimLock(lockId)) return true;
        }
      } catch (e) {
        console.warn("[Indexer] Error parsing lock:", e);
      }
    }

    await new Promise((resolve) => setTimeout(resolve, 100));
  }

  return false;
}

function startHeartbeat() {
  const lockId = localStorage.getItem(LOCK_KEY);
  if (!lockId) return;
  
  heartbeatTimer = setInterval(() => {
    if (localStorage.getItem(LOCK_KEY)?.endsWith(lockId.split('-')[1])) {
      const newLockId = `${Date.now()}-${lockId.split('-')[1]}`;
      localStorage.setItem(LOCK_KEY, newLockId);
    }
  }, HEARTBEAT_INTERVAL);
}

function stopHeartbeat() {
  if (heartbeatTimer) clearInterval(heartbeatTimer);
  localStorage.removeItem(LOCK_KEY);
  isIndexingActive = false;
}

function dispatchProgress(
  completed: number,
  total: number,
  indexing: boolean,
  status?: string,
  detail?: string,
) {
  const event = new CustomEvent("indexing-progress", {
    detail: { completed, total, indexing, status, detail },
  });
  window.dispatchEvent(event);
}

export async function loadAllStoredItems(): Promise<IndexItem[]> {
  const all: IndexItem[] = [];
  const jobIds = Object.keys(jobs);

  for (const jobId of jobIds) {
    try {
      const items = (await getAll(jobId)) as IndexItem[];
      const job = jobs[jobId];

      for (const item of items) {
        if (
          item &&
          item.id &&
          item.text &&
          item.category &&
          item.actionId &&
          job.renderComponentId // job might not be defined if store exists but job was removed
        ) {
          all.push(item);
        } else {
          console.warn(`Skipping invalid item from job store ${jobId}:`, item);
        }
      }
    } catch (error) {
      console.error(`Error loading items for job store ${jobId}:`, error);
    }
  }
  verboseDebug(
    `[Indexer] Loaded ${all.length} items from all primary stores.`,
  );
  return all;
}

function dispatchVectorProgress(
  progress: {
    status?: string;
    total?: number;
    processed?: number;
    message?: string;
  },
  completedJobs: number,
  totalSteps: number,
): number {
  const { status, total, processed, message = "" } = progress;
  let detail = message;
  let completed = completedJobs;

  if (status === "processing" && total != null && processed != null) {
    detail = `Vectorizing: ${processed} / ${total}`;
  } else if (status === "started") {
    detail = `Vectorization started for ${total} items`;
  } else if (status === "complete") {
    dispatchProgress(++completed, totalSteps, false, "Indexing finished", "Vectorization complete");
    return completed;
  } else if (status === "error") {
    dispatchProgress(completed, totalSteps, false, "Vectorization failed", `Vectorization error: ${message}`);
    return completed;
  } else if (status === "cancelled") {
    dispatchProgress(completed, totalSteps, false, "Vectorization cancelled", `Vectorization cancelled: ${message}`);
    return completed;
  } else {
    dispatchProgress(completed, totalSteps, true, "Vectorization in progress", detail);
  }

  return completed;
}

export async function runIndexing(): Promise<void> {
  if (isIndexingPaused()) {
    verboseDebug(
      "[Indexer] Skipping indexing — index was reset; reload the page to rebuild.",
    );
    return;
  }

  await ensureSchemaCurrent();
  if (isIndexingPaused()) return;

  if (!(await acquireLock())) {
    verboseDebug(
      "%c[Indexer] Could not acquire lock - another tab is indexing or this tab is already indexing",
      "color: gray",
    );
    return;
  }

  startHeartbeat();
  verboseDebug("%c[Indexer] Starting indexing...", "color: green");

  try {
  const jobIds = Object.keys(jobs);
  let completedJobs = 0;
  const totalSteps = jobIds.length + 1;
  dispatchProgress(completedJobs, totalSteps, true, "Starting jobs");

  for (const jobId of jobIds) {
    if (isIndexingPaused()) {
      verboseDebug(
        "[Indexer] Indexing stopped — index was reset; reload the page to rebuild.",
      );
      dispatchProgress(
        completedJobs,
        totalSteps,
        false,
        "Indexing paused — reload to rebuild",
      );
      return;
    }

    dispatchProgress(
      completedJobs,
      totalSteps,
      true,
      `Running job: ${jobs[jobId].label}`,
    );
    const job = jobs[jobId];
    const lastRun = await getLastRunMeta(jobId);

    if (!shouldRun(job, lastRun)) {
      verboseDebug(
        `%c[Indexer] Skipping job "${jobId}" (not due)`,
        "color: gray",
      );
      completedJobs++;
      dispatchProgress(
        completedJobs,
        totalSteps,
        true,
        `Skipped job: ${job.label}`,
      );
      continue;
    }

    const getStoredItems = async (storeId?: string) =>
      await getAll(storeId ?? jobId);
    const setStoredItems = async (items: IndexItem[], storeId?: string) => {
      const targetStore = storeId ?? jobId;
      await diffAndStoreItems(targetStore, items);
    };
    const addItem = async (item: IndexItem, storeId?: string) => {
      const targetStore = storeId ?? jobId;
      if (item && item.id) {
        await put(targetStore, item, item.id);
      } else {
        console.warn(
          `[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`,
          item,
        );
      }
    };
    const removeItem = async (id: string, storeId?: string) => {
      const targetStore = storeId ?? jobId;
      await remove(targetStore, id);
    };

    const ctx: JobContext = {
      getStoredItems,
      setStoredItems,
      addItem,
      removeItem,
      getProgress: () => loadProgress(jobId),
      setProgress: (p) => saveProgress(jobId, p),
    };

    verboseDebug(`%c[Indexer] Running job "${jobId}"...`, "color: #4ea1ff");

    try {
      const newItemsRaw = await job.run(ctx);
      const stored = await getStoredItems();

      let merged = mergeItems(stored, newItemsRaw);
      if (job.purge) merged = job.purge(merged);
      
      await setStoredItems(merged);
      await updateLastRunMeta(jobId);

      verboseDebug(
        `%c[Indexer] ${job.label}: ${newItemsRaw.length} new items reported by run, ${merged.length} total items now in '${jobId}' store.`,
        "color: #00c46f",
      );
    } catch (err) {
      verboseDebug(`%c[Indexer] Job ${job.label} failed:`, "color: red");
      console.error(err);
    }

    completedJobs++;
    dispatchProgress(
      completedJobs,
      totalSteps,
      true,
      `Finished job: ${job.label}`,
    );
  }

  let allItemsInPrimaryStores = await loadAllStoredItems();

  const liveItemIds = new Set(allItemsInPrimaryStores.map((item) => item.id));
  const prunedCount = await pruneOrphanVectorEmbeddings(liveItemIds);
  if (prunedCount > 0) {
    try {
      const { refreshVectorCache } = await import("../search/vector/vectorSearch");
      await refreshVectorCache();
    } catch (e) {
      console.warn("[Indexer] Failed to refresh vector cache after prune:", e);
    }
  }

  if (allItemsInPrimaryStores.length > 0) {
    verboseDebug(
      `%c[Indexer] Checking ${allItemsInPrimaryStores.length} items for vectorization...`,
      "color: #4ea1ff",
    );
    
    // Pre-filter items to avoid initializing worker if nothing new
    const vectorizedItemIds = await getVectorizedItemIds();
    const newItemsToVectorize = allItemsInPrimaryStores.filter(item => !vectorizedItemIds.has(item.id));
    
    if (newItemsToVectorize.length > 0) {
      verboseDebug(
        `%c[Indexer] Sending ${newItemsToVectorize.length} new items to worker for vectorization (${allItemsInPrimaryStores.length - newItemsToVectorize.length} already vectorized)`,
        "color: #4ea1ff",
      );
      dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of new items");

      try {
        const workerManager = VectorWorkerManager.getInstance();
        await workerManager.processItems(newItemsToVectorize, (progress) => {
          completedJobs = dispatchVectorProgress(progress, completedJobs, totalSteps);
        });
      verboseDebug(
        "%c[Indexer] Vectorization task for stored items sent to worker.",
        "color: green",
      );
    } catch (error) {
      console.error(
        `%c[Indexer] ❌ Failed to send items to vector worker:`,
        "color: red",
        error,
      );
      dispatchProgress(
        completedJobs,
        totalSteps,
        false,
        "Vectorization failed",
        String(error),
      );
    }
    } else {
      verboseDebug(
        `%c[Indexer] All ${allItemsInPrimaryStores.length} items are already vectorized, skipping worker initialization.`,
        "color: gray",
      );
      completedJobs++;
      dispatchProgress(
        completedJobs,
        totalSteps,
        false,
        "Indexing finished (all items already vectorized)",
      );
    }
  } else {
    verboseDebug(
      "%c[Indexer] No items found in primary stores to send for vectorization.",
      "color: gray",
    );
    completedJobs++;
    dispatchProgress(
      completedJobs,
      totalSteps,
      false,
      "Indexing finished (no items for vectorization)",
    );
  }

  allItemsInPrimaryStores = await loadAllStoredItems();
  const itemsWithComponents = decorateIndexItems(allItemsInPrimaryStores);
  loadDynamicItems(itemsWithComponents);
  window.dispatchEvent(
    new CustomEvent("dynamic-items-updated", {
      detail: { fullRebuild: true },
    }),
  );
  } finally {
    stopHeartbeat();
  }
}

function mergeItems(existing: IndexItem[], incoming: IndexItem[]): IndexItem[] {
  const map = new Map<string, IndexItem>();
  for (const item of existing) {
    if (item && item.id) map.set(item.id, item);
  }
  for (const item of incoming) {
    if (item && item.id) map.set(item.id, item);
  }
  return Array.from(map.values());
}