/*
 * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0.
 * Responsibility: Pure cursor planning for AgentRun events projection; never derives cursor by scanning trace events or result payloads.
 */

const DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT = 500;
const MAX_AGENTRUN_EVENTS_PAGE_LIMIT = 1000;

export function buildAgentRunProjectionEventsFetchPlan({ projectionState = null, agentRun = null, pageLimit = DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT } = {}) {
  const state = normalizeProjectionState(projectionState);
  const mapping = normalizeAgentRunMapping(agentRun);
  const runId = textValue(state?.sourceRunId ?? state?.runId ?? mapping.runId);
  const commandId = textValue(state?.sourceCommandId ?? state?.commandId ?? mapping.commandId);
  const stateSeq = maxNonNegativeInteger(state?.lastSourceSeq, state?.lastAgentRunSeq);
  const mappingSeq = maxNonNegativeInteger(mapping.lastSeq);
  return {
    runId,
    commandId,
    afterSeq: Math.max(stateSeq, mappingSeq),
    limit: boundedPageLimit(pageLimit),
    cursorSource: state ? "projection-state" : "agent-run-result",
    valuesPrinted: false
  };
}

export function buildWorkbenchProjectionStateUpdate({
  currentState = null,
  result = null,
  agentRun = null,
  eventsResponse = null,
  resultSyncState = null,
  projectionStatus = null,
  projectionHealth = null,
  error = null,
  now = () => new Date().toISOString()
} = {}) {
  const previous = normalizeProjectionState(currentState) ?? {};
  const mapping = normalizeAgentRunMapping(agentRun ?? result?.agentRun);
  const timestamp = timestampValue(now);
  const traceId = textValue(previous.traceId ?? result?.traceId ?? mapping.traceId);
  const runId = textValue(previous.sourceRunId ?? previous.runId ?? mapping.runId);
  const commandId = textValue(previous.sourceCommandId ?? previous.commandId ?? mapping.commandId);
  const lastSourceSeq = Math.max(
    maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq),
    maxNonNegativeInteger(mapping.lastSeq),
    maxNonNegativeInteger(eventsResponse?.traceLastSeq)
  );
  const sourceLatestSeq = Math.max(
    maxNonNegativeInteger(previous.sourceLatestSeq, previous.upstreamLatestSeq),
    maxNonNegativeInteger(eventsResponse?.maxSeq),
    lastSourceSeq
  );
  const nextResultSyncState = textValue(resultSyncState ?? previous.resultSyncState) || "not_started";
  const nextProjectionStatus = textValue(projectionStatus) || inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState: nextResultSyncState, previous });
  const nextProjectionHealth = textValue(projectionHealth) || (error ? "degraded" : "healthy");
  return {
    ...previous,
    traceId,
    sessionId: textValue(previous.sessionId ?? result?.sessionId ?? mapping.sessionId) || null,
    conversationId: textValue(previous.conversationId ?? result?.conversationId ?? mapping.conversationId) || null,
    threadId: textValue(previous.threadId ?? result?.threadId ?? mapping.threadId) || null,
    ownerUserId: textValue(previous.ownerUserId ?? result?.ownerUserId) || null,
    ownerRole: textValue(previous.ownerRole ?? result?.ownerRole) || null,
    managerUrl: textValue(previous.managerUrl ?? mapping.managerUrl) || null,
    backendProfile: textValue(previous.backendProfile ?? mapping.backendProfile) || null,
    providerId: textValue(previous.providerId ?? mapping.providerId) || null,
    sourceRunId: runId,
    sourceCommandId: commandId,
    runId,
    commandId,
    lastSourceSeq,
    lastAgentRunSeq: lastSourceSeq,
    lastProjectedSeq: Math.max(maxNonNegativeInteger(previous.lastProjectedSeq), lastSourceSeq),
    sourceLatestSeq,
    upstreamLatestSeq: sourceLatestSeq,
    projectionStatus: nextProjectionStatus,
    projectionHealth: nextProjectionHealth,
    resultSyncState: nextResultSyncState,
    lastProjectedAt: lastSourceSeq > maxNonNegativeInteger(previous.lastSourceSeq, previous.lastAgentRunSeq) ? timestamp : previous.lastProjectedAt ?? timestamp,
    lastResultSyncAt: nextResultSyncState === "synced" ? timestamp : previous.lastResultSyncAt ?? null,
    lastErrorCode: error ? textValue(error.code ?? error.errorCode ?? "projection_sync_failed") : null,
    lastErrorMessage: error ? textValue(error.message ?? "Projection sync failed.") : null,
    failureCount: error ? maxNonNegativeInteger(previous.failureCount) + 1 : 0,
    nextRetryAt: error ? previous.nextRetryAt ?? null : null,
    createdAt: previous.createdAt ?? timestamp,
    updatedAt: timestamp,
    valuesPrinted: false
  };
}

export function agentRunResultSyncDeferred({ forceResultSync = false, options = {}, env = process.env } = {}) {
  if (forceResultSync) return false;
  if (options.deferAgentRunResultSync === true) return true;
  const configured = String(env?.HWLAB_CODE_AGENT_AGENTRUN_RESULT_SYNC_IN_RUNNING ?? "").trim().toLowerCase();
  return ["0", "false", "off", "no"].includes(configured);
}

function normalizeProjectionState(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  return value;
}

function normalizeAgentRunMapping(value) {
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function inferProjectionStatus({ lastSourceSeq, sourceLatestSeq, resultSyncState, previous }) {
  if (["pending", "timed_out", "failed"].includes(resultSyncState)) return "terminal";
  if (sourceLatestSeq > lastSourceSeq) return "projecting";
  return textValue(previous?.projectionStatus) || "projecting";
}

function boundedPageLimit(value) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_AGENTRUN_EVENTS_PAGE_LIMIT;
  return Math.min(parsed, MAX_AGENTRUN_EVENTS_PAGE_LIMIT);
}

function maxNonNegativeInteger(...values) {
  let max = 0;
  for (const value of values) {
    const parsed = Number.parseInt(String(value ?? ""), 10);
    if (Number.isInteger(parsed) && parsed >= 0 && parsed > max) max = parsed;
  }
  return max;
}

function timestampValue(now) {
  const value = typeof now === "function" ? now() : now;
  const parsed = Date.parse(String(value ?? ""));
  return Number.isFinite(parsed) ? new Date(parsed).toISOString() : new Date().toISOString();
}

function textValue(value) {
  return String(value ?? "").trim();
}
