/*
 * SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-zero-repair; PJ2026-010403 API契约 draft-2026-06-18-r1.
 * 职责: Workbench 空消息 session 后台生命周期 GC。只调用服务端 session lifecycle mutation，不在 GET/read model/Web 中做 cleanup 或 repair。
 */

export const DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS = 10 * 60 * 1000;
export const DEFAULT_WORKBENCH_EMPTY_SESSION_GC_INTERVAL_MS = 60 * 1000;
export const DEFAULT_WORKBENCH_EMPTY_SESSION_GC_INITIAL_DELAY_MS = 5 * 1000;
export const DEFAULT_WORKBENCH_EMPTY_SESSION_GC_BATCH_SIZE = 100;
export const DEFAULT_WORKBENCH_EMPTY_SESSION_GC_JITTER_MS = 15 * 1000;
export const DEFAULT_WORKBENCH_EMPTY_SESSION_GC_FAILURE_BACKOFF_MS = 2 * 60 * 1000;

const RUNNING_STATUSES = new Set(["active", "accepted", "busy", "creating", "dispatching", "pending", "queued", "running", "streaming"]);
const TERMINAL_RESULT_STATUSES = new Set(["blocked", "canceled", "cancelled", "completed", "failed", "timeout"]);

export function startWorkbenchEmptySessionGc(options: any = {}) {
  const env = options.env ?? process.env;
  const store = options.store ?? options.accessController?.store ?? null;
  const enabled = envFlag(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_ENABLED, true);
  const ttlMs = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_TTL_MS, DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS, { min: 60_000, max: 86_400_000 });
  const intervalMs = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_INTERVAL_MS, DEFAULT_WORKBENCH_EMPTY_SESSION_GC_INTERVAL_MS, { min: 1_000, max: 3_600_000 });
  const initialDelayMs = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_INITIAL_DELAY_MS, DEFAULT_WORKBENCH_EMPTY_SESSION_GC_INITIAL_DELAY_MS, { min: 0, max: 3_600_000 });
  const batchSize = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_BATCH_SIZE, DEFAULT_WORKBENCH_EMPTY_SESSION_GC_BATCH_SIZE, { min: 1, max: 100 });
  const jitterMs = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_JITTER_MS, DEFAULT_WORKBENCH_EMPTY_SESSION_GC_JITTER_MS, { min: 0, max: intervalMs });
  const failureBackoffMs = boundedInteger(env.HWLAB_WORKBENCH_EMPTY_SESSION_GC_FAILURE_BACKOFF_MS, DEFAULT_WORKBENCH_EMPTY_SESSION_GC_FAILURE_BACKOFF_MS, { min: intervalMs, max: 3_600_000 });
  const canMutate = typeof store?.listAgentSessionsForUser === "function" && typeof store?.archiveAgentConversation === "function";
  if (!enabled || !canMutate) {
    return disabledGcHandle({ enabled, reason: enabled ? "session_store_missing_lifecycle_mutation" : "disabled", ttlMs, intervalMs, initialDelayMs, batchSize, jitterMs, failureBackoffMs });
  }

  let stopped = false;
  let running = false;
  let timer: ReturnType<typeof setTimeout> | null = null;
  let nextOffset = 0;
  const tick = async (reason = "manual") => {
    if (stopped) return { ok: false, status: "stopped", reason };
    if (running) return { ok: true, status: "skipped", reason: "already_running" };
    running = true;
    const offset = nextOffset;
    try {
      const result = await runWorkbenchEmptySessionGc({ store, ttlMs, batchSize, offset, now: options.now, reason });
      if (result?.ok) nextOffset = result.scanned >= batchSize ? offset + batchSize : 0;
      logGcResult(options.logger, result);
      return result;
    } catch (error) {
      const result = { ok: false, status: "failed", reason, offset, error: error instanceof Error ? error.message : String(error), valuesRedacted: true };
      logGcResult(options.logger, result);
      return result;
    } finally {
      running = false;
    }
  };

  const schedule = (delayMs: number, reason: string) => {
    if (stopped) return;
    const actualDelayMs = Math.max(0, delayMs) + randomJitterMs(jitterMs);
    timer = setTimeout(() => {
      timer = null;
      void tick(reason).then((result: any) => {
        if (stopped) return;
        const failed = result?.ok === false || result?.status === "failed";
        schedule(failed ? failureBackoffMs : intervalMs, failed ? "failure_backoff" : "interval");
      });
    }, actualDelayMs);
    timer.unref?.();
  };

  schedule(initialDelayMs, "startup");

  return {
    enabled: true,
    status: "running",
    ttlMs,
    intervalMs,
    initialDelayMs,
    batchSize,
    jitterMs,
    failureBackoffMs,
    tick,
    stop() {
      stopped = true;
      if (timer) clearTimeout(timer);
      timer = null;
    }
  };
}

