import { performance } from "node:perf_hooks";

import {
  buildGraphFromSnapshot,
  buildPersistDelta,
  buildSnapshotFromGraph,
} from "../../sync/bme-db.js";
import {
  BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY,
  OpfsGraphStore,
} from "../../sync/bme-opfs-store.js";
import { createMemoryOpfsRoot } from "../helpers/memory-opfs.mjs";

const RUNS = 4;
const outputJson = process.argv.includes("--json");
const useNativeHydrate = process.argv.includes("--native-hydrate");
const nativeHydrateThresholdArg = process.argv.find((entry) =>
  String(entry || "").startsWith("--native-hydrate-threshold="),
);
const nativeHydrateThresholdRecords = nativeHydrateThresholdArg
  ? Math.max(
      0,
      Math.floor(
        Number(String(nativeHydrateThresholdArg).split("=").slice(1).join("=") || 0) || 0,
      ),
    )
  : undefined;
const SIZE_PRESETS = [
  { label: "M", seed: 17, nodeCount: 1200, edgeCount: 3600, churn: 0.08 },
  { label: "L", seed: 29, nodeCount: 3600, edgeCount: 10800, churn: 0.1 },
  { label: "XL", seed: 43, nodeCount: 7200, edgeCount: 21600, churn: 0.12 },
];

let nativeHydratePreloadStatus = useNativeHydrate ? "pending" : "not-requested";
let nativeHydratePreloadError = "";

function summarize(values = []) {
  if (!values.length) {
    return { avg: 0, p95: 0, min: 0, max: 0 };
  }
  const sorted = [...values].sort((a, b) => a - b);
  const sum = sorted.reduce((acc, value) => acc + value, 0);
  const p95Index = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95));
  return {
    avg: sum / sorted.length,
    p95: sorted[p95Index],
    min: sorted[0],
    max: sorted[sorted.length - 1],
  };
}

function formatSummary(label, values = []) {
  const summary = summarize(values);
  return `${label} avg=${summary.avg.toFixed(2)}ms p95=${summary.p95.toFixed(2)}ms min=${summary.min.toFixed(2)}ms max=${summary.max.toFixed(2)}ms`;
}

function createRandom(seed = 1) {
  let state = seed >>> 0;
  return () => {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 0xffffffff;
  };
}

function buildRuntimeGraph(seed = 1, nodeCount = 100, edgeCount = 200, chatId = "bench-chat") {
  const rand = createRandom(seed);
  const nodes = [];
  const edges = [];
  for (let index = 0; index < nodeCount; index += 1) {
    nodes.push({
      id: `node-${index}`,
      type: "event",
      updatedAt: 1000 + index,
      archived: false,
      sourceFloor: index,
      fields: {
        title: `Node ${index}`,
        text: `node-${index}-${Math.floor(rand() * 100000)}`,
      },
    });
  }
  for (let index = 0; index < edgeCount; index += 1) {
    const fromIndex = Math.floor(rand() * nodeCount);
    let toIndex = Math.floor(rand() * nodeCount);
    if (toIndex === fromIndex) {
      toIndex = (toIndex + 1) % nodeCount;
    }
    edges.push({
      id: `edge-${index}`,
      fromId: `node-${fromIndex}`,
      toId: `node-${toIndex}`,
      relation: "related",
      strength: rand(),
      updatedAt: 2000 + index,
    });
  }
  return {
    version: 1,
    nodes,
    edges,
    historyState: {
      chatId,
      lastProcessedAssistantFloor: Math.max(0, Math.floor(nodeCount / 12)),
      extractionCount: Math.max(1, Math.floor(nodeCount / 40)),
      processedMessageHashes: {},
      processedMessageHashVersion: 1,
      processedMessageHashesNeedRefresh: false,
      recentRecallOwnerKeys: [],
      activeRecallOwnerKey: "",
      activeRegion: "",
      activeRegionSource: "",
      activeStorySegmentId: "",
      activeStoryTimeLabel: "",
      activeStoryTimeSource: "",
      lastBatchStatus: null,
      lastMutationSource: "bench",
      lastExtractedRegion: "",
      lastExtractedStorySegmentId: "",
      activeCharacterPovOwner: "",
      activeUserPovOwner: "",
    },
    vectorIndexState: {
      chatId,
      collectionId: "",
      hashToNodeId: {},
      nodeToHash: {},
      replayRequiredNodeIds: [],
      dirty: false,
      dirtyReason: "",
      pendingRepairFromFloor: null,
      lastIntegrityIssue: null,
      lastStats: {
        nodesIndexed: 0,
        updatedAt: 0,
      },
    },
    knowledgeState: {
      owners: {},
      activeOwnerKey: "",
    },
    regionState: {
      activeRegion: "",
      knownRegions: {},
      manualActiveRegion: "",
    },
    timelineState: {
      activeSegmentId: "",
      manualActiveSegmentId: "",
      segments: [],
    },
    summaryState: {
      updatedAt: 0,
      entries: [],
    },
    batchJournal: [],
    maintenanceJournal: [],
    lastRecallResult: null,
    lastProcessedSeq: Math.max(0, Math.floor(nodeCount / 12)),
  };
}

