/**
 * SET 41 / 51 — clone-safe run-record persistence + honest empty-log labels
 * + checkpointed persistence helpers.
 *
 * chrome.runtime.sendMessage uses the structured-clone algorithm. One Map,
 * undefined hole, or cycle in the record aborts the whole persist. These
 * helpers JSON-round-trip (with Map/Set/cycle handling) so the SW always
 * receives a cloneable payload, and give the viewer a single honest string
 * when a finished/cancelled id has no stored record.
 *
 * SET 51 — a run record is created at start, checkpointed during the run,
 * and finalized at end. "Never saved" is structurally impossible; worst case
 * is a truncated checkpoint with an honest viewer header.
 */

/** Viewer / pipeline-log empty state for a finished id with no stored record. */
export const RUN_RECORD_MISSING =
  'run record missing (persist failed or never saved)';

/** Red pipeline line + SW notification body prefix. */
export function formatPersistFailed(reason: string): string {
  const r = (reason || 'unknown').trim() || 'unknown';
  return `persist failed: ${r}`;
}

/** SET 51a — honest header when finalization never landed. */
export function formatCheckpointHeader(checkpointAt: string): string {
  const ts = (checkpointAt || '').trim() || 'unknown';
  return `record checkpointed through ${ts} — finalization failed`;
}

export type PersistStatus =
  | 'running'
  | 'complete'
  | 'failed'
  | 'cancelled'
  | 'interrupted';

export type PersistRecordLike = {
  status?: string | null;
  checkpointAt?: string | null;
  finalized?: boolean | null;
  running?: boolean | null;
  events?: unknown[] | null;
};

/** True when the stored record is a checkpoint that never finalized. */
export function isCheckpointedUnfinalized(rec: PersistRecordLike | null | undefined): boolean {
  if (!rec) return false;
  if (rec.finalized === true) return false;
  if (rec.status === 'interrupted') return true;
  if (rec.status === 'running' || rec.running) return true;
  if (rec.checkpointAt && rec.finalized === false) return true;
  return false;
}

/**
 * Produce a structured-clone-safe plain JSON value.
 * Maps → objects, Sets → arrays, cycles → null, bigints → strings.
 * undefined values are dropped by JSON.stringify (same as clone omit).
 */
export function cloneSafeForPersist<T>(value: T): T {
  const seen = new WeakSet<object>();
  return JSON.parse(JSON.stringify(value, (_key, v) => {
    if (typeof v === 'bigint') return String(v);
    if (v instanceof Map) return Object.fromEntries(v);
    if (v instanceof Set) return [...v];
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return null;
      seen.add(v);
    }
    return v;
  })) as T;
}

/**
 * Empty pipeline-log label — never "starting…" for a finished/cancelled id.
 * - running + empty → starting…
 * - not running + empty (current or missing prior) → RUN_RECORD_MISSING
 * - prior with empty saved events → "no events saved for this epoch"
 */
export function emptyPipelineLogLabel(opts: {
  isCurrent?: boolean;
  running?: boolean;
  recordMissing?: boolean;
}): string {
  if (opts.isCurrent) {
    return opts.running ? 'starting…' : RUN_RECORD_MISSING;
  }
  if (opts.recordMissing) return RUN_RECORD_MISSING;
  return 'no events saved for this epoch';
}

/** SET 51b — validate SW persist ACK shape. */
export function parsePersistAck(resp: unknown): {
  ok: true;
  ack: true;
  id: string;
  eventCount?: number;
  status?: string | null;
  checkpointAt?: string | null;
  bytes?: number;
} | { ok: false; error: string } {
  if (resp == null || typeof resp !== 'object') {
    return { ok: false, error: 'no ACK (null response)' };
  }
  const r = resp as Record<string, unknown>;
  if (typeof r.error === 'string' && r.error) {
    return { ok: false, error: r.error };
  }
  if (r.ok !== true || r.ack !== true || typeof r.id !== 'string' || !r.id) {
    return { ok: false, error: 'ACK missing ok/ack/id' };
  }
  return {
    ok: true,
    ack: true,
    id: r.id,
    eventCount: typeof r.eventCount === 'number' ? r.eventCount : undefined,
    status: typeof r.status === 'string' ? r.status : null,
    checkpointAt: typeof r.checkpointAt === 'string' ? r.checkpointAt : null,
    bytes: typeof r.bytes === 'number' ? r.bytes : undefined,
  };
}

/**
 * SET 51c — merge upsert: never wipe prior events with an empty fallback.
 * Keep the longer event list; prefer next for scalar fields.
 */
export function mergePersistRecord<T extends { events?: unknown[] | null }>(
  prev: T | null | undefined,
  next: T,
): T {
  const prevEvents = Array.isArray(prev?.events) ? prev!.events! : [];
  const nextEvents = Array.isArray(next.events) ? next.events! : [];
  const events = nextEvents.length >= prevEvents.length ? nextEvents : prevEvents;
  return { ...(prev || {}), ...next, events } as T;
}

/** SET 51c — header-only last-resort record (no events / result). */
export function buildMinimalFallbackRecord(input: {
  id: string;
  ts?: string | null;
  startedAt?: string | null;
  model?: string | null;
  mode?: string | null;
  projectId?: string | null;
  parentId?: string | null;
  promptHash?: string | null;
  status?: PersistStatus | string | null;
  error?: string | null;
  title?: string | null;
}): {
  id: string;
  ts: string;
  startedAt: string | null;
  model: string | null;
  mode: string;
  projectId: string | null;
  parentId: string | null;
  promptHash: string | null;
  status: string;
  error: string | null;
  title: string;
  events: [];
  result: null;
  finalized: false;
  running: false;
  fallback: true;
} {
  const now = new Date().toISOString();
  return {
    id: input.id,
    ts: input.ts || now,
    startedAt: input.startedAt ?? null,
    model: input.model ?? null,
    mode: input.mode || 'rha',
    projectId: input.projectId ?? null,
    parentId: input.parentId ?? null,
    promptHash: input.promptHash ?? null,
    status: input.status || 'interrupted',
    error: input.error ?? null,
    title: input.title || '(persist fallback)',
    events: [],
    result: null,
    finalized: false,
    running: false,
    fallback: true,
  };
}

/** SET 51b — step label for persist-path debug lines. */
export function formatPersistStep(step: string, detail?: string): string {
  const d = (detail || '').trim();
  return d ? `persist:${step} — ${d}` : `persist:${step}`;
}