export async function runWorkbenchEmptySessionGc({ store, ttlMs = DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS, batchSize = DEFAULT_WORKBENCH_EMPTY_SESSION_GC_BATCH_SIZE, offset = 0, now, reason = "manual" }: any = {}) {
  if (typeof store?.listAgentSessionsForUser !== "function" || typeof store?.archiveAgentConversation !== "function") {
    return { ok: false, status: "blocked", reason: "session_store_missing_lifecycle_mutation", ttlMs, batchSize, valuesRedacted: true };
  }
  const observedAt = timestampFor(now);
  const nowMs = Date.parse(observedAt);
  const sessions = await store.listAgentSessionsForUser({
    ownerScoped: false,
    actorRole: "admin",
    includeArchived: false,
    limit: batchSize,
    offset
  }) ?? [];
  const keptByReason: Record<string, number> = {};
  const archivedSessionIds: string[] = [];
  for (const session of sessions) {
    const decision = classifyWorkbenchEmptySessionGcCandidate(session, { nowMs, ttlMs });
    if (!decision.eligible) {
      keptByReason[decision.reason] = (keptByReason[decision.reason] ?? 0) + 1;
      continue;
    }
    const archived = await store.archiveAgentConversation({
      actorRole: "admin",
      ownerScoped: false,
      sessionId: session.id,
      now: observedAt
    }) ?? { count: 0 };
    if (Number(archived.count ?? 0) > 0) archivedSessionIds.push(session.id);
  }
  return {
    ok: true,
    status: "completed",
    reason,
    observedAt,
    cutoffAt: new Date(nowMs - ttlMs).toISOString(),
    ttlMs,
    batchSize,
    offset,
    scanned: sessions.length,
    archived: archivedSessionIds.length,
    archivedSessionIds,
    keptByReason,
    valuesRedacted: true
  };
}

export function classifyWorkbenchEmptySessionGcCandidate(session: any, { nowMs = Date.now(), ttlMs = DEFAULT_WORKBENCH_EMPTY_SESSION_TTL_MS } = {}) {
  if (!session?.id || typeof session.id !== "string") return { eligible: false, reason: "invalid_session" };
  const snapshot = objectValue(session.session);
  const status = normalizeStatus(session.status ?? snapshot.sessionStatus ?? snapshot.status);
  if (status === "archived") return { eligible: false, reason: "already_archived" };
  if (RUNNING_STATUSES.has(status)) return { eligible: false, reason: "running_or_admitting" };
  if (TERMINAL_RESULT_STATUSES.has(status)) return { eligible: false, reason: "terminal_result_status" };
  if (sessionMessageCount(session, snapshot) > 0) return { eligible: false, reason: "has_messages" };
  if (hasTraceOrTerminalEvidence(session, snapshot)) return { eligible: false, reason: "has_trace_or_terminal_evidence" };
  const activityMs = lastActivityMs(session, snapshot);
  if (!Number.isFinite(activityMs)) return { eligible: false, reason: "missing_activity_timestamp" };
  if (nowMs - activityMs < ttlMs) return { eligible: false, reason: "ttl_not_elapsed" };
  return { eligible: true, reason: "stale_empty_session", activityAt: new Date(activityMs).toISOString() };
}

function sessionMessageCount(session: any, snapshot: Record<string, any>) {
  for (const messages of [snapshot.messages, snapshot.chatMessages, session.messages, session.chatMessages]) {
    if (Array.isArray(messages) && messages.length > 0) return messages.length;
  }
  for (const value of [snapshot.messageCount, session.messageCount]) {
    const count = Number(value);
    if (Number.isFinite(count) && count > 0) return Math.trunc(count);
  }
  return 0;
}

function hasTraceOrTerminalEvidence(session: any, snapshot: Record<string, any>) {
  if (text(session.lastTraceId) || text(snapshot.lastTraceId) || text(snapshot.currentTraceId) || text(snapshot.traceId)) return true;
  if (objectHasKeys(snapshot.traceResults) || objectHasKeys(snapshot.traceResult)) return true;
  return Boolean(snapshot.finalResponse || snapshot.traceSummary || snapshot.agentRun || snapshot.terminalEvidence);
}

function lastActivityMs(session: any, snapshot: Record<string, any>) {
  const values = [
    session.updatedAt,
    snapshot.updatedAt,
    snapshot.lastTouchedAt,
    snapshot.lastActivityAt,
    session.startedAt,
    snapshot.startedAt,
    snapshot.createdAt
  ].map((value) => Date.parse(String(value ?? ""))).filter(Number.isFinite);
  return values.length > 0 ? Math.max(...values) : NaN;
}

function envFlag(value: unknown, fallback: boolean) {
  const textValue = text(value).toLowerCase();
  if (!textValue) return fallback;
  if (["0", "false", "no", "off", "disabled"].includes(textValue)) return false;
  if (["1", "true", "yes", "on", "enabled"].includes(textValue)) return true;
  return fallback;
}

function boundedInteger(value: unknown, fallback: number, { min, max }: { min: number; max: number }) {
  const parsed = Number.parseInt(String(value ?? ""), 10);
  const candidate = Number.isInteger(parsed) ? parsed : fallback;
  return Math.min(Math.max(candidate, min), max);
}

function randomJitterMs(maxJitterMs: number) {
  return maxJitterMs > 0 ? Math.floor(Math.random() * (maxJitterMs + 1)) : 0;
}

function timestampFor(now: unknown) {
  const value = typeof now === "function" ? (now as () => unknown)() : now;
  if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString();
  if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return value;
  return new Date().toISOString();
}

function normalizeStatus(value: unknown) {
  const status = text(value).toLowerCase().replace(/_/gu, "-");
  return status === "cancelled" ? "canceled" : status;
}

function objectValue(value: unknown): Record<string, any> {
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
}

function objectHasKeys(value: unknown) {
  return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
}

function text(value: unknown) {
  return String(value ?? "").trim();
}

function disabledGcHandle(meta: any) {
  return { ...meta, status: "disabled", tick: async () => ({ ok: false, status: "disabled", ...meta }), stop() {} };
}

function logGcResult(logger: any, result: any) {
  if (!logger || result?.archived <= 0 && result?.status !== "failed") return;
  const message = JSON.stringify({ event: "workbench_empty_session_gc", ...result });
  if (result?.status === "failed" && typeof logger.error === "function") logger.error(message);
  else if (typeof logger.info === "function") logger.info(message);
}
