"use strict";
/**
 * agent-plans — persistence for `amplitude-wizard plan` output.
 *
 * The wizard's plan/apply/verify command split decouples "decide what to do"
 * from "actually do it." `plan` runs detection + planning, persists a JSON
 * blob keyed by a generated `planId`, and emits a `plan` NDJSON event so an
 * outer agent (Claude Code, Cursor, custom orchestrator) can inspect what
 * the wizard intends to do _before_ any files are touched.
 *
 * `apply --plan-id <id> --yes` then re-loads the plan, validates it's still
 * fresh, and runs the actual instrumentation flow against it. This is the
 * sub-agent contract the design doc calls for: outer agent proposes →
 * human approves → wizard executes.
 *
 * Storage is a flat directory under `$TMPDIR`. Plans expire after 24 hours
 * (matching the wizard session checkpoint TTL) so stale plans don't pile up
 * across runs. All writes go through `atomicWriteJSON` to avoid corruption
 * on crash.
 *
 * Privacy: plans contain framework, SDK choice, event names, and file paths
 * relative to the install dir. They never include credentials, tokens, or
 * absolute paths outside the install dir. Outer agents see only what the
 * `plan` NDJSON event carries.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.PLAN_TTL_MS = exports.WizardPlanSchema = exports.PlannedEventSchema = exports.FileChangeSchema = void 0;
exports.getPlansDir = getPlansDir;
exports.isPlanFresh = isPlanFresh;
exports.createAndPersistPlan = createAndPersistPlan;
exports.loadPlan = loadPlan;
exports.pruneStalePlans = pruneStalePlans;
const fs_1 = require("fs");
const path_1 = require("path");
const crypto_1 = require("crypto");
const zod_1 = require("zod");
const atomic_write_js_1 = require("../utils/atomic-write.js");
const storage_paths_js_1 = require("../utils/storage-paths.js");
// ── Plan shape ──────────────────────────────────────────────────────
/** A single file the inner agent intends to create or modify. */
exports.FileChangeSchema = zod_1.z.object({
    /** Path relative to `installDir`. Never absolute, never escapes the project root. */
    path: zod_1.z.string().min(1).max(512),
    operation: zod_1.z.enum(['create', 'modify', 'delete']),
    reason: zod_1.z.string().max(500).optional(),
});
/** A single event the inner agent intends to instrument. */
exports.PlannedEventSchema = zod_1.z.object({
    name: zod_1.z.string().min(1).max(80),
    description: zod_1.z.string().max(500),
});
exports.WizardPlanSchema = zod_1.z.object({
    /** Wire-format version. Bump on breaking shape changes. */
    v: zod_1.z.literal(1),
    /** Stable identifier the outer agent passes to `apply --plan-id`. */
    planId: zod_1.z.string().uuid(),
    /** ISO-8601 creation timestamp; used to enforce TTL. */
    createdAt: zod_1.z.string(),
    /** Install directory the plan was generated against (absolute, used for re-validation). */
    installDir: zod_1.z.string(),
    /** Framework integration the agent will instrument. */
    framework: zod_1.z.string(),
    /** Human-readable framework name. */
    frameworkName: zod_1.z.string().nullable(),
    /** SDK package the wizard will install. */
    sdk: zod_1.z.string().nullable(),
    /** Events the inner agent intends to track (best-effort; may be empty pre-agent). */
    events: zod_1.z.array(exports.PlannedEventSchema).default([]),
    /** Files the inner agent intends to touch (best-effort). */
    fileChanges: zod_1.z.array(exports.FileChangeSchema).default([]),
    /**
     * Whether this plan still requires explicit approval before `apply` will
     * execute. Always `true` for now; reserved for future flows where the
     * orchestrator pre-approves a class of plans.
     */
    requiresApproval: zod_1.z.literal(true).default(true),
});
// ── Storage location ────────────────────────────────────────────────
/**
 * Plans live under `<cacheRoot>/plans/`. The directory is created lazily on
 * first write.
 */
function getPlansDir() {
    return (0, storage_paths_js_1.getPlansDir)();
}
function ensurePlansDir() {
    const dir = getPlansDir();
    if (!(0, fs_1.existsSync)(dir)) {
        (0, fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
    }
    return dir;
}
function planPath(planId) {
    // Plan IDs are UUIDs we generated. Defensive: use only the basename so a
    // path-like input can't traverse outside the plans dir.
    return (0, storage_paths_js_1.getPlanFile)((0, path_1.basename)(planId));
}
// ── TTL ─────────────────────────────────────────────────────────────
/** 24 hours, matching the session-checkpoint TTL. */
exports.PLAN_TTL_MS = 24 * 60 * 60 * 1000;
function isPlanFresh(plan, now = Date.now()) {
    const created = Date.parse(plan.createdAt);
    if (Number.isNaN(created))
        return false;
    return now - created < exports.PLAN_TTL_MS;
}
function createAndPersistPlan(input) {
    ensurePlansDir();
    const plan = {
        v: 1,
        planId: (0, crypto_1.randomUUID)(),
        createdAt: new Date().toISOString(),
        installDir: input.installDir,
        framework: input.framework,
        frameworkName: input.frameworkName,
        sdk: input.sdk,
        events: input.events ?? [],
        fileChanges: input.fileChanges ?? [],
        requiresApproval: true,
    };
    (0, atomic_write_js_1.atomicWriteJSON)(planPath(plan.planId), plan, 0o600);
    return plan;
}
async function loadPlan(planId) {
    const path = planPath(planId);
    let raw;
    try {
        raw = await fs_1.promises.readFile(path, 'utf8');
    }
    catch (e) {
        const err = e;
        if (err.code === 'ENOENT')
            return { kind: 'not_found' };
        return { kind: 'invalid', reason: err.message };
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch (e) {
        return {
            kind: 'invalid',
            reason: `Plan file is not valid JSON: ${e.message}`,
        };
    }
    const result = exports.WizardPlanSchema.safeParse(parsed);
    if (!result.success) {
        return {
            kind: 'invalid',
            reason: `Plan failed schema validation: ${result.error.issues
                .map((i) => `${i.path.join('.')}: ${i.message}`)
                .join('; ')}`,
        };
    }
    if (!isPlanFresh(result.data)) {
        return { kind: 'expired', createdAt: result.data.createdAt };
    }
    return { kind: 'ok', plan: result.data };
}
/** Delete plans older than the TTL. Best-effort; errors are swallowed. */
async function pruneStalePlans(now = Date.now()) {
    const dir = getPlansDir();
    if (!(0, fs_1.existsSync)(dir))
        return 0;
    let pruned = 0;
    try {
        const entries = await fs_1.promises.readdir(dir);
        for (const entry of entries) {
            if (!entry.endsWith('.json'))
                continue;
            const path = (0, path_1.join)(dir, entry);
            try {
                const stat = await fs_1.promises.stat(path);
                if (now - stat.mtimeMs > exports.PLAN_TTL_MS) {
                    await fs_1.promises.unlink(path);
                    pruned++;
                }
            }
            catch {
                // Ignore individual file failures
            }
        }
    }
    catch {
        // Ignore directory read failures (race with cleanup, etc.)
    }
    return pruned;
}