function mutateRuntimeGraph(baseGraph, seed = 1, churn = 0.1) {
  const rand = createRandom(seed);
  const nextGraph = structuredClone(baseGraph);
  const mutateNodeCount = Math.max(1, Math.floor(nextGraph.nodes.length * churn));
  const mutateEdgeCount = Math.max(1, Math.floor(nextGraph.edges.length * churn * 0.5));
  for (let index = 0; index < mutateNodeCount; index += 1) {
    const nodeIndex = Math.floor(rand() * nextGraph.nodes.length);
    const node = nextGraph.nodes[nodeIndex];
    node.updatedAt += 100 + index;
    node.fields.text = `${node.fields.text}-mut-${index}`;
  }
  for (let index = 0; index < mutateEdgeCount; index += 1) {
    const edgeIndex = Math.floor(rand() * nextGraph.edges.length);
    const edge = nextGraph.edges[edgeIndex];
    edge.updatedAt += 80 + index;
    edge.strength = rand();
  }
  const addNodeCount = Math.max(1, Math.floor(nextGraph.nodes.length * churn * 0.12));
  const baseNodeId = nextGraph.nodes.length;
  for (let index = 0; index < addNodeCount; index += 1) {
    nextGraph.nodes.push({
      id: `node-new-${baseNodeId + index}`,
      type: "event",
      updatedAt: 5000 + index,
      archived: false,
      sourceFloor: baseNodeId + index,
      fields: {
        title: `Node new ${index}`,
        text: `new-node-${index}`,
      },
    });
  }
  const deleteEdgeCount = Math.max(1, Math.floor(nextGraph.edges.length * churn * 0.08));
  nextGraph.edges.splice(0, deleteEdgeCount);
  nextGraph.historyState.lastProcessedAssistantFloor += 1;
  nextGraph.historyState.extractionCount += 1;
  nextGraph.lastProcessedSeq = nextGraph.historyState.lastProcessedAssistantFloor;
  nextGraph.summaryState.updatedAt += 1;
  return nextGraph;
}

function buildBenchPair({ label, seed, nodeCount, edgeCount, churn }) {
  const chatId = `bench-${label.toLowerCase()}`;
  const beforeGraph = buildRuntimeGraph(seed, nodeCount, edgeCount, chatId);
  const afterGraph = mutateRuntimeGraph(beforeGraph, seed + 101, churn);
  return {
    label,
    chatId,
    beforeGraph,
    afterGraph,
  };
}

function measureSnapshotBuild(graph, options) {
  let diagnostics = null;
  const startedAt = performance.now();
  const snapshot = buildSnapshotFromGraph(graph, {
    ...options,
    onDiagnostics(snapshotValue) {
      diagnostics = snapshotValue;
    },
  });
  return {
    elapsedMs: performance.now() - startedAt,
    snapshot,
    diagnostics,
  };
}

