import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";

import { probeAuthorityCapabilities } from "../../runtime/authority-capabilities.js";
import { AuthorityGraphStore } from "../../sync/authority-graph-store.js";
import {
  deleteAuthorityTriviumNodes,
  filterAuthorityTriviumNodes,
  normalizeAuthorityVectorConfig,
  purgeAuthorityTriviumNamespace,
  queryAuthorityTriviumNeighbors,
  searchAuthorityTriviumNodes,
  syncAuthorityTriviumLinks,
  upsertAuthorityTriviumEntries,
} from "../../vector/authority-vector-primary-adapter.js";
import {
  buildAuthorityJobIdempotencyKey,
  createAuthorityJobAdapter,
} from "../../maintenance/authority-job-adapter.js";
import { createAuthorityBlobAdapter } from "../../maintenance/authority-blob-adapter.js";

const env = process.env;
const baseUrl = String(env.AUTHORITY_E2E_BASE_URL || "").trim();

if (!baseUrl) {
  console.log("authority-server-primary E2E skipped: set AUTHORITY_E2E_BASE_URL to run against a real Authority server");
  process.exit(0);
}

function parsePositiveInteger(value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return fallback;
  return Math.min(max, Math.max(min, Math.trunc(parsed)));
}

function parseJsonObject(value, fallback = {}) {
  if (!value) return fallback;
  try {
    const parsed = JSON.parse(String(value));
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
  } catch {
    return fallback;
  }
}

function resolveBaseUrl(value) {
  const normalized = String(value || "").replace(/\/+$/g, "");
  if (/^https?:\/\//i.test(normalized)) return normalized;
  const origin = String(env.AUTHORITY_E2E_ORIGIN || "").replace(/\/+$/g, "");
  if (origin && normalized.startsWith("/")) return `${origin}${normalized}`;
  throw new Error("AUTHORITY_E2E_BASE_URL must be absolute, or set AUTHORITY_E2E_ORIGIN for relative plugin paths");
}

function createHeaderProvider() {
  const staticHeaders = parseJsonObject(env.AUTHORITY_E2E_HEADER_JSON, {});
  const token = String(env.AUTHORITY_E2E_TOKEN || "").trim();
  const cookie = String(env.AUTHORITY_E2E_COOKIE || "").trim();
  return () => ({
    ...staticHeaders,
    ...(token ? { Authorization: /^Bearer\s+/i.test(token) ? token : `Bearer ${token}` } : {}),
    ...(cookie ? { Cookie: cookie } : {}),
  });
}

function createFetchWithTimeout(timeoutMs) {
  return async (url, options = {}) => {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(new Error(`Authority E2E request timeout after ${timeoutMs}ms`)), timeoutMs);
    try {
      return await fetch(url, {
        ...options,
        signal: controller.signal,
      });
    } finally {
      clearTimeout(timer);
    }
  };
}

function createContractNode(id, title, nowMs) {
  return {
    id,
    type: "fact",
    fields: {
      title,
      summary: `${title} generated by Authority server-primary E2E contract smoke`,
    },
    seqRange: [1, 1],
    scope: {
      layer: "global",
      ownerType: "system",
      ownerId: "authority-e2e",
      bucket: "contract",
      regionKey: "authority-e2e-region",
    },
    storySegmentId: "authority-e2e-segment",
    importance: 0.8,
    archived: false,
    updatedAt: nowMs,
  };
}

function createContractGraph(chatId, runId) {
  const nowMs = Date.now();
  const nodeA = createContractNode(`${runId}-node-a`, "Authority E2E Alpha", nowMs);
  const nodeB = createContractNode(`${runId}-node-b`, "Authority E2E Beta", nowMs);
  return {
    meta: {
      schemaVersion: 1,
      chatId,
      deviceId: "authority-e2e",
      revision: 1,
      lastModified: nowMs,
      nodeCount: 2,
      edgeCount: 1,
      tombstoneCount: 0,
    },
    nodes: [nodeA, nodeB],
    edges: [
      {
        id: `${runId}-edge-a-b`,
        fromId: nodeA.id,
        toId: nodeB.id,
        relation: "related",
        type: "semantic",
        strength: 0.7,
        updatedAt: nowMs,
      },
    ],
    tombstones: [],
    state: {
      lastProcessedFloor: 1,
      extractionCount: 1,
    },
  };
}

function buildVectorEntries(graph) {
  return graph.nodes.map((node, index) => ({
    nodeId: node.id,
    index,
    hash: `${node.id}:hash`,
    text: `${node.fields.title}. ${node.fields.summary}`,
  }));
}

