// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. WebProbe observe action handlers.
// Responsibility: start/status/command/stop/gc remote action execution and child JSON recovery.
import { randomBytes } from "node:crypto";
import { resolveCliChildJsonCommandResult, type CliChildJsonResolution } from "../cli-child-json-recovery";
import type { HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
import { evaluateWebProbeMemoryGuard, runWebProbeMemoryGuard } from "../hwlab-node-web-probe-memory-guard";
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
import { withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
import type { RenderedCliResult } from "../output";

import type { BootstrapAdminPasswordMaterial, NodeWebProbeObserveOptions, RuntimeSecretSpec } from "./entry";
import { runTransWorkspaceStdinScript } from "./public-exposure";
import { compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, record, shellQuote } from "./utils";
import { nodeWebProbeAlertThresholds, nodeWebProbeAuthLoginConfig, nodeWebProbeBrowserFreezePolicy, nodeWebProbeHostProxyEnv, nodeWebProbeProjectManagementConfig, webObserveGcDefaultKeepHours, webProbeAccountEnvAssignments } from "./web-probe-observe-action-config";
import { nodeWebObserveResolveStateDirShell, renderWebObserveStartResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveShortcuts } from "./web-observe-render";
import { commandSummaryForOutput, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";

type WebObserveActionJsonContract = "gc" | "start" | "status" | "command" | "force-stop";

export function nodeWebProbeRealtimeFanoutProfiles(spec: HwlabRuntimeLaneSpec): Record<string, Record<string, unknown>> {
  const profiles = spec.webProbe?.realtimeFanoutProfiles ?? {};
  if (Object.keys(profiles).length === 0) return {};
  const kafka = spec.codeAgentRuntime?.kafkaEventBridge;
  if (kafka === undefined) throw new Error("realtimeFanoutProfiles requires codeAgentRuntime.kafkaEventBridge in the owning YAML");
  const capabilities = {
    transactionalProjector: kafka.enabled && kafka.features.transactionalProjector,
    projectionOutboxRelay: kafka.enabled && kafka.features.projectionOutboxRelay,
    projectionRealtime: kafka.enabled && kafka.features.projectionRealtime,
  };
  return Object.fromEntries(Object.entries(profiles).map(([id, profile]) => {
    if (profile.businessEvent !== kafka.hwlabEventTopic) {
      throw new Error(`realtimeFanoutProfiles.${id}.businessEvent must match codeAgentRuntime.kafkaEventBridge.hwlabEventTopic`);
    }
    return [id, {
      ...profile,
      expectedKafka: {
        enabled: kafka.enabled,
        topics: {
          stdio: kafka.stdioTopic,
          agentrun: kafka.agentRunEventTopic,
          hwlab: kafka.hwlabEventTopic,
        },
        groups: {
          transactionalProjector: kafka.transactionalProjectorConsumerGroupId,
          liveSse: kafka.hwlabEventConsumerGroupId,
        },
        capabilities,
        valuesRedacted: true,
      },
    }];
  }));
}

export function nodeWebProbeWorkbenchKafkaDebugReplay(spec: HwlabRuntimeLaneSpec): Record<string, unknown> | null {
  const config = spec.webProbe?.workbenchKafkaDebugReplay;
  if (config === undefined) return null;
  const pureKafkaLive = spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"];
  if (pureKafkaLive === undefined) {
    throw new Error("workbenchKafkaDebugReplay requires the pure-kafka-live realtimeFanoutProfile in the owning YAML");
  }
  return {
    ...config,
    productEventsPath: pureKafkaLive.productEventsPath,
    debugEventsPath: pureKafkaLive.debugEventsPath,
    requestObservationQuietMs: pureKafkaLive.reconnectQuietMs,
    valuesRedacted: true,
  };
}

export function nodeWebProbeWorkbenchTraceReadability(spec: HwlabRuntimeLaneSpec): Record<string, unknown> | null {
  const config = spec.webProbe?.workbenchTraceReadability;
  if (config === undefined) return null;
  return {
    ...config,
    valuesRedacted: true,
  };
}

export function resolveWebObserveActionJson(
  result: { stdout: string; stderr?: string; exitCode?: number | null; timedOut?: boolean },
  contract: WebObserveActionJsonContract,
): CliChildJsonResolution {
  return resolveCliChildJsonCommandResult({
    result,
    requestedStdoutType: `web-probe observe ${contract} contract`,
    acceptParsed: (value) => isWebObserveActionJsonContract(value, contract),
  });
}

function isWebObserveActionJsonContract(value: Record<string, unknown>, contract: WebObserveActionJsonContract): boolean {
  if (acceptFailureContract(value)) return true;
  if (contract === "gc") {
    return value.command === "web-probe observe gc" && hasAnyKey(value, ["mode", "stateRoot", "scannedRuns", "candidateCount", "selectedCount", "protectedCount"]);
  }
  if (contract === "start") {
    return value.command === "web-probe-observe start" && hasAnyKey(value, ["jobId", "stateDir", "pid", "manifestPath", "heartbeat", "manifest"]);
  }
  if (contract === "status") {
    return hasAnyKey(value, ["jobId", "stateDir", "manifest", "heartbeat", "diagnostics", "commands", "samples", "status"]);
  }
  if (contract === "command") {
    return hasAnyKey(value, ["ok", "queued", "waitTimedOut", "commandId", "status", "path", "error", "reason"]);
  }
  return hasAnyKey(value, ["ok", "forceStopped", "killed", "status", "pid", "reason", "error", "commandId"]);
}

function acceptFailureContract(value: Record<string, unknown>): boolean {
  return value.ok === false && hasAnyKey(value, ["error", "reason", "degradedReason", "status", "diagnostics", "result", "command"]);
}

function hasAnyKey(value: Record<string, unknown>, keys: string[]): boolean {
  return keys.some((key) => value[key] !== undefined && value[key] !== null);
}

export function runNodeWebProbeObserveGc(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
  const stateRoot = `.state/web-observe/${safeWebObserveSegment(options.node)}/${safeWebObserveSegment(options.lane)}`;
  const script = [
    "set -eu",
    `node - ${shellQuote(stateRoot)} ${shellQuote(String(options.gcKeepHours))} ${shellQuote(String(options.gcLimit))} ${shellQuote(options.confirm ? "run" : "plan")} ${shellQuote(options.node)} ${shellQuote(options.lane)} <<'UNIDESK_WEB_OBSERVE_GC'`,
    nodeWebObserveGcNodeScript(),
    "UNIDESK_WEB_OBSERVE_GC",
  ].join("\n");
  const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
  const payloadResolution = resolveWebObserveActionJson(result, "gc");
  const payload = payloadResolution.parsed ?? {};
  const ok = result.exitCode === 0 && payloadResolution.parsed !== null && payload.ok !== false;
  return {
    ok,
    status: ok ? (options.confirm ? "cleaned" : "planned") : "blocked",
    command: `web-probe observe gc --node ${options.node} --lane ${options.lane}`,
    node: options.node,
    lane: options.lane,
    workspace: spec.workspace,
    stateRoot,
    mode: options.confirm ? "confirmed-run" : "dry-run",
    retention: {
      source: "config/unidesk-cli.yaml#gc.stateStaleScratch.keepHours",
      keepHours: options.gcKeepHours,
      userOverride: options.gcKeepHours !== webObserveGcDefaultKeepHours(),
    },
    gc: options.full || options.raw ? payload : compactWebObserveGcPayload(payload),
    stdoutJson: payloadResolution.diagnostics,
    result: ok
      ? {
        exitCode: result.exitCode,
        timedOut: result.timedOut,
        stdoutBytes: result.stdout.length,
        stderrBytes: result.stderr.length,
      }
      : compactCommandResultWithStdoutTail(result),
    valuesRedacted: true,
  };
}

function compactWebObserveGcPayload(payload: Record<string, unknown>): Record<string, unknown> {
  const candidates = Array.isArray(payload.candidates) ? payload.candidates.map(record) : [];
  const protectedRuns = Array.isArray(payload.protected) ? payload.protected.map(record) : [];
  const deleted = Array.isArray(payload.deleted) ? payload.deleted.map(record) : [];
  const failures = Array.isArray(payload.failures) ? payload.failures.map(record) : [];
  return {
    ok: payload.ok ?? null,
    mode: payload.mode ?? null,
    mutation: payload.mutation ?? null,
    keepHours: payload.keepHours ?? null,
    limit: payload.limit ?? null,
    diskBefore: payload.diskBefore ?? null,
    diskAfter: payload.diskAfter ?? null,
    scannedRuns: payload.scannedRuns ?? null,
    candidateCount: payload.candidateCount ?? null,
    selectedCount: payload.selectedCount ?? null,
    deferredCount: payload.deferredCount ?? null,
    protectedCount: payload.protectedCount ?? null,
    estimatedReclaimBytes: payload.estimatedReclaimBytes ?? null,
    estimatedReclaimHuman: payload.estimatedReclaimHuman ?? null,
    reclaimedBytes: payload.reclaimedBytes ?? null,
    reclaimedHuman: payload.reclaimedHuman ?? null,
    candidates: candidates.slice(0, 20).map((item) => ({
      id: item.id ?? null,
      runDir: item.runDir ?? null,
      status: item.status ?? null,
      ageHours: item.ageHours ?? null,
      rawHuman: item.rawHuman ?? null,
      artifactCount: item.artifactCount ?? null,
    })),
    deleted: deleted.slice(0, 20).map((item) => ({
      id: item.id ?? null,
      runDir: item.runDir ?? null,
      rawHuman: item.rawHuman ?? null,
      artifactCount: item.artifactCount ?? null,
    })),
    failures: failures.slice(0, 20),
    protected: protectedRuns.slice(0, 20).map((item) => ({
      id: item.id ?? null,
      runDir: item.runDir ?? null,
      status: item.status ?? null,
      ageHours: item.ageHours ?? null,
      rawHuman: item.rawHuman ?? null,
      reasons: item.reasons ?? null,
    })),
    disclosure: {
      candidateRowsShown: Math.min(20, candidates.length),
      deletedRowsShown: Math.min(20, deleted.length),
      protectedRowsShown: Math.min(20, protectedRuns.length),
      full: "rerun with --full or --raw for all candidates/artifacts",
    },
    valuesRedacted: true,
  };
}

function nodeWebObserveGcNodeScript(): string {
  return String.raw`
const fs = require("fs");
const path = require("path");
const childProcess = require("child_process");

const stateRoot = process.argv[2];
const keepHours = Number(process.argv[3]);
const limit = Number(process.argv[4]);
const mode = process.argv[5] === "run" ? "run" : "plan";
const nodeId = process.argv[6];
const lane = process.argv[7];
const keepMs = keepHours * 60 * 60 * 1000;
const nowMs = Date.now();
const rawNames = ["samples.jsonl", "browser-process.jsonl", "network.jsonl", "console.jsonl", "artifacts.jsonl", "performance-events.jsonl", "screenshots", "performance"];

function jsonRead(file) {
  try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
}

function parseTime(value) {
  if (typeof value !== "string") return 0;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? ms : 0;
}

function dirTimeMs(runDir) {
  const base = path.basename(runDir);
  const match = base.match(/^(\d{8})T(\d{6})Z_/);
  if (!match) return 0;
  const raw = match[1].slice(0, 4) + "-" + match[1].slice(4, 6) + "-" + match[1].slice(6, 8) + "T" + match[2].slice(0, 2) + ":" + match[2].slice(2, 4) + ":" + match[2].slice(4, 6) + "Z";
  return Date.parse(raw) || 0;
}

function newestKnownTimeMs(runDir, manifest, heartbeat) {
  return Math.max(
    dirTimeMs(runDir),
    parseTime(manifest && manifest.completedAt),
    parseTime(manifest && manifest.updatedAt),
    parseTime(manifest && manifest.startedAt),
    parseTime(manifest && manifest.pageProvenance && manifest.pageProvenance.observedAt),
    parseTime(heartbeat && heartbeat.completedAt),
    parseTime(heartbeat && heartbeat.forceStoppedAt),
    parseTime(heartbeat && heartbeat.updatedAt),
    parseTime(heartbeat && heartbeat.pageProvenance && heartbeat.pageProvenance.observedAt),
  );
}

function sizePath(target) {
  let total = 0;
  const stack = [target];
  while (stack.length > 0) {
    const current = stack.pop();
    let st;
    try { st = fs.lstatSync(current); } catch { continue; }
    if (st.isSymbolicLink()) continue;
    total += st.blocks ? st.blocks * 512 : st.size;
    if (!st.isDirectory()) continue;
    let entries;
    try { entries = fs.readdirSync(current); } catch { continue; }
    for (const entry of entries) stack.push(path.join(current, entry));
  }
  return total;
}

function human(bytes) {
  const units = ["B", "KiB", "MiB", "GiB", "TiB"];
  let value = bytes;
  let unit = 0;
  while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; }
  return value.toFixed(unit === 0 ? 0 : 1) + units[unit];
}

function disk() {
  try {
    const line = childProcess.execFileSync("df", ["-Pk", "/"], { encoding: "utf8" }).trim().split("\n").slice(-1)[0];
    const parts = line.trim().split(/\s+/);
    const sizeBytes = Number(parts[1]) * 1024;
    const usedBytes = Number(parts[2]) * 1024;
    const availableBytes = Number(parts[3]) * 1024;
    const usePercent = Number(String(parts[4]).replace("%", ""));
    return { filesystem: parts[0], sizeBytes, usedBytes, availableBytes, usePercent, usedHuman: human(usedBytes), availableHuman: human(availableBytes) };
  } catch (error) {
    return { error: error instanceof Error ? error.message : String(error) };
  }
}

function findRunDirs(root) {
  const runs = [];
  for (const year of safeReadDir(root)) {
    const y = path.join(root, year.name);
    if (!year.isDirectory() || !/^\d{4}$/.test(year.name)) continue;
    for (const month of safeReadDir(y)) {
      const m = path.join(y, month.name);
      if (!month.isDirectory() || !/^\d{2}$/.test(month.name)) continue;
      for (const day of safeReadDir(m)) {
        const d = path.join(m, day.name);
        if (!day.isDirectory() || !/^\d{2}$/.test(day.name)) continue;
        for (const run of safeReadDir(d)) {
          const r = path.join(d, run.name);
          if (run.isDirectory() && /_webobs-[A-Za-z0-9_.-]+$/.test(run.name)) runs.push(r);
        }
      }
    }
  }
  return runs;
}

function safeReadDir(dir) {
  try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
}

function pidObserverAlive(pid, runAbs) {
  if (!Number.isInteger(pid) || pid <= 1) return { alive: false, reason: null };
  try {
    const cmdline = fs.readFileSync("/proc/" + pid + "/cmdline", "utf8").replace(/\0/g, " ");
    if (cmdline.includes("observer-runner") || cmdline.includes(runAbs)) return { alive: true, reason: "pid-alive" };
    return { alive: false, reason: null };
  } catch (error) {
    if (fs.existsSync("/proc/" + pid)) return { alive: true, reason: "pid-alive-unreadable" };
    return { alive: false, reason: null };
  }
}

function openRunDirs(rootAbs) {
  const active = new Set();
  const procEntries = safeReadDir("/proc").filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name));
  for (const proc of procEntries) {
    const fdDir = path.join("/proc", proc.name, "fd");
    for (const fd of safeReadDir(fdDir)) {
      let target;
      try { target = fs.readlinkSync(path.join(fdDir, fd.name)).replace(/ \(deleted\)$/, ""); } catch { continue; }
      if (!target.startsWith(rootAbs + path.sep)) continue;
      const rel = path.relative(rootAbs, target).split(path.sep);
      if (rel.length >= 4) active.add(path.join(rootAbs, rel[0], rel[1], rel[2], rel[3]));
    }
  }
  return active;
}

function rawArtifacts(runDir) {
  const artifacts = [];
  for (const name of rawNames) {
    const target = path.join(runDir, name);
    if (!fs.existsSync(target)) continue;
    const bytes = sizePath(target);
    if (bytes > 0) artifacts.push({ name, path: target, bytes, human: human(bytes) });
  }
  return artifacts;
}

function removeArtifact(artifact) {
  const st = fs.lstatSync(artifact.path);
  if (st.isSymbolicLink()) throw new Error("refusing symlink: " + artifact.path);
  if (st.isDirectory()) fs.rmSync(artifact.path, { recursive: true, force: false });
  else fs.unlinkSync(artifact.path);
}

const rootAbs = path.resolve(stateRoot);
const diskBefore = disk();
const openDirs = openRunDirs(rootAbs);
const candidates = [];
const protectedRuns = [];

for (const runDir of findRunDirs(rootAbs)) {
  const manifest = jsonRead(path.join(runDir, "manifest.json"));
  const heartbeat = jsonRead(path.join(runDir, "heartbeat.json"));
  const pid = Number((heartbeat && heartbeat.pid) || (manifest && manifest.pid) || 0);
  const pidState = pidObserverAlive(pid, runDir);
  const hasOpenFd = openDirs.has(runDir);
  const newestMs = newestKnownTimeMs(runDir, manifest, heartbeat);
  const ageMs = newestMs > 0 ? nowMs - newestMs : 0;
  const artifacts = rawArtifacts(runDir);
  const rawBytes = artifacts.reduce((sum, item) => sum + item.bytes, 0);
  const rel = path.relative(rootAbs, runDir);
  const reasons = [];
  if (!manifest) reasons.push("manifest-missing");
  if (pidState.alive) reasons.push(pidState.reason || "pid-alive");
  if (hasOpenFd) reasons.push("open-fd");
  if (newestMs <= 0) reasons.push("time-unknown");
  if (ageMs < keepMs) reasons.push("retention-window");
  if (rawBytes <= 0) reasons.push("no-raw-artifacts");
  const summary = {
    id: (manifest && manifest.jobId) || path.basename(runDir).match(/(webobs-[A-Za-z0-9_.-]+)$/)?.[1] || null,
    runDir: rel,
    status: heartbeat && heartbeat.status || null,
    pid: Number.isInteger(pid) && pid > 0 ? pid : null,
    newestAt: newestMs > 0 ? new Date(newestMs).toISOString() : null,
    ageHours: newestMs > 0 ? Number((ageMs / 3600000).toFixed(2)) : null,
    rawBytes,
    rawHuman: human(rawBytes),
    artifacts,
  };
  if (reasons.length > 0) protectedRuns.push({ ...summary, reasons });
  else candidates.push(summary);
}

candidates.sort((a, b) => b.rawBytes - a.rawBytes);
const selected = candidates.slice(0, limit);
const selectedSet = new Set(selected.map((item) => item.runDir));
const deferred = candidates.filter((item) => !selectedSet.has(item.runDir));
let reclaimedBytes = 0;
const deleted = [];
const failures = [];

if (mode === "run") {
  for (const item of selected) {
    const runDir = path.join(rootAbs, item.runDir);
    try {
      for (const artifact of item.artifacts) removeArtifact(artifact);
      reclaimedBytes += item.rawBytes;
      deleted.push({ id: item.id, runDir: item.runDir, rawBytes: item.rawBytes, rawHuman: item.rawHuman, artifactCount: item.artifacts.length });
    } catch (error) {
      failures.push({ id: item.id, runDir: item.runDir, error: error instanceof Error ? error.message : String(error) });
    }
  }
}

const estimatedReclaimBytes = selected.reduce((sum, item) => sum + item.rawBytes, 0);
const diskAfter = mode === "run" ? disk() : null;
console.log(JSON.stringify({
  ok: failures.length === 0,
  command: "web-probe observe gc",
  node: nodeId,
  lane,
  stateRoot,
  mode,
  mutation: mode === "run",
  keepHours,
  limit,
  diskBefore,
  diskAfter,
  scannedRuns: candidates.length + protectedRuns.length,
  candidateCount: candidates.length,
  selectedCount: selected.length,
  deferredCount: deferred.length,
  protectedCount: protectedRuns.length,
  estimatedReclaimBytes,
  estimatedReclaimHuman: human(estimatedReclaimBytes),
  reclaimedBytes,
  reclaimedHuman: human(reclaimedBytes),
  candidates: selected.map((item) => ({ id: item.id, runDir: item.runDir, status: item.status, ageHours: item.ageHours, rawBytes: item.rawBytes, rawHuman: item.rawHuman, artifactCount: item.artifacts.length, artifacts: item.artifacts.map((artifact) => ({ name: artifact.name, bytes: artifact.bytes, human: artifact.human })) })),
  deleted,
  failures,
  protected: protectedRuns.slice(0, Math.min(50, protectedRuns.length)).map((item) => ({ id: item.id, runDir: item.runDir, status: item.status, ageHours: item.ageHours, rawBytes: item.rawBytes, rawHuman: item.rawHuman, reasons: item.reasons })),
  valuesRedacted: true,
}, null, 2));
`;
}

export function nodeWebObserveFinalLaunchGuardSource(): string {
  return String.raw`
const fs = require("fs");
const thresholdBytes = Number(process.argv[2]);
const comparator = process.argv[3];
const mode = process.argv[4];
const jobId = process.argv[5];
const stateDir = process.argv[6];
const source = "/proc/meminfo";

function finish(reason, availableBytes) {
  const valid = Number.isSafeInteger(availableBytes) && availableBytes >= 0;
  const thresholdMatched = valid
    ? comparator === "less-than" ? availableBytes < thresholdBytes : availableBytes <= thresholdBytes
    : null;
  const blocked = reason !== null || thresholdMatched === true;
  const cadenceSkip = blocked && mode === "sentinel-cadence";
  const payload = {
    ok: !blocked || cadenceSkip,
    command: "web-probe-observe start",
    status: blocked ? cadenceSkip ? "skipped-wait-next-round" : "blocked" : "allowed",
    reason: reason || (thresholdMatched ? "memavailable-threshold-matched" : null),
    failureKind: blocked ? "web-probe-final-memory-start-guard" : null,
    jobId,
    stateDir,
    observerCreated: false,
    browserCreated: false,
    startBlocked: blocked,
    decision: blocked ? cadenceSkip ? "skip-observer" : "gc-required" : "start-observer",
    mutation: blocked,
    stateArtifactCreated: blocked,
    memoryGuard: {
      phase: "remote-launch-critical-section",
      metric: "MemAvailable",
      source,
      swapExcluded: true,
      availableBytes: valid ? availableBytes : null,
      thresholdBytes,
      comparator,
      thresholdMatched,
    },
    observedAt: new Date().toISOString(),
    valuesRedacted: true,
  };
  console.log(JSON.stringify(payload));
  process.exit(blocked ? 42 : 0);
}

if (!Number.isSafeInteger(thresholdBytes) || thresholdBytes <= 0) finish("memory-threshold-invalid", null);
if (comparator !== "less-than" && comparator !== "less-than-or-equal") finish("memory-comparator-invalid", null);
let text;
try { text = fs.readFileSync(source, "utf8"); } catch { finish("proc-meminfo-read-failed", null); }
const match = text.match(/^MemAvailable:\s+(\d+)\s+kB$/m);
if (!match) finish("memavailable-missing", null);
const availableBytes = Number(match[1]) * 1024;
finish(null, availableBytes);
`;
}

export function nodeWebObserveLaunchReadinessSource(): string {
  return String.raw`
const fs = require("fs");
const runnerPid = Number(process.argv[2]);
const stateDir = process.argv[3];
const expectedJobId = process.argv[4];
const timeoutMs = Number(process.argv[5]);
const pollMs = Number(process.argv[6]);
const deadline = Date.now() + timeoutMs;
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
const readJson = (name) => { try { return JSON.parse(fs.readFileSync(stateDir + "/" + name, "utf8")); } catch { return null; } };

function procRows() {
  const rows = new Map();
  let names = [];
  try { names = fs.readdirSync("/proc").filter((name) => /^\d+$/.test(name)); } catch { return rows; }
  for (const name of names) {
    try {
      const stat = fs.readFileSync("/proc/" + name + "/stat", "utf8");
      const close = stat.lastIndexOf(")");
      const fields = stat.slice(close + 2).trim().split(/\s+/);
      const command = fs.readFileSync("/proc/" + name + "/cmdline").toString("utf8").replace(/\0/g, " ");
      rows.set(Number(name), { pid: Number(name), ppid: Number(fields[1]), command });
    } catch {}
  }
  return rows;
}

function descendants(rows) {
  const selected = new Set([runnerPid]);
  let changed = true;
  while (changed) {
    changed = false;
    for (const row of rows.values()) if (!selected.has(row.pid) && selected.has(row.ppid)) { selected.add(row.pid); changed = true; }
  }
  return [...selected].map((pid) => rows.get(pid)).filter(Boolean);
}

function matchingStateRecord(name, value) {
  if (!value || typeof value !== "object") return null;
  const status = String(value.status || "");
  if (!/^(starting|running|ready)$/i.test(status)) return null;
  if (String(value.jobId || "") !== expectedJobId || Number(value.pid) !== runnerPid) return null;
  return { name, status, jobId: String(value.jobId), pid: Number(value.pid) };
}

while (Date.now() <= deadline) {
  const rows = procRows();
  const runnerAlive = rows.has(runnerPid);
  const children = runnerAlive ? descendants(rows) : [];
  const browserChild = children.find((row) => row.pid !== runnerPid && /(?:^|[ /])(chrome|chromium)(?:[ /]|$)/i.test(row.command));
  const heartbeat = readJson("heartbeat.json");
  const manifest = readJson("manifest.json");
  const stateRecord = matchingStateRecord("heartbeat.json", heartbeat) || matchingStateRecord("manifest.json", manifest);
  if (runnerAlive && browserChild && stateRecord) {
    console.log(JSON.stringify({ ok: true, expectedJobId, runnerPid, runnerAlive: true, browserPid: browserChild.pid, stateRecord, readiness: "current-state-and-browser-descendant", observedAt: new Date().toISOString(), valuesRedacted: true }));
    process.exit(0);
  }
  if (!runnerAlive) {
    console.log(JSON.stringify({ ok: false, reason: "runner-exited-before-ready", expectedJobId, runnerPid, runnerAlive: false, stateRecord, observedAt: new Date().toISOString(), valuesRedacted: true }));
    process.exit(43);
  }
  pause(pollMs);
}
console.log(JSON.stringify({ ok: false, reason: "launch-readiness-timeout", expectedJobId, runnerPid, runnerAlive: fs.existsSync("/proc/" + runnerPid), observedAt: new Date().toISOString(), valuesRedacted: true }));
process.exit(44);
`;
}

export function runNodeWebProbeObserveStart(
  options: NodeWebProbeObserveOptions,
  spec: HwlabRuntimeLaneSpec,
  secretSpec: RuntimeSecretSpec,
  material: BootstrapAdminPasswordMaterial,
  credential: Record<string, unknown>,
): Record<string, unknown> | RenderedCliResult {
  const memoryGuard = runWebProbeMemoryGuard(spec, options.memoryGuardMode);
  if (memoryGuard.status !== "allowed") {
    const cadenceSkip = options.memoryGuardMode === "sentinel-cadence" && memoryGuard.status === "skipped";
    return {
      ok: cadenceSkip,
      status: cadenceSkip ? "skipped-wait-next-round" : "blocked",
      failureKind: "web-probe-memory-start-guard",
      command: `web-probe observe start --node ${options.node} --lane ${options.lane}`,
      node: options.node,
      lane: options.lane,
      workspace: spec.workspace,
      observerCreated: false,
      startBlocked: true,
      decision: cadenceSkip ? "skip-observer" : "gc-required",
      mutation: false,
      memoryGuard,
      valuesRedacted: true,
    };
  }
  const observerLifecycle = spec.webProbe?.resourcePolicy?.observerLifecycle;
  const finalMemoryPolicy = spec.webProbe?.resourcePolicy?.memoryStartGuard;
  if (observerLifecycle === undefined) throw new Error("web-probe observer lifecycle policy is missing from the owning YAML");
  if (finalMemoryPolicy === undefined) throw new Error("web-probe memory start policy is missing from the owning YAML");
  const jobId = `webobs-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
  const timestamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/[.]\d{3}Z$/u, "Z");
  const day = timestamp.slice(0, 8);
  const defaultStateDir = `.state/web-observe/${safeWebObserveSegment(options.node)}/${safeWebObserveSegment(options.lane)}/${day.slice(0, 4)}/${day.slice(4, 6)}/${day.slice(6, 8)}/${timestamp}_${safeWebObserveTargetSegment(options.targetPath)}_${jobId}`;
  const stateDir = options.stateDir ?? defaultStateDir;
  const runnerB64 = Buffer.from(nodeWebObserveRunnerSource(), "utf8").toString("base64");
  const runnerB64Body = runnerB64.match(/.{1,76}/gu)?.join("\n") ?? runnerB64;
  const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
  const alertThresholds = nodeWebProbeAlertThresholds(spec);
  const browserFreezePolicy = nodeWebProbeBrowserFreezePolicy(spec);
  const projectManagement = nodeWebProbeProjectManagementConfig(spec);
  const authLogin = nodeWebProbeAuthLoginConfig(spec);
  const runnerEnvAssignments = [
    ...webProbeProxy.envAssignments,
    ...webProbeAccountEnvAssignments(),
    ...(spec.webProbe?.playwrightBrowsersPath === undefined ? [] : [
      `PLAYWRIGHT_BROWSERS_PATH=${shellQuote(spec.webProbe.playwrightBrowsersPath)}`,
    ]),
    `HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
    `UNIDESK_WEB_OBSERVE_ORIGIN_NAME=${shellQuote(options.originName)}`,
    `UNIDESK_WEB_OBSERVE_ORIGIN_MODE=${shellQuote(options.originMode)}`,
    `UNIDESK_WEB_OBSERVE_ORIGIN_CONFIG_PATH=${shellQuote(options.originConfigPath ?? "")}`,
    `UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE_SOURCE=${shellQuote(options.browserProxyModeSource)}`,
    `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`,
    `HWLAB_WEB_PASS=${shellQuote(material.password)}`,
    `UNIDESK_WEB_OBSERVE_STATE_DIR=${shellQuote(stateDir)}`,
    `UNIDESK_WEB_OBSERVE_JOB_ID=${shellQuote(jobId)}`,
    `UNIDESK_WEB_OBSERVE_TARGET_PATH=${shellQuote(options.targetPath)}`,
    `UNIDESK_WEB_OBSERVE_SAMPLE_INTERVAL_MS=${shellQuote(String(options.sampleIntervalMs))}`,
    `UNIDESK_WEB_OBSERVE_SCREENSHOT_INTERVAL_MS=${shellQuote(String(options.screenshotIntervalMs))}`,
    `UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS=${shellQuote(String(options.observerRefreshIntervalMs))}`,
    `UNIDESK_WEB_OBSERVE_MAX_SAMPLES=${shellQuote(String(options.maxSamples))}`,
    `UNIDESK_WEB_OBSERVE_MAX_RUN_MS=${shellQuote(String(options.maxRunSeconds > 0 ? options.maxRunSeconds * 1000 : 0))}`,
    `UNIDESK_WEB_OBSERVE_MAX_SCREENSHOTS=${shellQuote(String(observerLifecycle.maxScreenshots))}`,
    `UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_ENTRIES=${shellQuote(String(observerLifecycle.maxPerformanceEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_BROWSER_PROCESS_HISTORY_ENTRIES=${shellQuote(String(observerLifecycle.maxBrowserProcessHistoryEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_BROWSER_FREEZE_SIGNAL_HISTORY_ENTRIES=${shellQuote(String(observerLifecycle.maxBrowserFreezeSignalHistoryEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_ENTRIES=${shellQuote(String(observerLifecycle.maxRealtimeEventEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_REALTIME_EVENT_BODY_BYTES=${shellQuote(String(observerLifecycle.maxRealtimeEventBodyBytes))}`,
    `UNIDESK_WEB_OBSERVE_MAX_DOM_EVIDENCE_ROWS=${shellQuote(String(observerLifecycle.maxDomEvidenceRows))}`,
    `UNIDESK_WEB_OBSERVE_MAX_PROCESS_SCAN_ENTRIES=${shellQuote(String(observerLifecycle.maxProcessScanEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_PERFORMANCE_CAPTURE_SECONDS=${shellQuote(String(observerLifecycle.maxPerformanceCaptureSeconds))}`,
    `UNIDESK_WEB_OBSERVE_RESPONSE_BODY_MAX_BYTES=${shellQuote(String(observerLifecycle.maxResponseBodyBytes))}`,
    `UNIDESK_WEB_OBSERVE_WEB_PERFORMANCE_BODY_MAX_BYTES=${shellQuote(String(observerLifecycle.maxWebPerformanceBodyBytes))}`,
    `UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_ENTRIES=${shellQuote(String(observerLifecycle.maxJsonlQueueEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_JSONL_QUEUE_BYTES=${shellQuote(String(observerLifecycle.maxJsonlQueueBytes))}`,
    `UNIDESK_WEB_OBSERVE_MAX_JSONL_LINE_BYTES=${shellQuote(String(observerLifecycle.maxJsonlLineBytes))}`,
    `UNIDESK_WEB_OBSERVE_JSONL_FLUSH_TIMEOUT_MS=${shellQuote(String(observerLifecycle.jsonlFlushTimeoutMs))}`,
    `UNIDESK_WEB_OBSERVE_MAX_RESPONSE_BODY_IN_FLIGHT=${shellQuote(String(observerLifecycle.maxResponseBodyInFlight))}`,
    `UNIDESK_WEB_OBSERVE_MAX_PASSIVE_LISTENER_TASKS=${shellQuote(String(observerLifecycle.maxPassiveListenerTasks))}`,
    `UNIDESK_WEB_OBSERVE_PASSIVE_TASK_FLUSH_TIMEOUT_MS=${shellQuote(String(observerLifecycle.passiveTaskFlushTimeoutMs))}`,
    `UNIDESK_WEB_OBSERVE_CLOSE_STEP_TIMEOUT_MS=${shellQuote(String(observerLifecycle.closeStepTimeoutMs))}`,
    `UNIDESK_WEB_OBSERVE_MAX_PROCESS_READ_CONCURRENCY=${shellQuote(String(observerLifecycle.maxProcessReadConcurrency))}`,
    `UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_ENTRIES=${shellQuote(String(observerLifecycle.maxRealtimeTotalEntries))}`,
    `UNIDESK_WEB_OBSERVE_MAX_REALTIME_TOTAL_BODY_BYTES=${shellQuote(String(observerLifecycle.maxRealtimeTotalBodyBytes))}`,
    `UNIDESK_WEB_OBSERVE_VIEWPORT=${shellQuote(options.viewport)}`,
    `UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE=${shellQuote(options.browserProxyMode)}`,
    `UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON=${shellQuote(JSON.stringify(alertThresholds))}`,
    `UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON=${shellQuote(JSON.stringify(browserFreezePolicy))}`,
    `UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON=${shellQuote(JSON.stringify(projectManagement))}`,
    `UNIDESK_WEB_OBSERVE_REALTIME_FANOUT_PROFILES_JSON=${shellQuote(JSON.stringify(nodeWebProbeRealtimeFanoutProfiles(spec)))}`,
    `UNIDESK_WEB_OBSERVE_WORKBENCH_KAFKA_DEBUG_REPLAY_JSON=${shellQuote(JSON.stringify(nodeWebProbeWorkbenchKafkaDebugReplay(spec)))}`,
    `UNIDESK_WEB_OBSERVE_WORKBENCH_TRACE_READABILITY_JSON=${shellQuote(JSON.stringify(nodeWebProbeWorkbenchTraceReadability(spec)))}`,
    ...(authLogin === null
      ? []
      : [
        `UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_ATTEMPTS=${shellQuote(String(authLogin.maxAttempts))}`,
        `UNIDESK_WEB_OBSERVE_AUTH_LOGIN_REQUEST_TIMEOUT_MS=${shellQuote(String(authLogin.requestTimeoutMs))}`,
        `UNIDESK_WEB_OBSERVE_AUTH_LOGIN_INITIAL_DELAY_MS=${shellQuote(String(authLogin.initialDelayMs))}`,
        `UNIDESK_WEB_OBSERVE_AUTH_LOGIN_MAX_DELAY_MS=${shellQuote(String(authLogin.maxDelayMs))}`,
      ]),
  ].join(" ");
  const finalComparator = options.memoryGuardMode === "sentinel-cadence"
    ? finalMemoryPolicy.sentinelComparator
    : finalMemoryPolicy.manualComparator;
  const finalGuardFailure = (reason: string) => JSON.stringify({
    ok: false,
    command: "web-probe-observe start",
    status: "blocked",
    reason,
    failureKind: "web-probe-final-memory-start-guard",
    jobId,
    stateDir,
    observerCreated: false,
    startBlocked: true,
    mutation: true,
    stateArtifactCreated: true,
    browserCreated: false,
    valuesRedacted: true,
  });
  const launchFailureOutputSource = "const fs=require('fs'); const dir=process.argv[1]; const read=(n)=>{try{return JSON.parse(fs.readFileSync(dir+'/'+n,'utf8'))}catch{return null}}; const guard=read('launch-guard.json'); const readiness=read('launch-ready.json'); const pid=Number(process.argv[3]); const bool=(v)=>v==='true'; console.log(JSON.stringify({ok:false,command:'web-probe-observe start',status:'blocked',reason:'launch-readiness-unconfirmed',failureKind:'web-probe-launch-readiness',jobId:process.argv[2],stateDir:dir,observerCreated:false,browserCreated:false,browserCreationAttempted:true,startBlocked:true,mutation:true,stateArtifactCreated:true,memoryGuard:guard&&guard.memoryGuard||null,finalLaunchGuard:guard,launchReadiness:readiness,cleanup:{attempted:true,processGroupId:pid,termSent:bool(process.argv[4]),killSent:bool(process.argv[5]),aliveAfter:bool(process.argv[6]),completed:!bool(process.argv[6]),observedAt:new Date().toISOString(),valuesRedacted:true},valuesRedacted:true},null,2))";
  const cleanupPollCount = Math.ceil(finalMemoryPolicy.launchReadyTimeoutSeconds * 1000 / finalMemoryPolicy.launchReadyPollMilliseconds);
  const script = [
    "set -eu",
    `state_dir=${shellQuote(stateDir)}`,
    "mkdir -p \"$state_dir\"",
    "chmod 700 \"$state_dir\"",
    "runner=\"$state_dir/observer-runner.mjs\"",
    "runner_b64=\"$state_dir/observer-runner.mjs.b64\"",
    "cat >\"$runner_b64\" <<'UNIDESK_WEB_OBSERVE_RUNNER_B64'",
    runnerB64Body,
    "UNIDESK_WEB_OBSERVE_RUNNER_B64",
    "node -e \"const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(fs.readFileSync(process.argv[2], 'utf8').replace(/\\s+/g, ''), 'base64'))\" \"$runner\" \"$runner_b64\"",
    "rm -f \"$runner_b64\"",
    "chmod 700 \"$runner\"",
    `launch_lock=${shellQuote(finalMemoryPolicy.launchLockPath)}`,
    `launch_guard_result="$state_dir/launch-guard.json"`,
    `launch_ready_result="$state_dir/launch-ready.json"`,
    `if ! command -v flock >/dev/null 2>&1; then printf '%s\\n' ${shellQuote(finalGuardFailure("host-flock-unavailable"))} >"$launch_guard_result"; cat "$launch_guard_result"; exit 0; fi`,
    `if ! command -v setsid >/dev/null 2>&1; then printf '%s\\n' ${shellQuote(finalGuardFailure("host-setsid-unavailable"))} >"$launch_guard_result"; cat "$launch_guard_result"; exit 0; fi`,
    `exec 9>"$launch_lock"`,
    `if ! flock -w ${shellQuote(String(finalMemoryPolicy.launchLockTimeoutSeconds))} 9; then printf '%s\\n' ${shellQuote(finalGuardFailure("launch-lock-timeout"))} >"$launch_guard_result"; cat "$launch_guard_result"; exit 0; fi`,
    "trap 'flock -u 9 2>/dev/null || true' EXIT",
    "set +e",
    `node - ${shellQuote(String(finalMemoryPolicy.thresholdBytes))} ${shellQuote(finalComparator)} ${shellQuote(options.memoryGuardMode)} ${shellQuote(jobId)} ${shellQuote(stateDir)} >"$launch_guard_result" <<'UNIDESK_WEB_OBSERVE_FINAL_GUARD'`,
    nodeWebObserveFinalLaunchGuardSource(),
    "UNIDESK_WEB_OBSERVE_FINAL_GUARD",
    "launch_guard_rc=$?",
    "set -e",
    "if [ \"$launch_guard_rc\" -ne 0 ]; then cat \"$launch_guard_result\"; flock -u 9; exit 0; fi",
    `setsid env ${runnerEnvAssignments} node "$runner" >"$state_dir/stdout.log" 2>"$state_dir/stderr.log" </dev/null 9>&- &`,
    "pid=$!",
    "printf '%s\\n' \"$pid\" >\"$state_dir/pid\"",
    "set +e",
    `node - "$pid" "$state_dir" ${shellQuote(jobId)} ${shellQuote(String(finalMemoryPolicy.launchReadyTimeoutSeconds * 1000))} ${shellQuote(String(finalMemoryPolicy.launchReadyPollMilliseconds))} >"$launch_ready_result" <<'UNIDESK_WEB_OBSERVE_LAUNCH_READY'`,
    nodeWebObserveLaunchReadinessSource(),
    "UNIDESK_WEB_OBSERVE_LAUNCH_READY",
    "launch_ready_rc=$?",
    "set -e",
    `if [ "$launch_ready_rc" -ne 0 ]; then term_sent=false; if /bin/kill -TERM -- -"$pid" 2>/dev/null; then term_sent=true; fi; i=0; while [ "$i" -lt ${shellQuote(String(cleanupPollCount))} ] && /bin/kill -0 -- -"$pid" 2>/dev/null; do sleep ${shellQuote(String(finalMemoryPolicy.launchReadyPollMilliseconds / 1000))}; i=$((i+1)); done; kill_sent=false; if /bin/kill -0 -- -"$pid" 2>/dev/null; then if /bin/kill -KILL -- -"$pid" 2>/dev/null; then kill_sent=true; fi; fi; i=0; while [ "$i" -lt ${shellQuote(String(cleanupPollCount))} ] && /bin/kill -0 -- -"$pid" 2>/dev/null; do sleep ${shellQuote(String(finalMemoryPolicy.launchReadyPollMilliseconds / 1000))}; i=$((i+1)); done; alive_after=false; if /bin/kill -0 -- -"$pid" 2>/dev/null; then alive_after=true; fi; node -e ${shellQuote(launchFailureOutputSource)} "$state_dir" ${shellQuote(jobId)} "$pid" "$term_sent" "$kill_sent" "$alive_after"; flock -u 9; trap - EXIT; exit 0; fi`,
    "flock -u 9",
    "trap - EXIT",
    `node -e ${shellQuote("const fs=require('fs'); const dir=process.argv[1]; const read=(n)=>{try{return JSON.parse(fs.readFileSync(dir+'/'+n,'utf8'))}catch{return null}}; const pid=fs.existsSync(dir+'/pid')?fs.readFileSync(dir+'/pid','utf8').trim():null; console.log(JSON.stringify({ok:true,command:'web-probe-observe start',jobId:process.argv[2],stateDir:dir,pid:Number(pid)||null,manifestPath:dir+'/manifest.json',heartbeat:read('heartbeat.json'),manifest:read('manifest.json'),finalLaunchGuard:read('launch-guard.json'),launchReadiness:read('launch-ready.json'),observerCreated:true,browserCreated:true,observerMutation:true,stateArtifactMutation:true,statusCommand:'bun scripts/cli.ts web-probe observe status --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,stopCommand:'bun scripts/cli.ts web-probe observe stop --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,valuesRedacted:true},null,2))")} "$state_dir" ${shellQuote(jobId)} ${shellQuote(options.node)} ${shellQuote(options.lane)}`,
  ].join("\n");
  const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
  const startResolution = resolveWebObserveActionJson(result, "start");
  const started = startResolution.parsed;
  if (started?.observerCreated === false && started.startBlocked === true) {
    const finalSnapshot = record(started.memoryGuard);
    const finalMemoryGuard = evaluateWebProbeMemoryGuard(spec, options.memoryGuardMode, {
      ok: finalSnapshot.metric === "MemAvailable" && finalSnapshot.source === "/proc/meminfo" && finalSnapshot.swapExcluded === true,
      action: "gc remote memory",
      metric: finalSnapshot.metric,
      source: finalSnapshot.source,
      swapExcluded: finalSnapshot.swapExcluded,
      memory: { availableBytes: finalSnapshot.availableBytes },
    });
    return {
      ...started,
      node: options.node,
      lane: options.lane,
      workspace: spec.workspace,
      initialMemoryGuard: memoryGuard,
      memoryGuard: finalMemoryGuard,
      finalLaunchGuard: started.finalLaunchGuard ?? started,
      valuesRedacted: true,
    };
  }
  const startOk = result.exitCode === 0 && started?.ok === true;
  const allowedFinalSnapshot = record(record(started?.finalLaunchGuard).memoryGuard);
  const effectiveMemoryGuard = allowedFinalSnapshot.metric === "MemAvailable"
    ? evaluateWebProbeMemoryGuard(spec, options.memoryGuardMode, {
      ok: true,
      action: "gc remote memory",
      metric: allowedFinalSnapshot.metric,
      source: allowedFinalSnapshot.source,
      swapExcluded: allowedFinalSnapshot.swapExcluded,
      memory: { availableBytes: allowedFinalSnapshot.availableBytes },
    })
    : memoryGuard;
  const recovery = startOk
    ? null
    : readNodeWebProbeObserveRemoteStatus({ ...options, id: jobId, stateDir }, spec, 1, Math.min(options.commandTimeoutSeconds, 30));
  const recoveredStatus = !startOk && recovery !== null && recovery.result.exitCode === 0 && recovery.status !== null && recovery.status.ok !== false
    ? recovery.status
    : null;
  const effectiveOk = startOk || recoveredStatus !== null;
  const observer = startOk ? started : recoveredStatus;
  const observerId = typeof started?.jobId === "string"
    ? started.jobId
    : webObserveIdFromStatus(recoveredStatus, { ...options, id: jobId }) ?? jobId;
  const degradedReason = !startOk && recoveredStatus !== null
    ? "web-probe-start-transport-timeout-recovered"
    : result.timedOut
      ? "web-probe-command-timeout"
      : result.exitCode !== 0
        ? "web-probe-observe-start-failed"
        : null;
  const index = effectiveOk
    ? upsertWebObserveIndexEntry(startOk ? {
      id: observerId,
      node: options.node,
      lane: options.lane,
      workspace: spec.workspace,
      stateDir,
      url: options.url,
      originName: options.originName,
      originMode: options.originMode,
      originConfigPath: options.originConfigPath,
      browserProxyMode: options.browserProxyMode,
      browserProxyModeSource: options.browserProxyModeSource,
      targetPath: options.targetPath,
      status: "running",
      pid: typeof started?.pid === "number" ? started.pid : null,
      startedAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    } : webObserveIndexEntryFromOptions({ ...options, id: observerId, stateDir }, spec, observerId, recoveredStatus ?? {}))
    : null;
  return renderWebObserveStartResult({
    ok: effectiveOk,
    status: effectiveOk ? "started" : "blocked",
    command: `web-probe observe start --node ${options.node} --lane ${options.lane}${options.originName === "custom" ? "" : ` --origin ${options.originName}`}`,
    node: options.node,
    lane: options.lane,
    workspace: spec.workspace,
    url: options.url,
    origin: {
      name: options.originName,
      mode: options.originMode,
      configPath: options.originConfigPath,
      browserProxyMode: options.browserProxyMode,
      browserProxyModeSource: options.browserProxyModeSource,
      valuesRedacted: true,
    },
    network: webProbeProxy.summary,
    alertThresholds,
    browserFreezePolicy,
    projectManagement,
    initialMemoryGuard: memoryGuard,
    memoryGuard: effectiveMemoryGuard,
    finalLaunchGuard: started?.finalLaunchGuard ?? null,
    launchReadiness: started?.launchReadiness ?? null,
    targetPath: options.targetPath,
    id: observerId,
    degradedReason,
    credential,
    observer: withWebObserveShortcuts(observer, observerId),
    wrapper: buildWebObserveWrapperForObserveOptions("start", options, spec.workspace, { id: observerId, jobId: observerId, stateDir }),
    index,
    next: webObserveNextCommands(observerId),
    stdoutJson: startResolution.diagnostics,
    result: compactCommandResultRedacted(result, [material.password ?? ""]),
    recovery: recovery === null ? null : {
      attempted: true,
      ok: recoveredStatus !== null,
      reason: degradedReason,
      stdoutJson: recovery.stdoutJson,
      result: compactCommandResultWithStdoutTail(recovery.result),
      valuesRedacted: true,
    },
    valuesRedacted: true,
  });
}

export function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
  const { result, status, stdoutJson } = readNodeWebProbeObserveRemoteStatus(options, spec, options.tailLines, options.commandTimeoutSeconds);
  const observerId = webObserveIdFromStatus(status, options);
  const statusReadable = status !== null;
  const ok = result.exitCode === 0 && statusReadable && status.ok !== false;
  const degradedReason = result.timedOut
    ? "web-probe-command-timeout"
    : result.exitCode !== 0
      ? "web-probe-observe-status-failed"
      : !statusReadable
        ? "web-probe-observe-status-unreadable"
        : typeof status.degradedReason === "string"
          ? status.degradedReason
          : null;
  const index = ok && observerId !== null && options.stateDir !== null
    ? upsertWebObserveIndexEntry(webObserveIndexEntryFromOptions(options, spec, observerId, status))
    : null;
  return withWebObserveStatusRendered({
    ok,
    status: ok ? "observed" : "blocked",
    command: webObserveCommandLabel("status", options),
    id: observerId,
    node: options.node,
    lane: options.lane,
    workspace: spec.workspace,
    degradedReason,
    observer: withWebObserveShortcuts(status, observerId),
    wrapper: buildWebObserveWrapperForObserveOptions("status", options, spec.workspace, { id: observerId, stateDir: webObserveWrapperStateDirFromStatus(status, options.stateDir) }),
    index,
    next: observerId === null ? null : webObserveNextCommands(observerId),
    stdoutJson,
    result: compactCommandResultWithStdoutTail(result),
    valuesRedacted: true,
  });
}

export function readNodeWebProbeObserveRemoteStatus(
  options: NodeWebProbeObserveOptions,
  spec: HwlabRuntimeLaneSpec,
  tailLines: number,
  timeoutSeconds: number,
): { result: ReturnType<typeof runTransWorkspaceStdinScript>; status: Record<string, unknown> | null; stdoutJson: Record<string, unknown> } {
  const script = [
    "set -eu",
    nodeWebObserveResolveStateDirShell(options),
    nodeWebObserveStatusNodeScript(tailLines, options.node, options.lane, options.collectCommandId),
  ].join("\n");
  const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, timeoutSeconds);
  const statusResolution = resolveWebObserveActionJson(result, "status");
  return { result, status: statusResolution.parsed, stdoutJson: statusResolution.diagnostics };
}