function measureHydrate(snapshot, chatId) {
  let diagnostics = null;
  const startedAt = performance.now();
  buildGraphFromSnapshot(snapshot, {
    chatId,
    useNativeHydrate,
    loadNativeHydrateThresholdRecords: nativeHydrateThresholdRecords,
    nativeFailOpen: true,
    onDiagnostics(snapshotValue) {
      diagnostics = snapshotValue;
    },
  });
  return {
    elapsedMs: performance.now() - startedAt,
    diagnostics,
  };
}

async function measureOpfsCommit(baseSnapshot, afterSnapshot, delta, chatId) {
  const rootDirectory = createMemoryOpfsRoot();
  const store = new OpfsGraphStore(chatId, {
    rootDirectoryFactory: async () => rootDirectory,
    storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY,
  });
  await store.open();
  await store.importSnapshot(baseSnapshot, {
    mode: "replace",
    preserveRevision: true,
    markSyncDirty: false,
  });
  const startedAt = performance.now();
  const result = await store.commitDelta(delta, {
    reason: "bench-commit",
    requestedRevision: Number(afterSnapshot?.meta?.revision || 0),
    markSyncDirty: true,
    committedSnapshot: afterSnapshot,
  });
  const elapsedMs = performance.now() - startedAt;
  await store.close();
  return {
    elapsedMs,
    diagnostics: result?.diagnostics || {},
  };
}