async function runStep(name, fn) {
  const startedAt = Date.now();
  try {
    const result = await fn();
    const durationMs = Date.now() - startedAt;
    console.log(`authority E2E ${name}: ok (${durationMs}ms)`);
    return { name, ok: true, durationMs, result };
  } catch (error) {
    const durationMs = Date.now() - startedAt;
    console.error(`authority E2E ${name}: failed (${durationMs}ms)`);
    console.error(error?.stack || error?.message || String(error));
    throw error;
  }
}

const resolvedBaseUrl = resolveBaseUrl(baseUrl);
const timeoutMs = parsePositiveInteger(env.AUTHORITY_E2E_TIMEOUT_MS, 15000, 1000, 300000);
const jobWaitTimeoutMs = parsePositiveInteger(env.AUTHORITY_E2E_JOB_WAIT_TIMEOUT_MS, 15000, 1000, 300000);
const headerProvider = createHeaderProvider();
const fetchImpl = createFetchWithTimeout(timeoutMs);
const runId = String(env.AUTHORITY_E2E_RUN_ID || `authority-e2e-${Date.now()}-${randomUUID().slice(0, 8)}`)
  .replace(/[^A-Za-z0-9._:-]+/g, "-");
const chatId = String(env.AUTHORITY_E2E_CHAT_ID || `st-bme-${runId}`);
const namespace = String(env.AUTHORITY_E2E_NAMESPACE || `st-bme-e2e-${runId}`);
const collectionId = String(env.AUTHORITY_E2E_COLLECTION_ID || `${namespace}::${chatId}`);
const blobPath = String(env.AUTHORITY_E2E_BLOB_PATH || `st-bme/e2e/${runId}/contract.json`);
const graph = createContractGraph(chatId, runId);

const context = {
  baseUrl: resolvedBaseUrl,
  chatId,
  namespace,
  collectionId,
  blobPath,
};

console.log(`authority-server-primary E2E started: ${JSON.stringify(context)}`);

await runStep("probe", async () => {
  const state = await probeAuthorityCapabilities({
    settings: { authorityBaseUrl: resolvedBaseUrl },
    fetchImpl,
    headerProvider,
    allowRelativeUrl: false,
  });
  assert.equal(state.installed, true);
  assert.equal(state.healthy, true);
  return {
    endpoint: state.endpoint,
    features: state.features,
    missingFeatures: state.missingFeatures,
  };
});

await runStep("sql", async () => {
  const store = new AuthorityGraphStore(chatId, {
    baseUrl: resolvedBaseUrl,
    fetchImpl,
    headerProvider,
  });
  try {
    await store.open();
    const importResult = await store.importSnapshot(graph, {
      mode: "replace",
      preserveRevision: true,
      markSyncDirty: false,
    });
    assert.equal(importResult.imported.nodes, graph.nodes.length);
    assert.equal(importResult.imported.edges, graph.edges.length);

    const commitResult = await store.commitDelta(
      {
        upsertNodes: [createContractNode(`${runId}-node-c`, "Authority E2E Gamma", Date.now())],
        runtimeMetaPatch: { authorityE2eRunId: runId },
      },
      { reason: "authority-e2e-contract", markSyncDirty: false },
    );
    assert.ok(commitResult.revision >= importResult.revision);

    const snapshot = await store.exportSnapshot({ includeTombstones: false });
    assert.equal(snapshot.meta.chatId, chatId);
    assert.ok(snapshot.nodes.some((node) => node.id === graph.nodes[0].id));
    assert.ok(snapshot.nodes.some((node) => node.id === `${runId}-node-c`));
    return {
      revision: snapshot.meta.revision,
      nodes: snapshot.nodes.length,
      edges: snapshot.edges.length,
    };
  } finally {
    await store.clearAll().catch(() => null);
    await store.close().catch(() => null);
  }
});

