// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// Responsibility: Stable wrapper contract for the existing web-probe observe CLI verbs.

export type WebObserveWrapperAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";

export interface WebObserveWrapperInput {
  readonly action: WebObserveWrapperAction;
  readonly node: string;
  readonly lane: string;
  readonly workspace: string;
  readonly id: string | null;
  readonly jobId: string | null;
  readonly stateDir: string | null;
  readonly url: string | null;
  readonly originName: "internal" | "public" | "native" | "custom" | null;
  readonly originMode: "k8s-service-cluster-ip" | "public" | "native-public" | "custom-url" | null;
  readonly originConfigPath: string | null;
  readonly browserProxyMode: "auto" | "direct" | null;
  readonly browserProxyModeSource: "cli" | "yaml-origin" | "yaml-web-probe" | "default" | null;
  readonly targetPath: string | null;
  readonly commandType: string | null;
  readonly commandTimeoutSeconds: number | null;
  readonly collectView: string | null;
  readonly collectFile: string | null;
  readonly collectTraceId: string | null;
  readonly collectSampleSeq: number | null;
  readonly collectTimestamp: string | null;
  readonly collectTurn: number | null;
  readonly collectCommandId: string | null;
  readonly collectWindowMs: number | null;
  readonly analyzeArchivePrefix: string | null;
}

export interface WebObserveWrapperOptionsLike {
  readonly id: string | null;
  readonly jobId: string | null;
  readonly node: string;
  readonly lane: string;
  readonly url: string;
  readonly originName: "internal" | "public" | "native" | "custom";
  readonly originMode: "k8s-service-cluster-ip" | "public" | "native-public" | "custom-url";
  readonly originConfigPath: string | null;
  readonly browserProxyMode: "auto" | "direct";
  readonly browserProxyModeSource: "cli" | "yaml-origin" | "yaml-web-probe" | "default";
  readonly targetPath: string;
  readonly stateDir: string | null;
  readonly commandType: string | null;
  readonly commandTimeoutSeconds: number;
  readonly collectView: string;
  readonly collectFile: string | null;
  readonly collectTraceId: string | null;
  readonly collectSampleSeq: number | null;
  readonly collectTimestamp: string | null;
  readonly collectTurn: number | null;
  readonly collectCommandId: string | null;
  readonly collectWindowMs: number | null;
  readonly analyzeArchivePrefix: string | null;
}

export interface WebObserveWrapperOverrides {
  readonly id?: string | null;
  readonly jobId?: string | null;
  readonly stateDir?: string | null;
  readonly commandType?: string | null;
}

export const WEB_OBSERVE_WRAPPER_SPEC_REF = "PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel";

const WEB_OBSERVE_ARTIFACT_CONTRACT = [
  { path: "manifest.json", producer: "existing-observe-runner", purpose: "observer identity and immutable run settings" },
  { path: "heartbeat.json", producer: "existing-observe-runner", purpose: "bounded liveness and current browser state" },
  { path: "samples.jsonl", producer: "existing-observe-runner", purpose: "DOM, trace, session, timing, and final response samples" },
  { path: "control.jsonl", producer: "existing-observe-runner", purpose: "explicit user/control actions and command results" },
  { path: "network.jsonl", producer: "existing-observe-runner", purpose: "request, response, requestfailed, and timing evidence" },
  { path: "console.jsonl", producer: "existing-observe-runner", purpose: "browser console/runtime evidence" },
  { path: "artifacts.jsonl", producer: "existing-observe-runner", purpose: "screenshots and auxiliary artifact index" },
  { path: "performance-events.jsonl", producer: "existing-observe-runner", purpose: "LongTask, LoAF, event-loop gap, and performance capture events" },
  { path: "performance/captures/*/{profile.cpuprofile,summary.json}", producer: "observe-command-cli", purpose: "explicit CPU profile captures for frontend hotspot attribution" },
  { path: "commands/{pending,processing,done,failed}/*.json", producer: "observe-command-cli", purpose: "durable command queue handoff" },
  { path: "analysis/report.json", producer: "existing-observe-analyzer", purpose: "offline machine-readable findings" },
  { path: "analysis/report.md", producer: "existing-observe-analyzer", purpose: "offline human-readable report" },
] as const;

