// Node.js 22.14+. Trusted server/local use only; never ship an API key to a browser.
// With no ANIMGEN_APPROVE_CREDITS this only prepares and quotes. A local quote
// check is NOT a server-enforced spending cap. Resume with the same private state.
import { readFile, writeFile, mkdir, rename, unlink, lstat, stat, open, link } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { dirname, extname, join } from "node:path";
import { pathToFileURL } from "node:url";

type Model = { id: string; modes: string[]; supports_first_frame: boolean; default_duration_seconds: number; default_resolution: string | null; default_ratio: string | null };
type Account = { id: string; api_key: { id: string } };
type Asset = { id: string; mime_type: string; byte_size: number; download_url: string };
type Task = { id: string; status: string; outputs: Asset[]; error?: { code: string } | null };
type State = { version: 1; apiBase: string; accountId: string; keyId: string; body: Record<string, unknown>; bodyHash: string; idempotencyKey: string; taskId: string | null; firstCreateAt: number | null };
class SafeError extends Error {}
const safe = (value: unknown) => /^[\w.-]{1,100}$/.test(String(value)) ? String(value) : "unknown";
const hash = (body: unknown) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
const uuid = (value: string) => {
  if (!/^[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value)) throw new SafeError("Invalid public resource ID.");
  return value;
};
export function retryDelay(value: string | null, fallback: number): number {
  if (value === null) return fallback;
  const seconds = Number(value);
  if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
  const date = Date.parse(value);
  return Number.isFinite(date) ? Math.max(0, date - Date.now()) : fallback;
}