async function runPreset(preset) {
  const snapshotBuildSamples = [];
  const hydrateSamples = [];
  const opfsCommitSamples = [];
  const snapshotNodesSamples = [];
  const hydrateRuntimeMetaSamples = [];
  const hydrateNodesSamples = [];
  const hydrateEdgesSamples = [];
  const hydrateStateSamples = [];
  const hydrateNormalizeSamples = [];
  const hydrateIntegritySamples = [];
  const hydrateNativeRecordsSamples = [];
  const walFileWriteSamples = [];
  const manifestFileWriteSamples = [];
  let hydrateNativeUsedRuns = 0;

  for (let run = 0; run < RUNS; run += 1) {
    const pair = buildBenchPair({
      ...preset,
      seed: preset.seed + run * 17,
    });
    const beforeSnapshotResult = measureSnapshotBuild(pair.beforeGraph, {
      chatId: pair.chatId,
      revision: 1,
    });
    const afterSnapshotResult = measureSnapshotBuild(pair.afterGraph, {
      chatId: pair.chatId,
      revision: 2,
      baseSnapshot: beforeSnapshotResult.snapshot,
    });
    const delta = buildPersistDelta(
      beforeSnapshotResult.snapshot,
      afterSnapshotResult.snapshot,
      { useNativeDelta: false },
    );
    const hydrateResult = measureHydrate(afterSnapshotResult.snapshot, pair.chatId);
    const opfsCommitResult = await measureOpfsCommit(
      beforeSnapshotResult.snapshot,
      afterSnapshotResult.snapshot,
      delta,
      pair.chatId,
    );

    snapshotBuildSamples.push(afterSnapshotResult.elapsedMs);
    hydrateSamples.push(hydrateResult.elapsedMs);
    opfsCommitSamples.push(opfsCommitResult.elapsedMs);
    snapshotNodesSamples.push(Number(afterSnapshotResult.diagnostics?.nodesMs || 0));
    hydrateRuntimeMetaSamples.push(Number(hydrateResult.diagnostics?.runtimeMetaMs || 0));
    hydrateNodesSamples.push(Number(hydrateResult.diagnostics?.nodesMs || 0));
    hydrateEdgesSamples.push(Number(hydrateResult.diagnostics?.edgesMs || 0));
    hydrateStateSamples.push(Number(hydrateResult.diagnostics?.stateMs || 0));
    hydrateNormalizeSamples.push(Number(hydrateResult.diagnostics?.normalizeMs || 0));
    hydrateIntegritySamples.push(Number(hydrateResult.diagnostics?.integrityMs || 0));
    hydrateNativeRecordsSamples.push(
      Number(hydrateResult.diagnostics?.nativeRecordsMs || 0),
    );
    if (hydrateResult.diagnostics?.nativeUsed === true) {
      hydrateNativeUsedRuns += 1;
    }
    walFileWriteSamples.push(Number(opfsCommitResult.diagnostics?.walFileWriteMs || 0));
    manifestFileWriteSamples.push(
      Number(opfsCommitResult.diagnostics?.manifestFileWriteMs || 0),
    );
  }

  const result = {
    snapshotBuildMs: summarize(snapshotBuildSamples),
    snapshotNodesMs: summarize(snapshotNodesSamples),
    hydrateMs: summarize(hydrateSamples),
    hydrateNodesMs: summarize(hydrateNodesSamples),
    hydrateEdgesMs: summarize(hydrateEdgesSamples),
    hydrateStateMs: summarize(hydrateStateSamples),
    hydrateNormalizeMs: summarize(hydrateNormalizeSamples),
    hydrateIntegrityMs: summarize(hydrateIntegritySamples),
    hydrateNativeRecordsMs: summarize(hydrateNativeRecordsSamples),
    hydrateNativeUsedRuns,
    nativeHydrateRequested: useNativeHydrate,
    nativeHydrateThresholdRecords:
      nativeHydrateThresholdRecords == null ? null : nativeHydrateThresholdRecords,
    hydrateRuntimeMetaMs: summarize(hydrateRuntimeMetaSamples),
    opfsCommitMs: summarize(opfsCommitSamples),
    opfsWalFileWriteMs: summarize(walFileWriteSamples),
    opfsManifestFileWriteMs: summarize(manifestFileWriteSamples),
  };
  if (!outputJson) {
    console.log(`\n[ST-BME][persist-load-bench] ${preset.label}`);
    console.log(
      formatSummary("snapshot-build", snapshotBuildSamples),
      `nodesPhaseP95=${result.snapshotNodesMs.p95.toFixed(2)}ms`,
    );
    console.log(
      formatSummary("hydrate", hydrateSamples),
      `nodesP95=${result.hydrateNodesMs.p95.toFixed(2)}ms`,
      `edgesP95=${result.hydrateEdgesMs.p95.toFixed(2)}ms`,
      `normalizeP95=${result.hydrateNormalizeMs.p95.toFixed(2)}ms`,
      `integrityP95=${result.hydrateIntegrityMs.p95.toFixed(2)}ms`,
      `nativeRecordsP95=${result.hydrateNativeRecordsMs.p95.toFixed(2)}ms`,
      `nativeUsed=${result.hydrateNativeUsedRuns}/${RUNS}`,
      `runtimeMetaP95=${result.hydrateRuntimeMetaMs.p95.toFixed(2)}ms`,
    );
    console.log(
      formatSummary("opfs-commit", opfsCommitSamples),
      `walFileP95=${result.opfsWalFileWriteMs.p95.toFixed(2)}ms`,
      `manifestFileP95=${result.opfsManifestFileWriteMs.p95.toFixed(2)}ms`,
    );
  }
  return result;
}

async function main() {
  if (useNativeHydrate) {
    try {
      const nativeModule = await import("../../vendor/wasm/stbme_core.js");
      const nativeStatus = await nativeModule?.installNativeHydrateHook?.();
      nativeHydratePreloadStatus = nativeStatus?.loaded ? "loaded" : "not-loaded";
      nativeHydratePreloadError = String(nativeStatus?.error || "");
    } catch (error) {
      nativeHydratePreloadStatus = "failed";
      nativeHydratePreloadError = error?.message || String(error);
      console.warn(
        "[ST-BME][persist-load-bench] native hydrate preload failed, fallback to JS hydrate:",
        error,
      );
    }
  }
  const presets = {};
  for (const preset of SIZE_PRESETS) {
    presets[preset.label] = await runPreset(preset);
  }
  const payload = {
    runs: RUNS,
    nativeHydrateRequested: useNativeHydrate,
    nativeHydratePreloadStatus,
    nativeHydratePreloadError,
    nativeHydrateThresholdRecords:
      nativeHydrateThresholdRecords == null ? null : nativeHydrateThresholdRecords,
    presets,
  };
  if (outputJson) {
    console.log(JSON.stringify(payload));
  }
}

await main();