const WEB_OBSERVE_CLI_DEFAULT_DISCLOSURE = {
  targetPath: "/workbench",
  viewport: "1440x900",
  sampleIntervalMs: 5000,
  screenshotIntervalMs: 300000,
  observerRefreshIntervalMs: 180000,
  maxSamples: 0,
  commandTimeoutSeconds: 55,
  waitMs: 0,
  tailLines: 5,
  maxFiles: 80,
  collectView: "files",
  source: "current observe CLI parser defaults; P2 must move sentinel policy into YAML configRefs",
  valuesRedacted: true,
} as const;

export function buildWebObserveWrapperContract(input: WebObserveWrapperInput): Record<string, unknown> {
  const observerId = input.id ?? input.jobId;
  const stateDir = input.stateDir;
  return {
    specRef: WEB_OBSERVE_WRAPPER_SPEC_REF,
    adapter: "web-probe-observe-cli-wrapper",
    adapterVersion: 1,
    mode: "wrapper-only",
    action: input.action,
    node: input.node,
    lane: input.lane,
    workspace: input.workspace,
    id: observerId,
    jobId: input.jobId ?? observerId,
    cli: {
      family: "bun scripts/cli.ts web-probe observe",
      commandShape: webObserveWrapperCommandShape(input),
      verb: input.action,
      valuesRedacted: true,
    },
    effectiveParameters: {
      url: input.url,
      origin: {
        name: input.originName,
        mode: input.originMode,
        configPath: input.originConfigPath,
        browserProxyMode: input.browserProxyMode,
        browserProxyModeSource: input.browserProxyModeSource,
        valuesRedacted: true,
      },
      targetPath: input.targetPath,
      commandType: input.commandType,
      commandTimeoutSeconds: input.commandTimeoutSeconds,
      collectView: input.collectView,
      collectFile: input.collectFile,
      collectTraceId: input.collectTraceId,
      collectSampleSeq: input.collectSampleSeq,
      collectTimestamp: input.collectTimestamp,
      collectTurn: input.collectTurn,
      collectCommandId: input.collectCommandId,
      collectWindowMs: input.collectWindowMs,
      analyzeArchivePrefix: input.analyzeArchivePrefix,
      valuesRedacted: true,
    },
    cliDefaults: WEB_OBSERVE_CLI_DEFAULT_DISCLOSURE,
    artifacts: {
      stateDir,
      rootKnown: stateDir !== null,
      manifest: artifactPath(stateDir, "manifest.json"),
      heartbeat: artifactPath(stateDir, "heartbeat.json"),
      samples: artifactPath(stateDir, "samples.jsonl"),
      control: artifactPath(stateDir, "control.jsonl"),
      network: artifactPath(stateDir, "network.jsonl"),
      console: artifactPath(stateDir, "console.jsonl"),
      analysisReportJson: artifactPath(stateDir, "analysis/report.json"),
      analysisReportMd: artifactPath(stateDir, "analysis/report.md"),
      contract: WEB_OBSERVE_ARTIFACT_CONTRACT,
      valuesRedacted: true,
    },
    invariants: {
      wrapperOnly: true,
      reusesExistingObserveRunner: true,
      reusesExistingObserveAnalyzer: true,
      createsSecondPlaywrightRunner: false,
      createsSecondAnalyzer: false,
      createsSecondStateMachine: false,
      usesPrivateWebProbeApi: false,
      collectViewsRenderExistingArtifactsOnly: true,
      analyzeReadsExistingArtifactsOnly: true,
      valuesRedacted: true,
    },
    nextStageBoundary: {
      p2: "YAML/configRefs must own sentinel policy and scenario cadence.",
      p3: "The persistent service may orchestrate this wrapper, not call Playwright or analyzer internals directly.",
      valuesRedacted: true,
    },
    valuesRedacted: true,
  };
}