export function webObserveText(value: unknown): string {
  if (value === null || value === undefined || value === "") return "-";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  return JSON.stringify(value);
}

export function webObserveShort(value: string, maxLength: number): string {
  if (value.length <= maxLength) return value;
  if (maxLength <= 1) return value.slice(0, maxLength);
  return `${value.slice(0, maxLength - 1)}~`;
}

export function buildWebObserveCommandVisibility(input: {
  commandType: string;
  commandResult: Record<string, unknown> | null;
  exitCode: number | null;
}): {
  status: string;
  control: Record<string, unknown>;
  asyncTurn: Record<string, unknown>;
} {
  const commandResult = input.commandResult;
  const executionStatus = input.exitCode !== 0 || commandResult === null
    ? "blocked"
    : commandResult.ok === false
      ? "failed"
      : commandResult.completedAt !== undefined && commandResult.completedAt !== null
        ? "completed"
        : commandResult.queued === true || commandResult.waitTimedOut === true
          ? "queued"
          : "accepted";
  const actionResult = record(commandResult?.result);
  const chatSubmit = record(actionResult.chatSubmit);
  const traceId = typeof chatSubmit.traceId === "string" ? chatSubmit.traceId : null;
  const otelTraceId = typeof chatSubmit.otelTraceId === "string" ? chatSubmit.otelTraceId : null;
  const httpStatus = typeof chatSubmit.status === "number" ? chatSubmit.status : null;
  const turnApplicable = input.commandType === "sendPrompt" || input.commandType === "steer";
  const submissionAccepted = chatSubmit.sideEffectObserved === true
    || httpStatus !== null && httpStatus >= 200 && httpStatus < 300
    || traceId !== null;
  const submissionStatus = !turnApplicable
    ? "not-applicable"
    : executionStatus === "blocked"
      ? "not-submitted"
      : executionStatus === "failed"
        ? "failed"
        : submissionAccepted
          ? "accepted"
          : executionStatus === "queued"
            ? "pending"
            : "not-observed";
  return {
    status: "control-" + executionStatus,
    control: {
      executionStatus,
      completedAt: commandResult?.completedAt ?? null,
      completionMeaning: executionStatus === "completed" ? "observer-browser-control-action-finished" : null,
      businessTurnTerminalImplied: false,
      valuesRedacted: true,
    },
    asyncTurn: {
      applicable: turnApplicable,
      submissionStatus,
      submissionHttpStatus: httpStatus,
      terminalStatus: null,
      terminalObserved: false,
      terminalStatusSource: turnApplicable ? "not-evaluated-by-control-command" : "not-applicable",
      traceId,
      otelTraceId,
      valuesRedacted: true,
    },
  };
}

