// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Analyzer API-to-DOM lag and trace-events page-read source fragment.

export function nodeWebObserveAnalyzerApiDomLagSource(): string {
  return String.raw`function buildApiDomLagReport(samples, network) {
  const windowMs = 30_000;
  const budgetMs = Number.isFinite(Number(alertThresholds.sameOriginApiSlowMs)) ? Number(alertThresholds.sameOriginApiSlowMs) : 10_000;
  const sampleRows = (Array.isArray(samples) ? samples : [])
    .map((sample) => {
      const tsMs = timestampMs(sample?.ts);
      return {
        sample,
        tsMs,
        pageKey: samplePageKey(sample),
        digest: digestSample(sample),
        sessionIds: new Set([sample?.routeSessionId, sample?.activeSessionId].filter(Boolean).map(String)),
        traceIds: sampleTraceIds(sample)
      };
    })
    .filter((item) => Number.isFinite(item.tsMs))
    .sort((a, b) => a.tsMs - b.tsMs);
  const samplesByPage = new Map();
  for (const row of sampleRows) {
    const rows = samplesByPage.get(row.pageKey) || [];
    rows.push(row);
    samplesByPage.set(row.pageKey, rows);
  }
  const naturalApiResponses = (Array.isArray(network) ? network : [])
    .filter((item) => item?.observerInitiated !== true && item?.type === "response" && isApiLikePath(urlPath(item?.url)));
  const telemetryExcluded = [];
  const nonStateRelevant = [];
  const stateRelevantResponses = [];
  for (const item of naturalApiResponses) {
    const event = compactApiDomLagResponseEvent(item);
    if (!Number.isFinite(event.tsMs)) {
      nonStateRelevant.push(event);
      continue;
    }
    if (isApiDomLagTelemetryPath(event.path)) telemetryExcluded.push(event);
    else if (!isApiDomLagStateRelevantPath(event.path)) nonStateRelevant.push(event);
    else stateRelevantResponses.push(event);
  }
  const candidates = [];
  for (const event of stateRelevantResponses) {
    const pageSamples = samplesByPage.get(event.pageKey) || [];
    const before = lastSampleAtOrBefore(pageSamples, event.tsMs, event);
    const firstAfter = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event);
    const baselineDigest = before?.digest ?? null;
    const change = firstSampleAfter(pageSamples, event.tsMs, event.tsMs + windowMs, event, (row) => !baselineDigest || row.digest !== baselineDigest);
    candidates.push({
      ...event,
      windowMs,
      budgetMs,
      firstSampleDeltaMs: firstAfter ? Math.max(0, Math.round(firstAfter.tsMs - event.tsMs)) : null,
      domChangeDeltaMs: change ? Math.max(0, Math.round(change.tsMs - event.tsMs)) : null,
      overBudget: change ? (change.tsMs - event.tsMs) > budgetMs : false,
      domChanged: Boolean(change),
      noDomChangeWithinWindow: !change,
      beforeSample: compactApiDomLagSample(before),
      firstAfterSample: compactApiDomLagSample(firstAfter),
      changeSample: compactApiDomLagSample(change),
      confidence: apiDomLagConfidence(event.path),
      valuesRedacted: true
    });
  }
  const changedDeltas = candidates.map((item) => nullableNumber(item.domChangeDeltaMs)).filter(Number.isFinite).sort((a, b) => a - b);
  const groups = groupApiDomLagCandidates(candidates);
  const overBudget = candidates.filter((item) => item.overBudget === true);
  return {
    summary: {
      windowMs,
      budgetMs,
      naturalApiResponseCount: naturalApiResponses.length,
      telemetryExcludedCount: telemetryExcluded.length,
      nonStateRelevantResponseCount: nonStateRelevant.length,
      stateRelevantResponseCount: stateRelevantResponses.length,
      candidateCount: candidates.length,
      domChangedCount: changedDeltas.length,
      noDomChangeWithinWindowCount: candidates.filter((item) => item.noDomChangeWithinWindow === true).length,
      lowConfidenceStreamOpenCount: candidates.filter((item) => item.confidence === "low-stream-open-only").length,
      overBudgetCount: overBudget.length,
      p50DomChangeDeltaMs: percentile(changedDeltas, 50),
      p95DomChangeDeltaMs: percentile(changedDeltas, 95),
      maxDomChangeDeltaMs: changedDeltas.length > 0 ? Math.max(...changedDeltas) : null,
      groupCount: groups.length,
      valuesRedacted: true
    },
    groups,
    worstCandidates: candidates
      .filter((item) => Number.isFinite(nullableNumber(item.domChangeDeltaMs)))
      .sort((a, b) => nullableNumber(b.domChangeDeltaMs) - nullableNumber(a.domChangeDeltaMs))
      .slice(0, 20),
    recentCandidates: candidates.slice(-40),
    telemetryExcluded: telemetryExcluded.slice(0, 20),
    nonStateRelevant: nonStateRelevant.slice(0, 20),
    valuesRedacted: true
  };
}

function compactApiDomLagResponseEvent(item) {
  const parsed = parseApiDomLagUrl(item?.url);
  const tsMs = timestampMs(item?.ts);
  return {
    ts: item?.ts ?? null,
    tsMs,
    pageRole: item?.pageRole ?? null,
    pageId: item?.pageId ?? null,
    pageKey: String(item?.pageRole || "control") + ":" + String(item?.pageId || "default"),
    commandId: item?.commandId ?? null,
    method: String(item?.method || "GET").toUpperCase(),
    status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
    path: parsed.path,
    rawPath: parsed.rawPath,
    queryKeys: parsed.queryKeys,
    sessionId: parsed.sessionId,
    traceId: parsed.traceId,
    urlHash: item?.url ? sha256(item.url) : null,
    routeKind: apiDomLagRouteKind(parsed.path),
    valuesRedacted: true
  };
}

function parseApiDomLagUrl(value) {
  try {
    const parsed = new URL(String(value || "http://invalid.local/"));
    const rawPath = parsed.pathname || "-";
    const queryKeys = Array.from(parsed.searchParams.keys()).sort().slice(0, 12);
    const sessionId = parsed.searchParams.get("sessionId") || parsed.searchParams.get("includeSessionId") || firstIdInText(parsed.pathname + " " + parsed.search, /\bses_[A-Za-z0-9_-]+\b/u);
    const traceId = parsed.searchParams.get("traceId") || firstIdInText(parsed.pathname + " " + parsed.search, /\btrc_[A-Za-z0-9_-]+\b/u);
    return {
      rawPath,
      path: normalizeApiPath(rawPath),
      queryKeys,
      sessionId,
      traceId
    };
  } catch {
    const rawPath = urlPath(value);
    return {
      rawPath,
      path: normalizeApiPath(rawPath),
      queryKeys: [],
      sessionId: firstIdInText(String(value || ""), /\bses_[A-Za-z0-9_-]+\b/u),
      traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u)
    };
  }
}

function firstIdInText(text, pattern) {
  const match = String(text || "").match(pattern);
  return match ? match[0] : null;
}

function nullableNumber(value) {
  if (value === null || value === undefined || value === "") return NaN;
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : NaN;
}

function isApiDomLagTelemetryPath(path) {
  const value = String(path || "");
  return value === "/v1/web-performance" || value === "/v1/health" || value === "/health";
}

function isApiDomLagStateRelevantPath(path) {
  const value = String(path || "");
  return value.startsWith("/auth/") || value.startsWith("/v1/workbench/") || value === "/v1/agent/chat" || value === "/v1/agent/chat/steer";
}

function apiDomLagRouteKind(path) {
  const value = String(path || "");
  if (value === "/v1/workbench/events") return "workbench-events-stream";
  if (value.startsWith("/v1/workbench/sessions")) return "workbench-sessions";
  if (value.startsWith("/v1/workbench/traces")) return "workbench-traces";
  if (value.startsWith("/v1/workbench/turns")) return "workbench-turns";
  if (value === "/v1/agent/chat" || value === "/v1/agent/chat/steer") return "agent-chat-submit";
  if (value.startsWith("/auth/")) return "auth";
  return "state-api";
}

function apiDomLagConfidence(path) {
  return String(path || "") === "/v1/workbench/events" ? "low-stream-open-only" : "medium-response-to-dom";
}

function sampleTraceIds(sample) {
  const ids = new Set();
  for (const group of [sample?.messages, sample?.traceRows, sample?.turns, sample?.diagnostics]) {
    if (!Array.isArray(group)) continue;
    for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
  }
  return ids;
}

function lastSampleAtOrBefore(rows, tsMs, event) {
  let result = null;
  for (const row of rows) {
    if (row.tsMs > tsMs) break;
    if (apiDomLagSampleMatchesEvent(row, event)) result = row;
  }
  return result;
}

function firstSampleAfter(rows, startMs, endMs, event, predicate = null) {
  for (const row of rows) {
    if (row.tsMs < startMs) continue;
    if (row.tsMs > endMs) break;
    if (!apiDomLagSampleMatchesEvent(row, event)) continue;
    if (typeof predicate === "function" && !predicate(row)) continue;
    return row;
  }
  return null;
}

function apiDomLagSampleMatchesEvent(row, event) {
  if (!row || !event) return false;
  if (event.sessionId && !row.sessionIds.has(String(event.sessionId))) return false;
  if (event.traceId && row.traceIds.size > 0 && !row.traceIds.has(String(event.traceId))) return false;
  return true;
}

function compactApiDomLagSample(row) {
  if (!row) return null;
  const sample = row.sample || {};
  return {
    seq: sample.seq ?? null,
    ts: sample.ts ?? null,
    pageRole: sample.pageRole ?? null,
    pageId: sample.pageId ?? null,
    routeSessionId: sample.routeSessionId ?? null,
    activeSessionId: sample.activeSessionId ?? null,
    messageCount: Array.isArray(sample.messages) ? sample.messages.length : null,
    traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : null,
    diagnosticCount: Array.isArray(sample.diagnostics) ? sample.diagnostics.length : null,
    valuesRedacted: true
  };
}

function groupApiDomLagCandidates(candidates) {
  const groups = new Map();
  for (const item of candidates || []) {
    const key = [item.method || "-", item.path || "-", item.status ?? "-", item.confidence || "-"].join(" ");
    const group = groups.get(key) || {
      method: item.method ?? null,
      path: item.path ?? "-",
      routeKind: item.routeKind ?? null,
      status: item.status ?? null,
      confidence: item.confidence ?? null,
      count: 0,
      domChangedCount: 0,
      noDomChangeWithinWindowCount: 0,
      overBudgetCount: 0,
      firstAt: item.ts ?? null,
      lastAt: item.ts ?? null,
      deltas: [],
      examples: []
    };
    group.count += 1;
    group.firstAt = minIso(group.firstAt, item.ts ?? null);
    group.lastAt = maxIso(group.lastAt, item.ts ?? null);
    if (item.domChanged === true && Number.isFinite(Number(item.domChangeDeltaMs))) {
      group.domChangedCount += 1;
      group.deltas.push(Number(item.domChangeDeltaMs));
    }
    if (item.noDomChangeWithinWindow === true) group.noDomChangeWithinWindowCount += 1;
    if (item.overBudget === true) group.overBudgetCount += 1;
    if (group.examples.length < 6) {
      group.examples.push({
        ts: item.ts ?? null,
        sessionId: item.sessionId ?? null,
        traceId: item.traceId ?? null,
        domChangeDeltaMs: item.domChangeDeltaMs ?? null,
        firstSampleDeltaMs: item.firstSampleDeltaMs ?? null,
        changeSeq: item.changeSample?.seq ?? null,
        beforeSeq: item.beforeSample?.seq ?? null,
        valuesRedacted: true
      });
    }
    groups.set(key, group);
  }
  return Array.from(groups.values())
    .map((item) => {
      const deltas = item.deltas.slice().sort((a, b) => a - b);
      return {
        method: item.method,
        path: item.path,
        routeKind: item.routeKind,
        status: item.status,
        confidence: item.confidence,
        count: item.count,
        domChangedCount: item.domChangedCount,
        noDomChangeWithinWindowCount: item.noDomChangeWithinWindowCount,
        overBudgetCount: item.overBudgetCount,
        p50DomChangeDeltaMs: percentile(deltas, 50),
        p95DomChangeDeltaMs: percentile(deltas, 95),
        maxDomChangeDeltaMs: deltas.length > 0 ? Math.max(...deltas) : null,
        firstAt: item.firstAt,
        lastAt: item.lastAt,
        examples: item.examples,
        valuesRedacted: true
      };
    })
    .sort((a, b) => Number(b.maxDomChangeDeltaMs ?? -1) - Number(a.maxDomChangeDeltaMs ?? -1) || b.count - a.count || String(a.path).localeCompare(String(b.path)));
}

function detectTraceEventsPageReadIssues(network) {
  const events = (Array.isArray(network) ? network : [])
    .filter((item) => item?.observerInitiated !== true && (item?.type === "response" || item?.type === "requestfailed"))
    .map(compactTraceEventsPageReadEvent)
    .filter((item) => item !== null);
  const http404 = events.filter((item) => item.type === "response" && Number(item.status) === 404);
  const httpErrors = events.filter((item) => item.type === "response" && Number(item.status) >= 400 && Number(item.status) !== 404);
  const requestFailed = events.filter((item) => item.type === "requestfailed");
  return {
    events,
    http404,
    httpErrors,
    requestFailed,
    summary: traceEventsPageReadIssueSummary(events),
    valuesRedacted: true
  };
}

function compactTraceEventsPageReadEvent(item) {
  const parsed = parseTraceEventsPageReadUrl(item?.url);
  if (!parsed.match) return null;
  const failureText = item?.failureKind ?? item?.failure ?? item?.errorText ?? null;
  return {
    ts: item?.ts ?? null,
    pageRole: item?.pageRole ?? null,
    pageId: item?.pageId ?? null,
    commandId: item?.commandId ?? null,
    method: String(item?.method || "GET").toUpperCase(),
    type: item?.type ?? null,
    status: Number.isFinite(Number(item?.status)) ? Number(item.status) : null,
    path: "/v1/workbench/traces/:traceId/events",
    rawPath: parsed.rawPath,
    traceId: parsed.traceId,
    afterProjectedSeq: parsed.afterProjectedSeq,
    sinceSeq: parsed.sinceSeq,
    limit: parsed.limit,
    tail: parsed.tail,
    queryKeys: parsed.queryKeys,
    failureKind: failureText ? limitText(String(failureText), 120) : null,
    urlHash: item?.url ? sha256(item.url) : null,
    valuesRedacted: true
  };
}

function parseTraceEventsPageReadUrl(value) {
  const fallback = {
    match: false,
    rawPath: urlPath(value),
    traceId: firstIdInText(String(value || ""), /\btrc_[A-Za-z0-9_-]+\b/u),
    afterProjectedSeq: null,
    sinceSeq: null,
    limit: null,
    tail: null,
    queryKeys: [],
  };
  try {
    const parsed = new URL(String(value || ""), "http://invalid.local/");
    const rawPath = parsed.pathname || "-";
    const match = rawPath.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
    return {
      match: Boolean(match),
      rawPath,
      traceId: match ? decodeURIComponent(match[1]) : fallback.traceId,
      afterProjectedSeq: numericSearchParam(parsed.searchParams, "afterProjectedSeq"),
      sinceSeq: numericSearchParam(parsed.searchParams, "sinceSeq") ?? numericSearchParam(parsed.searchParams, "afterSeq"),
      limit: numericSearchParam(parsed.searchParams, "limit"),
      tail: numericSearchParam(parsed.searchParams, "tail"),
      queryKeys: Array.from(parsed.searchParams.keys()).sort().slice(0, 12),
    };
  } catch {
    return fallback;
  }
}

function numericSearchParam(searchParams, key) {
  const raw = searchParams?.get?.(key);
  if (raw === null || raw === undefined || raw === "") return null;
  const parsed = Number(raw);
  return Number.isFinite(parsed) ? parsed : null;
}

function traceEventsPageReadIssueSummary(events) {
  const items = Array.isArray(events) ? events : [];
  const statuses = uniqueSorted(items.map((item) => item.status).filter((item) => item !== null && item !== undefined).map(String));
  const traceIds = uniqueSorted(items.map((item) => item.traceId).filter(Boolean).map(String)).slice(0, 8);
  const afterProjectedSeqs = uniqueSorted(items.map((item) => item.afterProjectedSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
  const sinceSeqs = uniqueSorted(items.map((item) => item.sinceSeq).filter((item) => item !== null && item !== undefined).map(String)).slice(0, 8);
  const failureKinds = uniqueSorted(items.map((item) => item.failureKind).filter(Boolean).map(String)).slice(0, 6);
  return {
    eventCount: items.length,
    responseErrorCount: items.filter((item) => item.type === "response" && Number(item.status) >= 400).length,
    http404Count: items.filter((item) => item.type === "response" && Number(item.status) === 404).length,
    requestFailedCount: items.filter((item) => item.type === "requestfailed").length,
    statuses,
    traceIds,
    afterProjectedSeqs,
    sinceSeqs,
    failureKinds,
    firstAt: items.reduce((value, item) => minIso(value, item.ts ?? null), null),
    lastAt: items.reduce((value, item) => maxIso(value, item.ts ?? null), null),
    rootCauseVisibility: "browser network rows identify trace-events page-read path; OTel trace_events_read should confirm backend paging fields",
    valuesRedacted: true
  };
}

function uniqueSorted(values) {
  return Array.from(new Set((values || []).filter((item) => item !== null && item !== undefined).map(String))).sort();
}

function compactApiDomLagForOutput(report) {
  if (!report || typeof report !== "object") return null;
  return {
    summary: report.summary ?? null,
    groups: Array.isArray(report.groups) ? report.groups.slice(0, 8) : [],
    worstCandidates: Array.isArray(report.worstCandidates) ? report.worstCandidates.slice(0, 8) : [],
    recentCandidates: Array.isArray(report.recentCandidates) ? report.recentCandidates.slice(-8) : [],
    valuesRedacted: true
  };
}
`;
}
