// Cleaned, trimmed adaptation of the real production script referenced as evidence in
// "118 Clips Were Ready. The Documentary Still Would Not Publish."
// https://kittyclaw.dev/recover-a-stalled-video-render-without-rebuilding-it
//
// This keeps only the recovery-relevant mechanics: aggregating node status, classifying
// each node with the preserve/retry/wait/validate matrix, retrying only what is broken while
// leaving everything valid untouched, rebuilding the affected part of a merge cascade, and
// wiring the gate/publish/callback tail. All local paths and internal service hostnames are
// read from environment variables. The original 23-scene creative content has been replaced
// with two placeholder scenes for illustration; this file is meant to be read and adapted,
// not run as-is against a real production engine.
//
// Phases:
//   node build-script.example.mjs status   # aggregate node statuses of the request
//   node build-script.example.mjs retry    # classify + retry only the failed/blocked nodes

import { readFileSync, writeFileSync, existsSync } from 'fs';

const ENGINE_URL = process.env.PRODUCTION_ENGINE_URL || 'https://engine.example.invalid';
const CALLBACK_URL = process.env.SOURCE_TICKET_CALLBACK_URL || 'https://kittyclaw.example.invalid/api/projects/REPLACE_ME/tickets/REPLACE_ME/comments';
const META_FILE = process.env.PRODUCTION_META_FILE || './node-manifest.example.json';

// Two placeholder scenes stand in for the real 23-scene shot list.
const SCENES = [
  { scene: 1, ref: 'reference-a', imagePrompt: 'placeholder scene 1 description', clipPrompt: 'placeholder six-second motion description for scene 1' },
  { scene: 2, ref: 'reference-b', imagePrompt: 'placeholder scene 2 description', clipPrompt: 'placeholder six-second motion description for scene 2' },
];

async function api(method, urlPath, body) {
  const opts = { method };
  if (body !== undefined) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(body); }
  const res = await fetch(ENGINE_URL + urlPath, opts);
  const text = await res.text();
  if (!res.ok) throw new Error(`${method} ${urlPath} -> ${res.status}: ${text.slice(0, 300)}`);
  try { return JSON.parse(text); } catch { return text; }
}

const loadMeta = () => existsSync(META_FILE) ? JSON.parse(readFileSync(META_FILE, 'utf8')) : (() => { throw new Error('run against a real manifest, not this placeholder'); })();
const saveMeta = (m) => writeFileSync(META_FILE, JSON.stringify(m, null, 2));

// ── Diagnosis: aggregate node status across the whole production request ────────
async function phaseStatus() {
  const meta = loadMeta();
  const req = await api('GET', `/requests/${meta.pipeline.requestId}`);
  const byType = {};
  for (const n of req.nodes) {
    byType[n.type] ??= {};
    byType[n.type][n.status] = (byType[n.type][n.status] || 0) + 1;
  }
  console.table(byType);
  const failed = req.nodes.filter((n) => n.status === 'Failed');
  for (const n of failed) console.log(`FAILED ${n.type} #${n.id}: ${(n.errorMessage || '').slice(0, 160)}`);
  return { req, failed };
}

// ── Preserve / retry / wait / validate classification for one segment ───────────
// See preserve-retry-wait-validate-matrix.md for the rule this encodes.
function classifySegment(segment, nodeStatusById) {
  const clipStatuses = segment.clipNodeIds.map((id) => nodeStatusById.get(id));
  if (clipStatuses.every((s) => s === 'Done')) return 'preserve';
  if (clipStatuses.some((s) => s === 'Failed')) return 'retry';
  if (clipStatuses.some((s) => s === 'Queued' || s === 'Inactive')) return 'wait';
  return 'retry';
}

// 6 slots per merge node; never leave a single-item chunk dangling on its own.
function chunk6(ids) {
  const out = [];
  for (let i = 0; i < ids.length; i += 6) out.push(ids.slice(i, i + 6));
  if (out.length > 1 && out[out.length - 1].length === 1) {
    out[out.length - 1].unshift(out[out.length - 2].pop());
  }
  return out;
}

// ── Retry only what the diagnosis names as broken, preserve everything else ─────
async function phaseRetry() {
  const meta = loadMeta();
  const { req } = await phaseStatus();
  const nodeStatusById = new Map(req.nodes.map((n) => [n.id, n.status]));

  const preserved = [];
  const retried = [];
  for (const segment of meta.segments) {
    const action = classifySegment(segment, nodeStatusById);
    if (action === 'preserve') { preserved.push(segment.scene); continue; }
    if (action === 'wait') { console.log(`scene ${segment.scene}: waiting on an upstream/external condition, not retrying`); continue; }

    const example = SCENES[(segment.scene - 1) % SCENES.length];
    const image = await api('POST', `/requests/${meta.pipeline.requestId}/nodes`, {
      type: 'ImageGeneration', prompt: example.imagePrompt,
    });
    const clip = await api('POST', `/requests/${meta.pipeline.requestId}/nodes`, {
      type: 'ClipGeneration', prompt: example.clipPrompt, edges: { reference: image.id },
    });
    segment.imageNodeId = image.id;
    segment.clipNodeIds = [clip.id];
    retried.push(segment.scene);
  }
  console.log(`preserved ${preserved.length} scenes untouched, retried ${retried.length} scenes: [${retried.join(', ')}]`);

  // Rebuild the merge cascade over the full, now-consistent clip list.
  const allClipIds = meta.segments.flatMap((s) => s.clipNodeIds);
  let level = allClipIds, levelNum = 0;
  const mergeLevels = [];
  while (level.length > 1) {
    const groups = chunk6(level);
    const ids = [];
    for (const group of groups) {
      const edges = {};
      group.forEach((id, k) => { edges[`clip${k}`] = id; });
      const node = await api('POST', `/requests/${meta.pipeline.requestId}/nodes`, { type: 'ConcatVideo', edges });
      ids.push(node.id);
    }
    mergeLevels.push(ids);
    level = ids; levelNum++;
  }
  const rootMerge = level[0];

  // Rewire the final assembly onto the rebuilt cascade; the gate, publish and callback nodes
  // are untouched, they only need the upstream video input to point at the new root.
  await api('PUT', `/nodes/${meta.pipeline.assembleNodeId}/inputs/video`, { fromNodeId: rootMerge });

  meta.pipeline.mergeLevels = mergeLevels;
  saveMeta(meta);
  console.log(`retry done: assemble ${meta.pipeline.assembleNodeId} rewired -> root ${rootMerge}`);
  console.log(`next: run the master QC checklist once the assemble node reaches Done, then open the gate`);
}

// ── Confirm the tail: gate cleared, published, callback fired ───────────────────
async function confirmPublished(meta) {
  const req = await api('GET', `/requests/${meta.pipeline.requestId}`);
  const byId = new Map(req.nodes.map((n) => [n.id, n]));
  const gate = byId.get(meta.pipeline.gateNodeId);
  const publish = byId.get(meta.pipeline.publishNodeId);
  const callback = byId.get(meta.pipeline.callbackNodeId);
  if (gate?.status !== 'Done') throw new Error('gate not cleared yet');
  if (publish?.status !== 'Done') throw new Error('publish node not done yet');
  if (callback?.status !== 'Done') throw new Error('callback not fired yet');
  console.log('gate, publish and callback are all Done - safe to post the delivery comment');
}

const phase = process.argv[2];
if (phase === 'status') await phaseStatus();
else if (phase === 'retry') await phaseRetry();
else if (phase === 'confirm') await confirmPublished(loadMeta());
else { console.log('usage: node build-script.example.mjs status|retry|confirm'); process.exit(1); }