export function webObserveStopFallbackReason(input: {
  stopCommand: boolean;
  exitCode: number;
  commandResult: Record<string, unknown> | null;
}): string | null {
  if (!input.stopCommand) return null;
  if (input.exitCode !== 0) return "graceful-stop-failed";
  if (input.commandResult === null) return "graceful-stop-contract-missing";
  if (input.commandResult.ok === false || input.commandResult.status === "failed") return "graceful-stop-rejected";
  if (input.commandResult.waitTimedOut === true) return "graceful-stop-not-consumed";
  if (input.commandResult.queued === true) return "graceful-stop-queued";
  return null;
}

export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, stopCommand: boolean): Record<string, unknown> | RenderedCliResult {
  const type = options.commandType ?? (stopCommand ? "stop" : null);
  if (type === null) throw new Error("web-probe observe command requires --type");
  if (type === "validateRealtimeFanout") {
    if (!options.commandProfile?.trim()) throw new Error("validateRealtimeFanout requires --profile");
    if (spec.webProbe?.realtimeFanoutProfiles?.[options.commandProfile.trim()] === undefined) {
      throw new Error(`validateRealtimeFanout profile is not declared by the owning YAML: ${options.commandProfile.trim()}`);
    }
    if (!options.commandProvider?.trim()) throw new Error("validateRealtimeFanout requires --provider");
    if (!options.commandText?.trim()) throw new Error("validateRealtimeFanout requires --text or --text-stdin for the first turn");
    if (!options.commandSecondText?.trim()) throw new Error("validateRealtimeFanout requires --second-text for the follow-up turn");
  }
  if (type === "validateExistingSessionRefresh") {
    if (!options.commandProfile?.trim()) throw new Error("validateExistingSessionRefresh requires --profile");
    if (spec.webProbe?.realtimeFanoutProfiles?.[options.commandProfile.trim()] === undefined) {
      throw new Error(`validateExistingSessionRefresh profile is not declared by the owning YAML: ${options.commandProfile.trim()}`);
    }
  }
  if (type === "validateWorkbenchKafkaDebugReplay" && spec.webProbe?.workbenchKafkaDebugReplay?.enabled !== true) {
    throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
  }
  if (type === "validateWorkbenchTraceReadability" && nodeWebProbeWorkbenchTraceReadability(spec)?.enabled !== true) {
    throw new Error("owning YAML 未启用 validateWorkbenchTraceReadability");
  }
  const commandId = `cmd-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
  const payload = {
    id: commandId,
    type,
    createdAt: new Date().toISOString(),
    source: "cli",
    path: options.commandPath,
    text: options.commandText,
    secondText: options.commandSecondText,
    label: options.commandLabel,
    sessionId: options.commandSessionId,
    provider: options.commandProvider,
    profile: options.commandProfile,
    afterRound: options.commandAfterRound,
    severity: options.commandSeverity,
    alternateSessionStrategy: options.commandAlternateSessionStrategy,
    expectedSentinelRange: options.commandExpectedSentinelRange,
    expectedActionWaitMs: options.commandExpectedActionWaitMs,
    durationMs: options.commandDurationMs,
    requireComposerReady: options.commandRequireComposerReady,
    waitProjectManagementReady: options.commandWaitProjectManagementReady,
    findingId: options.commandFindingId,
    blocking: options.commandBlocking,
    accountId: options.commandAccountId,
    fromAccountId: options.commandFromAccountId,
    toAccountId: options.commandToAccountId,
    sourceId: options.commandSourceId,
    fileRef: options.commandFileRef,
    filename: options.commandFilename,
    taskRef: options.commandTaskRef,
    taskId: options.commandTaskId,
    field: options.commandField,
    link: options.commandLink,
    title: options.commandTitle,
    body: options.commandBody,
    status: options.commandStatus,
    hwpodId: options.commandHwpodId,
    nodeId: options.commandNodeId,
    workspaceRoot: options.commandWorkspaceRoot,
    root: options.commandRoot,
  };
  const preStopStatus = options.force && stopCommand
    ? readNodeWebProbeObserveRemoteStatus(options, spec, 1, Math.min(options.commandTimeoutSeconds, 30))
    : null;
  const preStopDiagnostics = record(preStopStatus?.status?.diagnostics);
  const preStopCommands = record(preStopStatus?.status?.commands);
  const preStopPending = Number(preStopCommands?.pendingCount ?? 0);
  const preStopProcessing = Number(preStopCommands?.processingCount ?? 0);
  const forceBeforeQueueReason = options.force && stopCommand
    ? preStopDiagnostics?.heartbeatStale === true
      ? "heartbeat-stale"
      : preStopPending > 0 || preStopProcessing > 0
        ? "command-backlog"
        : null
    : null;
  if (forceBeforeQueueReason !== null) {
    return runNodeWebProbeObserveForceStop(options, spec, payload, commandId, forceBeforeQueueReason, preStopStatus?.result ?? null, preStopStatus?.status ?? null, null);
  }
  const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
  const waitMs = stopCommand ? Math.max(options.waitMs, 5000) : options.waitMs;
  const script = [
    "set -eu",
    nodeWebObserveResolveStateDirShell(options),
    "mkdir -p \"$state_dir/commands/pending\"",
    `node -e "const fs=require('fs'),path=require('path'); const dir=process.argv[1], id=process.argv[2], payload=Buffer.from(process.argv[3], 'base64').toString('utf8'); fs.writeFileSync(path.join(dir,'commands','pending',id+'.json'), payload+'\\n', {mode:0o600});" "$state_dir" ${shellQuote(commandId)} ${shellQuote(payloadB64)}`,
    nodeWebObserveWaitCommandShell(commandId, waitMs),
  ].join("\n");
  const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
  const commandResolution = resolveWebObserveActionJson(result, "command");
  const commandResult = commandResolution.parsed;
  const stopFallbackReason = webObserveStopFallbackReason({ stopCommand, exitCode: result.exitCode, commandResult });
  if (stopFallbackReason !== null) {
    return runNodeWebProbeObserveForceStop(options, spec, payload, commandId, stopFallbackReason, preStopStatus?.result ?? null, preStopStatus?.status ?? null, result);
  }
  const visibility = buildWebObserveCommandVisibility({
    commandType: type,
    commandResult,
    exitCode: result.exitCode,
  });
  const commandOk = result.exitCode === 0 && commandResult !== null && commandResult.ok !== false;
  const payloadResult = {
    ok: commandOk,
    status: visibility.status,
    command: webObserveCommandLabel(stopCommand ? "stop" : "command", options),
    id: webObserveIdFromOptions(options),
    node: options.node,
    lane: options.lane,
    workspace: spec.workspace,
    commandId,
    observerCommand: commandSummaryForOutput(payload),
    observer: commandResult,
    control: visibility.control,
    asyncTurn: visibility.asyncTurn,
    cleanup: stopCommand ? {
      attempted: true,
      mode: "graceful-command",
      state: commandOk ? "requested" : "unknown",
      terminationConfirmed: false,
      valuesRedacted: true,
    } : null,
    wrapper: buildWebObserveWrapperForObserveOptions(stopCommand ? "stop" : "command", options, spec.workspace, { commandType: type }),
    stdoutJson: commandResolution.diagnostics,
    result: compactCommandResult(result),
    full: options.full,
    valuesRedacted: true,
  };
  return options.raw ? payloadResult : withWebObserveCommandRendered(payloadResult);
}

export function runNodeWebProbeObserveForceStop(
  options: NodeWebProbeObserveOptions,
  spec: HwlabRuntimeLaneSpec,
  payload: Record<string, unknown>,
  commandId: string,
  reason: string,
  preflightResult: ReturnType<typeof runTransWorkspaceStdinScript> | null,
  preflightStatus: Record<string, unknown> | null,
  gracefulResult: ReturnType<typeof runTransWorkspaceStdinScript> | null,
): Record<string, unknown> | RenderedCliResult {
  const killResult = runTransWorkspaceStdinScript(options.node, spec.workspace, [
    "set -eu",
    nodeWebObserveResolveStateDirShell(options),
    nodeWebObserveForceStopNodeScript(reason, commandId),
  ].join("\n"), 55);
  const forceResolution = resolveWebObserveActionJson(killResult, "force-stop");
  const forcePayload = forceResolution.parsed;
  const cleanupConfirmed = killResult.exitCode === 0
    && forcePayload !== null
    && forcePayload.ok !== false
    && forcePayload.aliveAfter !== true;
  const payloadResult = {
    ok: cleanupConfirmed,
    status: cleanupConfirmed ? "forced-stopped" : "blocked",
    command: webObserveCommandLabel("stop", options),
    id: webObserveIdFromOptions(options),
    node: options.node,
    lane: options.lane,
    workspace: spec.workspace,
    commandId,
    observerCommand: commandSummaryForOutput(payload),
    observer: forcePayload,
    wrapper: buildWebObserveWrapperForObserveOptions("stop", options, spec.workspace, { commandType: "stop" }),
    forceReason: reason,
    cleanup: {
      attempted: true,
      mode: "force-stop",
      state: cleanupConfirmed ? "confirmed" : "unknown",
      terminationConfirmed: cleanupConfirmed,
      processAliveAfter: forcePayload?.aliveAfter ?? null,
      transportExitCode: killResult.exitCode,
      reason,
      valuesRedacted: true,
    },
    preflightObserver: preflightStatus,
    preflightResult: preflightResult === null ? null : compactCommandResult(preflightResult),
    gracefulResult: gracefulResult === null ? null : compactCommandResult(gracefulResult),
    stdoutJson: forceResolution.diagnostics,
    forceResult: compactCommandResultWithStdoutTail(killResult),
    full: options.full,
    valuesRedacted: true,
  };
  return options.raw ? payloadResult : withWebObserveCommandRendered(payloadResult);
}
