// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-12-p17-monitor-central-persistence.
// Responsibility: Build canonical immutable terminal envelopes and submit them with YAML-declared bounded retries.
import type { MonitorArtifactLocator, MonitorCentralIngest } from "./monitor-central-persistence";
import { monitorCentralStableJson, normalizeMonitorCentralIngest } from "./monitor-central-persistence";

export interface MonitorIngestRetry { readonly maxAttempts: number; readonly delayMs: number; }
export interface MonitorTerminalIngestConfig { readonly endpoint: string; readonly timeoutMs: number; readonly retry: MonitorIngestRetry; }
export interface MonitorTerminalIdentity { readonly sentinelId: string; readonly node: string; readonly lane: string; readonly runId: string; }
export interface MonitorTerminalArtifact { readonly relativePath: string; readonly kind: string; readonly sha256: string; readonly sizeBytes: number; }

export class MonitorIngestFailedError extends Error {
  readonly code = "monitor-ingest-failed";
  constructor(readonly details: Record<string, unknown>) { super("Monitor terminal ingest failed after bounded retries"); this.name = "MonitorIngestFailedError"; }
}

export function buildMonitorTerminalIngest(identity: MonitorTerminalIdentity, report: Record<string, unknown>, artifacts: readonly MonitorTerminalArtifact[], locatorOwner: Pick<MonitorArtifactLocator, "ownerNode" | "ownerLane" | "ownerPvc" | "stateDir">): MonitorCentralIngest {
  const immutableReport = JSON.parse(monitorCentralStableJson(report)) as Record<string, unknown>;
  if (!identity.runId) throw new MonitorIngestFailedError({ reason: "run_id_missing", valuesRedacted: true });
  const updatedAt = stringValue(immutableReport.updatedAt) ?? stringValue(immutableReport.analyzedAt) ?? stringValue(immutableReport.reportUpdatedAt);
  if (!updatedAt) throw new MonitorIngestFailedError({ reason: "terminal_timestamp_missing", runId: identity.runId, valuesRedacted: true });
  for (const artifact of artifacts) if (!/^sha256:[a-f0-9]{64}$/u.test(artifact.sha256) || !Number.isSafeInteger(artifact.sizeBytes) || artifact.sizeBytes < 1) throw new MonitorIngestFailedError({ reason: "terminal_artifact_unverified", runId: identity.runId, relativePath: artifact.relativePath, valuesRedacted: true });
  return normalizeMonitorCentralIngest({
    ...identity,
    updatedAt,
    status: stringValue(immutableReport.status) ?? (immutableReport.ok === true ? "ok" : "blocked"),
    payload: { ...immutableReport, report: immutableReport, artifacts: artifacts.map((artifact) => ({ ...artifact })) },
    findings: recordArray(immutableReport.findings),
    timeline: recordArray(immutableReport.timeline),
    artifactLocators: artifacts.map((artifact) => ({ ...locatorOwner, relativePath: artifact.relativePath, kind: artifact.kind, sha256: artifact.sha256, sizeBytes: artifact.sizeBytes, reachable: true })),
  });
}

export async function submitMonitorTerminalIngest(config: MonitorTerminalIngestConfig, payload: MonitorCentralIngest, fetchImpl: typeof fetch = fetch): Promise<Record<string, unknown>> {
  validateConfig(config);
  const canonical = normalizeMonitorCentralIngest(payload);
  const body = monitorCentralStableJson(canonical);
  let lastError: Record<string, unknown> | null = null;
  for (let attempt = 1; attempt <= config.retry.maxAttempts; attempt += 1) {
    try {
      const response = await fetchImpl(config.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(config.timeoutMs) });
      const responseBody = await response.json().catch(() => null);
      if (response.ok && isRecord(responseBody) && responseBody.ok === true) return { ...responseBody, attempts: attempt, payloadHash: canonical.payloadHash, valuesRedacted: true };
      lastError = { attempt, httpStatus: response.status, response: responseBody, valuesRedacted: true };
    } catch (error) { lastError = { attempt, reason: error instanceof Error ? error.message : String(error), valuesRedacted: true }; }
    if (attempt < config.retry.maxAttempts && config.retry.delayMs > 0) await sleep(config.retry.delayMs);
  }
  throw new MonitorIngestFailedError({ endpoint: config.endpoint, attempts: config.retry.maxAttempts, payloadHash: canonical.payloadHash, lastError, valuesRedacted: true });
}

function validateConfig(config: MonitorTerminalIngestConfig): void {
  if (!/^https?:\/\//u.test(config.endpoint)) throw new Error("monitor ingest endpoint must be an absolute HTTP(S) URL");
  if (!Number.isSafeInteger(config.timeoutMs) || config.timeoutMs < 1) throw new Error("monitor ingest timeoutMs must be a positive integer");
  if (!Number.isSafeInteger(config.retry.maxAttempts) || config.retry.maxAttempts < 1) throw new Error("monitor ingest retry.maxAttempts must be a positive integer");
  if (!Number.isSafeInteger(config.retry.delayMs) || config.retry.delayMs < 0) throw new Error("monitor ingest retry.delayMs must be a non-negative integer");
}
function recordArray(value: unknown): readonly Record<string, unknown>[] { return Array.isArray(value) ? value.filter(isRecord) : []; }
function stringValue(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
function sleep(delayMs: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, delayMs)); }

export interface MonitorTerminalWriterConfig { readonly mode: "legacy-sqlite" | "central"; readonly ingest?: MonitorTerminalIngestConfig; }

export function resolveMonitorTerminalWriter(runtime: Record<string, unknown>): MonitorTerminalWriterConfig {
  const sqlite = runtime.sqlite;
  const sqlitePath = sqlite && typeof sqlite === "object" && !Array.isArray(sqlite) ? stringValue((sqlite as Record<string, unknown>).path) : undefined;
  const monitor = runtime.monitor;
  const writer = monitor && typeof monitor === "object" && !Array.isArray(monitor) ? (monitor as Record<string, unknown>).terminalWriter : null;
  if (!writer || typeof writer !== "object" || Array.isArray(writer)) {
    if (sqlitePath) return { mode: "legacy-sqlite" };
    throw new Error("terminal writer authority is undeclared: sqlite.path or monitor.terminalWriter is required");
  }
  const value = writer as Record<string, unknown>;
  const mode = value.mode;
  if (mode !== "legacy-sqlite" && mode !== "central") throw new Error("monitor.terminalWriter.mode must be legacy-sqlite or central");
  if (mode === "legacy-sqlite") {
    if (!sqlitePath) throw new Error("sqlite.path is required when mode=legacy-sqlite");
    return { mode };
  }
  if (sqlitePath) throw new Error("central terminal writer and sqlite.path are mutually exclusive");
  const ingest = value.ingest;
  if (!ingest || typeof ingest !== "object" || Array.isArray(ingest)) throw new Error("monitor.terminalWriter.ingest is required when mode=central");
  const config = ingest as Record<string, unknown>;
  const retry = config.retry;
  if (!retry || typeof retry !== "object" || Array.isArray(retry)) throw new Error("monitor.terminalWriter.ingest.retry is required when mode=central");
  return { mode, ingest: { endpoint: String(config.endpoint ?? ""), timeoutMs: Number(config.timeoutMs), retry: { maxAttempts: Number((retry as Record<string, unknown>).maxAttempts), delayMs: Number((retry as Record<string, unknown>).delayMs) } } };
}
