// Work store — manages persistent work (content) definitions for AutoViral.
// D3: a Work is a content piece with module-as-capability semantics — no
// stage-coupled fields. The agent owns its own progress tracking via chat.
import { readFile, writeFile, mkdir, readdir, rm } from "node:fs/promises";
import { join, relative } from "node:path";
import yaml from "js-yaml";
import { dataDir } from "../infra/config.js";
/** Keys removed in D3 — silently dropped from update inputs and on read so
 *  legacy YAML files do not bleed back into the in-memory Work shape. */
const STRIP_KEYS = ["pipeline", "evaluationMode", "evalSessionIds", "evalAttempts"];
// ── Storage paths ────────────────────────────────────────────────────────────
const WORKS_BASE = join(dataDir, "works");
const INDEX_FILE = join(WORKS_BASE, "works.yaml");
async function ensureWorksDir() {
    await mkdir(WORKS_BASE, { recursive: true });
}
// ── Index helpers ────────────────────────────────────────────────────────────
async function readIndex() {
    await ensureWorksDir();
    try {
        const raw = await readFile(INDEX_FILE, "utf-8");
        const parsed = yaml.load(raw);
        return parsed ?? { works: [] };
    }
    catch {
        return { works: [] };
    }
}
async function writeIndex(data) {
    await ensureWorksDir();
    const raw = yaml.dump(data, { lineWidth: -1 });
    await writeFile(INDEX_FILE, raw, "utf-8");
}
// ── Per-work file helpers ────────────────────────────────────────────────────
function workDir(id) {
    return join(WORKS_BASE, id);
}
function workFilePath(id) {
    return join(workDir(id), "work.yaml");
}
function assetsDir(id) {
    return join(workDir(id), "assets");
}
async function readWorkFile(id) {
    try {
        const raw = await readFile(workFilePath(id), "utf-8");
        const parsed = yaml.load(raw);
        if (!parsed)
            return undefined;
        // Strip legacy stage-coupled fields on read so callers never see them.
        for (const k of STRIP_KEYS)
            delete parsed[k];
        return parsed;
    }
    catch {
        return undefined;
    }
}
async function writeWorkFile(work) {
    const dir = workDir(work.id);
    await mkdir(dir, { recursive: true });
    await mkdir(assetsDir(work.id), { recursive: true });
    const raw = yaml.dump(work, { lineWidth: -1, sortKeys: false });
    await writeFile(workFilePath(work.id), raw, "utf-8");
}
function toSummary(w) {
    return { id: w.id, title: w.title, type: w.type, contentCategory: w.contentCategory, platforms: w.platforms, status: w.status, updatedAt: w.updatedAt };
}
// ── ID generation ────────────────────────────────────────────────────────────
function generateId() {
    const now = new Date();
    const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
    const hex = Math.random().toString(16).slice(2, 5);
    return `w_${ts}_${hex}`;
}
// ── Public API ───────────────────────────────────────────────────────────────
export async function listWorks() {
    const index = await readIndex();
    return index.works;
}
export async function getWork(id) {
    return readWorkFile(id);
}
export async function createWork(input) {
    const now = new Date().toISOString();
    const id = generateId();
    const work = {
        id,
        title: input.title,
        type: input.type,
        contentCategory: input.contentCategory,
        videoSource: input.videoSource,
        videoSearchQuery: input.videoSearchQuery,
        status: input.videoSource === "search" ? "creating" : "draft",
        platforms: input.platforms,
        topicHint: input.topicHint,
        createdAt: now,
        updatedAt: now,
    };
    // Create workspace directories
    const wDir = join(dataDir, "works", id);
    await mkdir(join(wDir, "research"), { recursive: true });
    await mkdir(join(wDir, "plan"), { recursive: true });
    await mkdir(join(wDir, "assets", "frames"), { recursive: true });
    await mkdir(join(wDir, "assets", "clips"), { recursive: true });
    await mkdir(join(wDir, "assets", "images"), { recursive: true });
    await mkdir(join(wDir, "output"), { recursive: true });
    await writeWorkFile(work);
    // Update index
    const index = await readIndex();
    index.works.push(toSummary(work));
    await writeIndex(index);
    return work;
}
export async function updateWork(id, updates) {
    const work = await readWorkFile(id);
    if (!work)
        return undefined;
    const cleaned = { ...updates };
    for (const k of STRIP_KEYS)
        delete cleaned[k];
    const updated = { ...work, ...cleaned, id, updatedAt: new Date().toISOString() };
    await writeWorkFile(updated);
    // Sync index
    const index = await readIndex();
    const idx = index.works.findIndex((w) => w.id === id);
    const summary = toSummary(updated);
    if (idx >= 0) {
        index.works[idx] = summary;
    }
    else {
        index.works.push(summary);
    }
    await writeIndex(index);
    return updated;
}
export async function deleteWork(id) {
    const index = await readIndex();
    const before = index.works.length;
    index.works = index.works.filter((w) => w.id !== id);
    if (index.works.length === before)
        return false;
    await writeIndex(index);
    // Remove work directory
    try {
        await rm(workDir(id), { recursive: true, force: true });
    }
    catch {
        // directory may already be gone
    }
    return true;
}
/** Recursively list files in assets/ and output/ dirs, returning relative paths. */
export async function listAssets(id) {
    const results = [];
    const baseDir = workDir(id);
    async function walk(dir) {
        try {
            const entries = await readdir(dir, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = join(dir, entry.name);
                if (entry.isDirectory()) {
                    await walk(fullPath);
                }
                else {
                    results.push(relative(baseDir, fullPath));
                }
            }
        }
        catch {
            // directory may not exist yet
        }
    }
    await walk(join(baseDir, "assets"));
    await walk(join(baseDir, "output"));
    return results;
}
export function getAssetPath(id, filename) {
    return join(workDir(id), filename);
}
/** Save full conversation to chat.json (single file per work). */
export async function saveWorkChat(id, data) {
    await writeFile(join(workDir(id), "chat.json"), JSON.stringify(data), "utf-8");
}
/** The id of a work's first/legacy chat session. Kept in sync with
 *  ws-bridge's `DEFAULT_CHAT_SESSION_ID` — inlined (not imported) because
 *  ws-bridge imports THIS module, so importing it back would form a cycle. */
