import fs from "node:fs";
import path from "node:path";
/**
 * Simple JSON file store for goals.
 * Goals persist across server restarts.
 */
export class GoalStore {
    storeDir;
    storeFile;
    goals = new Map();
    /** Monotonically increasing counter — bumped on every mutation. Resets to 0 on server restart. */
    generation = 0;
    constructor(stateDir) {
        this.storeDir = stateDir;
        this.storeFile = path.join(stateDir, "goals.json");
        this.load();
    }
    load() {
        try {
            if (fs.existsSync(this.storeFile)) {
                const data = JSON.parse(fs.readFileSync(this.storeFile, "utf-8"));
                if (Array.isArray(data)) {
                    for (const g of data) {
                        if (g.id) {
                            // Migrate legacy 'swarm' field to 'team'
                            if (g.swarm !== undefined && g.team === undefined) {
                                g.team = g.swarm;
                                delete g.swarm;
                            }
                            // Migrate skipArtifactRequirements → skipGateRequirements
                            if (g.skipArtifactRequirements && !g.skipGateRequirements) {
                                g.skipGateRequirements = g.skipArtifactRequirements;
                                delete g.skipArtifactRequirements;
                            }
                            // Default setupStatus for existing goals
                            if (!g.setupStatus) {
                                g.setupStatus = "ready";
                            }
                            this.goals.set(g.id, g);
                        }
                    }
                }
            }
        }
        catch (err) {
            console.error("[goal-store] Failed to load persisted goals:", err);
        }
    }
    save() {
        try {
            if (!fs.existsSync(this.storeDir)) {
                fs.mkdirSync(this.storeDir, { recursive: true });
            }
            const data = Array.from(this.goals.values());
            fs.writeFileSync(this.storeFile, JSON.stringify(data, null, 2), "utf-8");
        }
        catch (err) {
            console.error("[goal-store] Failed to save goals:", err);
        }
    }
    /** Current generation counter — bumped on every mutation. */
    getGeneration() {
        return this.generation;
    }
    /** Optional callback invoked after any goal mutation (put/update/archive). */
    onIndexUpdate;
    /**
     * Called once when a goal id appears in the store for the first time.
     * Wired by `ProjectContext.setGoalTriggerDispatcher` from `server.ts`.
     * MUST be assigned independently of `onIndexUpdate` — the search index
     * relies on the latter and must not be stomped.
     */
    onGoalCreated;
    /**
     * Called once per archive transition (false → true). Idempotent —
     * a second `archive` call on an already-archived goal does NOT fire.
     */
    onGoalArchived;
    /** Bump generation without mutating goal data (e.g. when gate status changes). */
    bumpGeneration() {
        this.generation++;
    }
    put(goal) {
        // Detect "new id" BEFORE the set so the goal_created callback fires
        // exactly once per id. Subsequent puts (updates) skip the callback.
        const isNew = !this.goals.has(goal.id);
        this.generation++;
        this.goals.set(goal.id, goal);
        this.save();
        this.onIndexUpdate?.(goal);
        if (isNew)
            this.onGoalCreated?.(goal);
    }
    get(id) {
        return this.goals.get(id);
    }
    remove(id) {
        this.generation++;
        this.goals.delete(id);
        this.save();
    }
    getAll() {
        return Array.from(this.goals.values());
    }
    archive(id) {
        const existing = this.goals.get(id);
        if (!existing)
            return false;
        // Capture the transition BEFORE mutating so onGoalArchived fires only
        // once. Idempotent: re-archiving an already-archived goal still returns
        // true (back-compat with existing callers) but does NOT re-fire.
        const wasAlreadyArchived = existing.archived === true;
        this.generation++;
        existing.archived = true;
        existing.archivedAt = Date.now();
        this.save();
        this.onIndexUpdate?.(existing);
        if (!wasAlreadyArchived)
            this.onGoalArchived?.(existing);
        return true;
    }
    getLive() {
        return Array.from(this.goals.values()).filter(g => !g.archived);
    }
    getArchived() {
        return Array.from(this.goals.values()).filter(g => g.archived === true);
    }
    update(id, updates) {
        const existing = this.goals.get(id);
        if (!existing)
            return false;
        this.generation++;
        // Strip undefined values to avoid overwriting existing fields
        const cleaned = {};
        for (const [k, v] of Object.entries(updates)) {
            if (v !== undefined)
                cleaned[k] = v;
        }
        Object.assign(existing, cleaned, { updatedAt: Date.now() });
        this.save();
        this.onIndexUpdate?.(existing);
        return true;
    }
    /**
     * Paginated listing of archived goals, sorted by archivedAt DESC.
     * @param limit Max items per page
     * @param afterCursor archivedAt timestamp — return items with archivedAt < cursor
     */
    listArchivedGoalsPaginated(limit, afterCursor) {
        let archived = this.getArchived().sort((a, b) => (b.archivedAt ?? 0) - (a.archivedAt ?? 0));
        const total = archived.length;
        if (afterCursor !== undefined) {
            archived = archived.filter(g => (g.archivedAt ?? 0) < afterCursor);
        }
        const page = archived.slice(0, limit);
        const hasMore = archived.length > limit;
        const nextCursor = page.length > 0 ? page[page.length - 1].archivedAt : undefined;
        return { goals: page, total, hasMore, nextCursor };
    }
}
//# sourceMappingURL=goal-store.js.map