export function buildWebObserveWrapperForObserveOptions(
  action: WebObserveWrapperAction,
  options: WebObserveWrapperOptionsLike,
  workspace: string,
  overrides: WebObserveWrapperOverrides = {},
): Record<string, unknown> {
  const id = overrides.id ?? options.id ?? options.jobId;
  return buildWebObserveWrapperContract({
    action,
    node: options.node,
    lane: options.lane,
    workspace,
    id,
    jobId: overrides.jobId ?? options.jobId ?? id,
    stateDir: overrides.stateDir ?? options.stateDir,
    url: options.url,
    originName: options.originName,
    originMode: options.originMode,
    originConfigPath: options.originConfigPath,
    browserProxyMode: options.browserProxyMode,
    browserProxyModeSource: options.browserProxyModeSource,
    targetPath: options.targetPath,
    commandType: overrides.commandType ?? options.commandType,
    commandTimeoutSeconds: options.commandTimeoutSeconds,
    collectView: options.collectView,
    collectFile: options.collectFile,
    collectTraceId: options.collectTraceId,
    collectSampleSeq: options.collectSampleSeq,
    collectTimestamp: options.collectTimestamp,
    collectTurn: options.collectTurn,
    collectCommandId: options.collectCommandId,
    collectWindowMs: options.collectWindowMs,
    analyzeArchivePrefix: options.analyzeArchivePrefix,
  });
}

export function webObserveWrapperStateDirFromStatus(status: Record<string, unknown> | null, explicitStateDir: string | null): string | null {
  const heartbeat = record(status?.heartbeat);
  const manifest = record(status?.manifest);
  const heartbeatStateDir = typeof heartbeat.stateDir === "string" ? heartbeat.stateDir : null;
  const manifestStateDir = typeof manifest.stateDir === "string" ? manifest.stateDir : null;
  return explicitStateDir ?? heartbeatStateDir ?? manifestStateDir;
}

function artifactPath(stateDir: string | null, relative: string): string | null {
  return stateDir === null ? null : `${stateDir}/${relative}`;
}

function webObserveWrapperCommandShape(input: WebObserveWrapperInput): string {
  const base = ["bun", "scripts/cli.ts", "web-probe", "observe", input.action];
  const observerId = input.id ?? input.jobId;
  const parts = observerId === null
    ? [...base, "--node", input.node, "--lane", input.lane]
    : [...base, observerId];
  if (observerId === null && input.stateDir !== null && input.action !== "start") {
    parts.push("--state-dir", input.stateDir);
  }
  if (input.action === "start" && input.targetPath !== null) {
    if (input.originName === "internal" || input.originName === "public" || input.originName === "native") parts.push("--origin", input.originName);
    parts.push("--target-path", input.targetPath);
  }
  if (input.action === "command" && input.commandType !== null) {
    parts.push("--type", input.commandType);
  }
  if (input.action === "collect") {
    if (input.collectView !== null) parts.push("--view", input.collectView);
    if (input.collectFile !== null) parts.push("--file", input.collectFile);
    if (input.collectTraceId !== null) parts.push("--trace-id", input.collectTraceId);
    if (input.collectSampleSeq !== null) parts.push("--sample-seq", String(input.collectSampleSeq));
    if (input.collectTimestamp !== null) parts.push("--timestamp", input.collectTimestamp);
    if (input.collectTurn !== null) parts.push("--turn", String(input.collectTurn));
    if (input.collectCommandId !== null) parts.push("--command-id", input.collectCommandId);
    if (input.collectWindowMs !== null) parts.push("--window-ms", String(input.collectWindowMs));
  }
  if (input.action === "analyze" && input.analyzeArchivePrefix !== null) {
    parts.push("--archive-prefix", input.analyzeArchivePrefix);
  }
  return parts.map(shellWord).join(" ");
}

function shellWord(value: string): string {
  if (/^[A-Za-z0-9_./:=@+-]+$/u.test(value)) return value;
  return `'${value.replace(/'/gu, "'\\''")}'`;
}

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