const DEFAULT_CHAT_SESSION_ID = "s_1";
/** Resolve the on-disk jsonl log for a (workId, sessionId). Mirrors ws-bridge's
 *  `chatLogPath`: the default/legacy session keeps `chat.jsonl`; other sessions
 *  use `chat-{sessionId}.jsonl`. */
function sessionJsonlPath(id, sessionId) {
    return sessionId === DEFAULT_CHAT_SESSION_ID
        ? join(workDir(id), "chat.jsonl")
        : join(workDir(id), `chat-${sessionId}.jsonl`);
}
/** Load full conversation for a work's chat session. Tries chat.json (single-
 *  shot snapshot saved by PUT /api/works/:id/chat) first ONLY for the default
 *  session, falling back to the session's jsonl (the live stream log appended
 *  by ws-bridge.appendToChatLog). Without the jsonl fallback, refreshing the
 *  studio page wiped the entire visible chat history because no caller actually
 *  writes chat.json today — only the jsonl path is on disk for live sessions.
 *
 *  `sessionId` defaults to the legacy default session so existing single-
 *  session callers keep reading chat.jsonl; passing a non-default session
 *  reads its `chat-{sessionId}.jsonl` instead, so a session-aware HTTP seed
 *  agrees with the WS `message_history` reseed (ADR-008 §4 / I24). */
export async function loadWorkChat(id, sessionId = DEFAULT_CHAT_SESSION_ID) {
    // chat.json is the legacy single-file snapshot — it only ever held the
    // default session, so non-default sessions skip it and read their jsonl.
    if (sessionId === DEFAULT_CHAT_SESSION_ID) {
        try {
            const raw = await readFile(join(workDir(id), "chat.json"), "utf-8");
            return JSON.parse(raw);
        }
        catch {
            /* fall through to jsonl */
        }
    }
    try {
        const raw = await readFile(sessionJsonlPath(id, sessionId), "utf-8");
        const blocks = raw
            .split("\n")
            .map((l) => l.trim())
            .filter((l) => l.length > 0)
            .map((line) => {
            try {
                const b = JSON.parse(line);
                // Normalize ISO timestamp → ms epoch so the client's hydration
                // path (which expects b.ts: number) shows real timestamps
                // instead of fabricated `Date.now() - i*1000` values.
                if (typeof b.timestamp === "string" && b.ts === undefined) {
                    const t = Date.parse(b.timestamp);
                    if (!Number.isNaN(t))
                        b.ts = t;
                }
                return b;
            }
            catch {
                return null;
            }
        })
            .filter((b) => b !== null);
        if (blocks.length === 0)
            return null;
        return { blocks };
    }
    catch {
        return null;
    }
}
// Evaluation result helpers were removed in the D3 cleanup — the evaluator was
// demoted from a gate to a read-only rubric tool (GET /api/works/:id/rubric/:module),
// so per-step EvalResult persistence has no consumer. Removed by Codex review
// follow-up 2026-04-27. If a future feature needs to log evaluator scores, add
// it back through a fresh, deliberate path (e.g. a single eval-log.jsonl).
//# sourceMappingURL=work-store.js.map