export async function runExample(
  env: Record<string, string | undefined> = process.env,
  fetchRequest: typeof fetch = globalThis.fetch,
  sleep: (ms: number) => Promise<void> = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
): Promise<number> {
  const apiKey = env.ANIMGEN_API_KEY;
  if (!apiKey) throw new SafeError("Set ANIMGEN_API_KEY privately.");
  const apiBase = (env.ANIMGEN_API_BASE ?? "https://api.animgen.com/v1").replace(/\/+$/, "");
  const base = new URL(apiBase);
  const local = ["localhost", "127.0.0.1", "[::1]"].includes(base.hostname);
  if (base.username || base.password || base.search || base.hash || (base.protocol !== "https:" && !(base.protocol === "http:" && local))) throw new SafeError("Use a clean HTTPS API base URL; HTTP is only for local mocks.");
  const timeout = Number(env.ANIMGEN_TIMEOUT_SECONDS ?? "900");
  if (!Number.isFinite(timeout) || timeout <= 0) throw new SafeError("Invalid timeout.");
  const deadline = Date.now() + timeout * 1000;
  const remaining = () => {
    const ms = deadline - Date.now();
    if (ms <= 0) throw new SafeError("Local deadline reached. Resume the SAME state; remote work may continue.");
    return Math.max(1, Math.min(30000, ms));
  };
  const wait = async (ms: number) => {
    if (Date.now() + Math.max(50, ms) >= deadline) throw new SafeError("Retry-After exceeds this deadline; resume later with the SAME state.");
    await sleep(Math.max(50, ms));
  };
  async function api<T>(path: string, method = "GET", body?: unknown, idempotencyKey?: string): Promise<{ body: T; response: Response }> {
    if (method !== "GET" && !path.endsWith("/quote") && !idempotencyKey) throw new SafeError("Refusing an unkeyed retryable write.");
    for (let attempt = 0; attempt < 5; attempt++) {
      let response: Response;
      try {
        response = await fetchRequest(apiBase + path, {
          method, redirect: "error", signal: AbortSignal.timeout(remaining()),
          headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}), ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}) },
          body: body ? JSON.stringify(body) : undefined,
        });
      } catch (error) {
        if (error instanceof SafeError) throw error;
        if (attempt === 4) throw new SafeError("API transport failed. Keep the original state and credential identity.");
        await wait(2 ** attempt * 1000);
        continue;
      }
      const value = await response.json() as T & { error?: { code?: string; retryable?: boolean; request_id?: string } };
      if (response.ok) return { body: value, response };
      const retryable = response.status === 429 || (value.error?.retryable === true && (response.status === 503 || (response.status === 409 && value.error.code === "IDEMPOTENCY_IN_PROGRESS")));
      if (!retryable || attempt === 4) throw new SafeError(`API HTTP ${response.status}: ${safe(value.error?.code)}; request_id=${safe(value.error?.request_id)}`);
      await wait(retryDelay(response.headers.get("Retry-After"), 2 ** attempt * 1000 + Math.random() * 500));
    }
    throw new SafeError("Retry limit reached.");
  }
  const statePath = env.ANIMGEN_STATE ?? "animgen-state.json";
  let state: State | undefined;
  try { state = JSON.parse(await readFile(statePath, "utf8")) as State; }
  catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw new SafeError("Invalid private state file."); }
  async function saveState(newFile = false) {
    if (newFile) { await writeFile(statePath, JSON.stringify(state, null, 2), { flag: "wx", mode: 0o600 }); return; }
    const temporary = join(dirname(statePath), `.animgen-state-${randomUUID()}.tmp`);
    try { await writeFile(temporary, JSON.stringify(state, null, 2), { flag: "wx", mode: 0o600 }); await rename(temporary, statePath); }
    finally { await unlink(temporary).catch(() => undefined); }
  }
  const { body: account } = await api<Account>("/account");
  if (!state) {
    if (!env.IMAGE_PATH || !env.ANIMGEN_MODEL) throw new SafeError("Set IMAGE_PATH and ANIMGEN_MODEL from GET /models to prepare a new operation.");
    const mime = ({ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" } as Record<string, string>)[extname(env.IMAGE_PATH).toLowerCase()];
    if (!mime || (await stat(env.IMAGE_PATH)).size > 10 * 1024 * 1024) throw new SafeError("Use a PNG/JPEG/WebP no larger than 10 MiB.");
    const { body: models } = await api<{ data: Model[] }>("/models");
    const model = models.data.find((item) => item.id === env.ANIMGEN_MODEL && item.supports_first_frame && item.modes.includes("first_frame"));
    if (!model) throw new SafeError("Choose a live model supporting first-frame input.");
    const body = {
      input: { first_frame: { type: "base64", media_type: mime, data: (await readFile(env.IMAGE_PATH)).toString("base64") } },
      prompt: env.ANIMGEN_PROMPT ?? "A character runs in place, side view, fixed camera.",
      video: { model: model.id, duration_seconds: model.default_duration_seconds, resolution: model.default_resolution, ratio: model.default_ratio },
      selection: { mode: "full" }, export: { output_formats: ["frames_zip"], frame_count: 24, output_width: 512, output_height: 512 },
    };
    state = { version: 1, apiBase, accountId: account.id, keyId: account.api_key.id, body, bodyHash: hash(body), idempotencyKey: randomUUID(), taskId: null, firstCreateAt: null };
    await saveState(true);
  }
  if (state.version !== 1 || state.apiBase !== apiBase || state.accountId !== account.id || state.bodyHash !== hash(state.body)) throw new SafeError("State/account/base/request mismatch. Recover the original task; do not create again.");
  if (!state.taskId) {
    if (state.keyId !== account.api_key.id) throw new SafeError("API key identity changed; idempotency does not cross keys.");
    if (state.firstCreateAt !== null && Date.now() - state.firstCreateAt >= 86400000) throw new SafeError("Uncertain create is older than 24 hours. Inspect existing tasks before any new create.");
    if (state.firstCreateAt === null) {
      const { body: quote } = await api<{ credits: number }>("/animations/quote", "POST", state.body);
      const { body: balance } = await api<{ available: number }>("/credits/balance");
      console.log(`Quoted credits: ${quote.credits}; available: ${balance.available}. Local preflight only, not a server spending cap.`);
      if (env.ANIMGEN_APPROVE_CREDITS === undefined) { console.log("No generation started. Review and set ANIMGEN_APPROVE_CREDITS, then resume the SAME state."); return 0; }
      const approved = Number(env.ANIMGEN_APPROVE_CREDITS);
      if (!Number.isSafeInteger(approved) || approved < 0 || quote.credits > approved) throw new SafeError("Current quote exceeds a valid explicit local approval. No create sent.");
      state.firstCreateAt = Date.now();
      await saveState();
    }
    const { body: created } = await api<Task>("/animations", "POST", state.body, state.idempotencyKey);
    state.taskId = uuid(created.id);
    await saveState();
  }
  let task: Task;
  while (true) {
    const result = await api<Task>(`/animations/${uuid(state.taskId)}`);
    task = result.body;
    console.log(`Task ${state.taskId}: ${safe(task.status)}`);
    if (["succeeded", "failed", "cancelled"].includes(task.status)) break;
    if (!["queued", "running", "cancelling"].includes(task.status)) throw new SafeError("Unknown task status; keep the state.");
    await wait(retryDelay(result.response.headers.get("Retry-After"), 5000));
  }
  const output = env.ANIMGEN_OUTPUT ?? "animgen-output";
  await mkdir(output, { recursive: true });
  let failed = task.outputs.length === 0;
  for (const item of task.outputs) {
    try {
      let saved = false;
      for (let refresh = 0; refresh < 2 && !saved; refresh++) {
        const { body: asset } = await api<Asset>(`/assets/${uuid(item.id)}`);
        if (!Number.isSafeInteger(asset.byte_size) || asset.byte_size < 0) throw new SafeError("Invalid asset size.");
        const suffix = ({ "application/zip": ".zip", "image/png": ".png", "application/json": ".json", "video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov" } as Record<string, string>)[asset.mime_type] ?? ".bin";
        const target = join(output, uuid(asset.id) + suffix);
        try {
          const existing = await lstat(target);
          if (!existing.isFile() || existing.isSymbolicLink() || existing.size !== asset.byte_size) throw new SafeError("Existing output does not match.");
          saved = true;
          continue;
        } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
        let url = new URL(asset.download_url);
        let response: Response | undefined;
        for (let redirects = 0; redirects <= 3; redirects++) {
          if (url.username || url.password || url.hash || (url.protocol !== "https:" && !(local && url.origin === base.origin))) throw new SafeError("Unsafe download URL.");
          response = await fetchRequest(url, { redirect: "manual", credentials: "omit", signal: AbortSignal.timeout(remaining()) }); // Deliberately no API headers.
          if (![301, 302, 303, 307, 308].includes(response.status)) break;
          await response.body?.cancel();
          if (redirects === 3 || !response.headers.get("Location")) throw new SafeError("Unsafe download redirect chain.");
          url = new URL(response.headers.get("Location")!, url);
        }
        if (response && [401, 403, 404].includes(response.status) && refresh === 0) { await response.body?.cancel(); continue; }
        if (!response?.ok || !response.body) throw new SafeError("Asset transfer failed; signed URL omitted.");
        const temporary = join(output, `.animgen-download-${randomUUID()}.part`);
        const file = await open(temporary, "wx", 0o600);
        const reader = response.body.getReader();
        try {
          let bytes = 0;
          while (true) {
            remaining();
            const chunk = await reader.read();
            if (chunk.done) break;
            bytes += chunk.value.byteLength;
            if (bytes > asset.byte_size) throw new SafeError("Asset exceeds declared byte count.");
            await file.writeFile(chunk.value);
          }
          if (bytes !== asset.byte_size) throw new SafeError("Incomplete asset transfer.");
          await file.close();
          await link(temporary, target); // Exclusive: never overwrite an unrelated file.
          saved = true;
          console.log(`Saved asset: ${target}`);
        } finally { await reader.cancel().catch(() => undefined); await file.close().catch(() => undefined); await unlink(temporary).catch(() => undefined); }
      }
      if (!saved) throw new SafeError("Asset could not be saved.");
    } catch { failed = true; console.error(`Could not save asset ${safe(item.id)}; resume the SAME state.`); }
  }
  if (task.status !== "succeeded") console.error(`Task incomplete: ${safe(task.error?.code ?? task.status)}. Available outputs were processed.`);
  return task.status === "succeeded" && !failed ? 0 : 2;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  runExample().then((code) => { process.exitCode = code; }).catch((error) => {
    console.error(error instanceof SafeError ? error.message : "Workflow failed; preserve the private state. Sensitive details omitted.");
    process.exitCode = 1;
  });
}
