import { mkdir, readFile, writeFile, readdir, rename, unlink, } from "fs/promises";
import { dirname, isAbsolute, join, relative, resolve } from "path";
const INACTIVE_STATUSES = ["done", "failed", "cancelled", "closed"];
function safePath(basePath, inputPath) {
    if (isAbsolute(inputPath)) {
        throw new Error(`GoalPersister: absolute paths are not allowed: ${inputPath}`);
    }
    const full = resolve(join(basePath, inputPath));
    const rel = relative(basePath, full);
    if (rel.startsWith("..") || rel === "..") {
        throw new Error(`GoalPersister: path traversal detected: ${inputPath}`);
    }
    return full;
}
function goalDir(basePath, goalId) {
    const sanitized = goalId.replace(/[^a-zA-Z0-9_.-]/g, "");
    if (sanitized !== goalId || sanitized.length === 0 || sanitized.length > 128) {
        throw new Error(`Invalid goalId: ${goalId}`);
    }
    return safePath(basePath, sanitized);
}
function atomicWrite(filePath, content) {
    return new Promise((resolve, reject) => {
        const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
        mkdir(dirname(filePath), { recursive: true })
            .then(() => writeFile(tempPath, content, "utf-8"))
            .then(() => rename(tempPath, filePath))
            .then(() => resolve())
            .catch(async (err) => {
            try {
                await unlink(tempPath);
            }
            catch {
                // ignore cleanup error
            }
            reject(err);
        });
    });
}
export function createGoalPersister(basePath = ".omk/goals") {
    return {
        async load(goalId) {
            const filePath = join(goalDir(basePath, goalId), "goal.json");
            try {
                const content = await readFile(filePath, "utf-8");
                const parsed = JSON.parse(content);
                if (!parsed || typeof parsed !== "object") {
                    console.warn(`GoalPersister: corrupt JSON for ${goalId} — not an object`);
                    return null;
                }
                const obj = parsed;
                if (obj.schemaVersion !== 1) {
                    console.warn(`GoalPersister: unsupported schemaVersion for ${goalId}: ${obj.schemaVersion}`);
                    return null;
                }
                return parsed;
            }
            catch (err) {
                if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
                    return null;
                }
                console.warn(`GoalPersister: failed to load ${goalId}: ${err instanceof Error ? err.message : String(err)}`);
                return null;
            }
        },
        async save(spec) {
            spec.updatedAt = new Date().toISOString();
            const dir = goalDir(basePath, spec.goalId);
            await mkdir(dir, { recursive: true });
            const filePath = join(dir, "goal.json");
            await atomicWrite(filePath, JSON.stringify(spec, null, 2));
        },
        async list() {
            try {
                const entries = await readdir(basePath, { withFileTypes: true });
                const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
                const withMtime = await Promise.all(dirs.map(async (id) => {
                    const spec = await this.load(id);
                    return { id, updatedAt: spec?.updatedAt ?? "1970-01-01T00:00:00.000Z" };
                }));
                return withMtime
                    .filter((g) => g.updatedAt !== "1970-01-01T00:00:00.000Z")
                    .sort((a, b) => {
                    const timeDiff = b.updatedAt.localeCompare(a.updatedAt);
                    return timeDiff !== 0 ? timeDiff : b.id.localeCompare(a.id);
                })
                    .map((g) => g.id);
            }
            catch {
                return [];
            }
        },
        async loadEvidence(goalId) {
            const filePath = join(goalDir(basePath, goalId), "evidence.json");
            try {
                const content = await readFile(filePath, "utf-8");
                return JSON.parse(content);
            }
            catch {
                return [];
            }
        },
        async saveEvidence(goalId, evidence) {
            const dir = goalDir(basePath, goalId);
            await mkdir(dir, { recursive: true });
            const filePath = join(dir, "evidence.json");
            await atomicWrite(filePath, JSON.stringify(evidence, null, 2));
        },
        async appendHistory(goalId, entry) {
            const dir = goalDir(basePath, goalId);
            await mkdir(dir, { recursive: true });
            const filePath = join(dir, "history.jsonl");
            const line = JSON.stringify(entry) + "\n";
            await writeFile(filePath, line, { flag: "a", encoding: "utf-8" });
        },
        async loadLatestActive() {
            try {
                const entries = await readdir(basePath, { withFileTypes: true });
                const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
                const specs = await Promise.all(dirs.map(async (id) => {
                    const spec = await this.load(id);
                    return { id, spec };
                }));
                const active = specs
                    .filter((s) => s.spec !== null && !INACTIVE_STATUSES.includes(s.spec.status))
                    .sort((a, b) => b.spec.updatedAt.localeCompare(a.spec.updatedAt));
                return active[0]?.spec ?? null;
            }
            catch {
                return null;
            }
        },
    };
}