await runStep("trivium", async () => {
  const config = normalizeAuthorityVectorConfig({ authorityBaseUrl: resolvedBaseUrl });
  const entries = buildVectorEntries(graph);
  await purgeAuthorityTriviumNamespace(config, {
    namespace,
    collectionId,
    chatId,
    fetchImpl,
    headerProvider,
  }).catch(() => null);

  try {
    const upsertResult = await upsertAuthorityTriviumEntries(graph, config, entries, {
      namespace,
      collectionId,
      chatId,
      modelScope: "authority-e2e",
      revision: 1,
      fetchImpl,
      headerProvider,
    });
    assert.equal(upsertResult.upserted, entries.length);

    const linkResult = await syncAuthorityTriviumLinks(graph, config, {
      namespace,
      collectionId,
      chatId,
      revision: 1,
      fetchImpl,
      headerProvider,
    });
    assert.equal(linkResult.linked, graph.edges.length);

    const searchResults = await searchAuthorityTriviumNodes(graph, "Authority E2E Alpha", config, {
      namespace,
      collectionId,
      chatId,
      topK: 5,
      fetchImpl,
      headerProvider,
    });
    assert.ok(Array.isArray(searchResults));

    const filteredIds = await filterAuthorityTriviumNodes(config, {
      namespace,
      collectionId,
      chatId,
      topK: 10,
      where: { chatId, archived: false },
      searchText: "Authority E2E",
      fetchImpl,
      headerProvider,
    });
    assert.ok(Array.isArray(filteredIds));

    const neighborIds = await queryAuthorityTriviumNeighbors(config, [graph.nodes[0].id], {
      namespace,
      collectionId,
      chatId,
      topK: 5,
      fetchImpl,
      headerProvider,
    });
    assert.ok(Array.isArray(neighborIds));

    return {
      upserted: upsertResult.upserted,
      linked: linkResult.linked,
      searchResults: searchResults.length,
      filteredIds: filteredIds.length,
      neighborIds: neighborIds.length,
    };
  } finally {
    await deleteAuthorityTriviumNodes(config, graph.nodes.map((node) => node.id), {
      namespace,
      collectionId,
      chatId,
      fetchImpl,
      headerProvider,
    }).catch(() => null);
    await purgeAuthorityTriviumNamespace(config, {
      namespace,
      collectionId,
      chatId,
      fetchImpl,
      headerProvider,
    }).catch(() => null);
  }
});

await runStep("jobs", async () => {
  const adapter = createAuthorityJobAdapter(
    {
      authorityBaseUrl: resolvedBaseUrl,
      pollIntervalMs: 500,
      waitTimeoutMs: jobWaitTimeoutMs,
    },
    {
      fetchImpl,
      headerProvider,
    },
  );
  const listBefore = await adapter.listPage({ limit: 5 });
  assert.ok(Array.isArray(listBefore.jobs));

  const kind = String(env.AUTHORITY_E2E_JOB_KIND || "authority.vector.rebuild");
  const idempotencyKey = buildAuthorityJobIdempotencyKey({
    kind,
    chatId,
    collectionId,
    revision: 1,
  });
  const submitted = await adapter.submit(
    kind,
    {
      chatId,
      collectionId,
      namespace,
      modelScope: "authority-e2e",
      source: "authority-e2e-contract",
      purge: false,
      dryRun: true,
      contractSmoke: true,
      idempotencyKey,
    },
    { idempotencyKey },
  );
  assert.ok(submitted.id);

  const waited = await adapter.waitForCompletion(submitted.id, {
    timeoutMs: jobWaitTimeoutMs,
    pollIntervalMs: 500,
  });
  assert.ok(waited.id || waited.status);

  const requeued = await adapter.requeue(submitted.id, { safe: true });
  assert.ok(requeued.id || requeued.status);

  const listAfter = await adapter.listPage({ limit: 5 });
  assert.ok(Array.isArray(listAfter.jobs));
  return {
    listBefore: listBefore.jobs.length,
    submitted: submitted.id,
    waitedStatus: waited.status,
    requeuedStatus: requeued.status,
    listAfter: listAfter.jobs.length,
  };
});

await runStep("blob", async () => {
  const adapter = createAuthorityBlobAdapter(
    { authorityBaseUrl: resolvedBaseUrl, authorityBlobNamespace: namespace },
    { fetchImpl, headerProvider },
  );
  const payload = {
    runId,
    chatId,
    collectionId,
    createdAt: new Date().toISOString(),
    graph: {
      nodes: graph.nodes.length,
      edges: graph.edges.length,
    },
  };
  try {
    const writeResult = await adapter.writeJson(blobPath, payload, {
      metadata: { chatId, runId, purpose: "authority-e2e-contract" },
    });
    assert.equal(writeResult.ok, true);

    const statResult = await adapter.stat(blobPath);
    assert.equal(statResult.exists, true);

    const readResult = await adapter.readJson(blobPath);
    assert.equal(readResult.exists, true);
    assert.equal(readResult.payload.runId, runId);
    return {
      path: writeResult.path,
      size: writeResult.size,
      etag: writeResult.etag,
    };
  } finally {
    const deleteResult = await adapter.delete(blobPath).catch(() => null);
    if (deleteResult) assert.equal(deleteResult.ok, true);
  }
});

console.log("authority-server-primary E2E passed");
