/**
 * PromptPlan — structured prompt composition with phases, steps, and sub-steps.
 *
 * Design goals:
 * - Main steps are always non-conditional; their labels (1, 2, 3, …) are
 *   static string constants usable in tests without calling any function.
 * - Sub-steps are sequentially numbered within their parent (1.1, 1.2, 2.1, …).
 *   No manually chosen suffixes — insertion or removal automatically renumbers.
 * - Conditional sub-steps: when absent, remaining siblings re-number so the
 *   LLM never sees a gap in the step sequence.
 * - Phases group steps under an optional named heading with a configurable
 *   rendering style.
 * - preview(params) returns a structured snapshot for developer inspection:
 *   which steps are active, what their labels are, and why conditional ones
 *   are absent.
 *
 * Usage:
 *   const _plan = new PromptPlan<MyParams>()
 *     .addPhase("main", "Task Instructions", { headerLevel: "##", stepFormat: "hash" })
 *     .step("READ",   "Read the files",   (p) => buildReadBody(p))
 *     .subStep("PATHS", "Resolve paths",  (p) => buildPathsBody(p))
 *     .step("EXTRACT", "Extract items",   (p) => buildExtractBody(p))
 *     .subStep("TRACE", "Trace callers",  (p) => buildTraceBody(p),
 *       { when: (p) => p.hasUnmatched, whenDesc: "unmatched backend files present" })
 *     .step("CALL",   "Call the tool",    (p) => buildCallBody(p))
 *     .done();
 *
 *   // Static constants for non-conditional steps (safe to export at module level)
 *   export const STEP_READ    = _plan.labels.READ;    // "1"
 *   export const STEP_PATHS   = _plan.labels.PATHS;   // "1.1"
 *   export const STEP_EXTRACT = _plan.labels.EXTRACT; // "2"
 *   export const STEP_CALL    = _plan.labels.CALL;    // "3"
 *
 *   // Dynamic accessor for the conditional sub-step
 *   export const getStepTrace = (p: MyParams) => _plan.labelFor("TRACE", p); // "2.1" | null
 *
 *   // Render for the LLM
 *   const promptText = _plan.render(params);
 *
 *   // Developer review
 *   console.log(_plan.preview(params));
 */
// ─────────────────────────────────────────────────────────────────────────────
export class PhaseBuilder {
    _plan;
    _phase;
    _lastStep = null;
    constructor(plan, phase) {
        this._plan = plan;
        this._phase = phase;
    }
    /**
     * Add a non-conditional main step.
     * Main steps always appear in the rendered output and always carry a stable
     * label (1, 2, 3, …) regardless of which sub-steps are active.
     */
    step(key, title, body) {
        this._plan._registerKey(key);
        const def = { key, title, body, subSteps: [] };
        this._phase.steps.push(def);
        this._lastStep = def;
        return this;
    }
    /**
     * Add a sub-step under the most recently declared main step.
     * Sub-steps are sequentially numbered within their parent (1.1, 1.2, …).
     *
     * When opts.when is provided the sub-step is conditional: if the condition
     * returns false for the given params, the step is omitted and remaining
     * siblings re-number so the LLM never sees a gap.
     *
     * @throws when called before any step() in this phase.
     */
    subStep(key, title, body, opts) {
        if (!this._lastStep) {
            throw new Error(`subStep("${key}") must follow a step() call in the same phase`);
        }
        this._plan._registerKey(key);
        this._lastStep.subSteps.push({
            key,
            title,
            body,
            condition: opts?.when,
            conditionDesc: opts?.whenDesc,
        });
        return this;
    }
    /** Finish building this phase and return the parent PromptPlan. */
    done() {
        return this._plan;
    }
}
// ─────────────────────────────────────────────────────────────────────────────
export class PromptPlan {
    _phases = [];
    _startFrom;
    /** Track registered step/sub-step keys for duplicate detection. */
    _registeredKeys = new Set();
    /**
     * @param opts.startFrom - Label of the first main step. Defaults to 1.
     *   Use 0 for execution-plan style (Step 0, Step 1, Step 2, …).
     */
    constructor(opts) {
        this._startFrom = opts?.startFrom ?? 1;
    }
    /** @internal Register a key and throw if already taken. */
    _registerKey(key) {
        if (this._registeredKeys.has(key)) {
            throw new Error(`PromptPlan: duplicate step key "${key}"`);
        }
        this._registeredKeys.add(key);
    }
    /**
     * Add a phase and return a PhaseBuilder to register its steps.
     * Multiple phases share a single global main-step counter, so step labels
     * increment continuously across phase boundaries.
     */
    addPhase(key, title, opts) {
        const phase = {
            key,
            title,
            headerLevel: opts?.headerLevel ?? "hidden",
            stepFormat: opts?.stepFormat ?? "hash",
            steps: [],
        };
        this._phases.push(phase);
        return new PhaseBuilder(this, phase);
    }
    /**
     * Static label map for all non-conditional steps (main steps and
     * unconditional sub-steps). Conditional sub-steps are not present here —
     * use labelFor() for those.
     *
     * Safe to export as module-level constants for use in tests.
     *
     * **Ordering constraint**: unconditional sub-steps that appear *after* a
     * conditional sibling will have a stable label here only if no conditional
     * siblings precede them. When a conditional sibling is active at runtime,
     * labelFor() will assign a higher number than the static label here. To
     * avoid this mismatch, place conditional sub-steps AFTER all unconditional
     * siblings, or use labelFor() for any sub-step with a conditional predecessor.
     *
     * O(steps) per call — intentionally avoids a stale-cache window during plan
     * construction. Plans have ~6 steps so the cost is negligible; exported
     * constants are evaluated once at module-load time.
     */
    get labels() {
        const map = {};
        let mainIdx = this._startFrom - 1;
        for (const phase of this._phases) {
            for (const step of phase.steps) {
                mainIdx++;
                map[step.key] = String(mainIdx);
                let subIdx = 0;
                for (const sub of step.subSteps) {
                    if (!sub.condition) {
                        subIdx++;
                        map[sub.key] = `${mainIdx}.${subIdx}`;
                    }
                }
            }
        }
        return map;
    }
    /**
     * Dynamic label for any registered step given runtime params.
     *
     * Returns null when the step has a condition that evaluates to false.
     * Throws when key is not registered in this plan.
     *
     * For non-conditional steps the result equals labels[key], but using
     * labels is preferred for those since it avoids passing params.
     */
    labelFor(key, params) {
        let mainIdx = this._startFrom - 1;
        for (const phase of this._phases) {
            for (const step of phase.steps) {
                mainIdx++;
                if (step.key === key)
                    return String(mainIdx);
                let subIdx = 0;
                for (const sub of step.subSteps) {
                    const active = !sub.condition || sub.condition(params);
                    if (active)
                        subIdx++;
                    if (sub.key === key) {
                        return active ? `${mainIdx}.${subIdx}` : null;
                    }
                }
            }
        }
        throw new Error(`PromptPlan: unknown step key "${key}"`);
    }
    /**
     * Render the full prompt for the given params.
     *
     * - Phase headers are emitted when headerLevel is not "hidden".
     * - Steps are formatted per the phase's stepFormat.
     * - Conditional sub-steps absent for these params are skipped; remaining
     *   siblings re-number with no gaps.
     * - Each section (header + body) is separated by a single blank line.
     */
    render(params) {
        const sections = [];
        let mainIdx = this._startFrom - 1;
        for (const phase of this._phases) {
            if (phase.headerLevel !== "hidden") {
                sections.push(`${phase.headerLevel} ${phase.title}`);
            }
            for (const step of phase.steps) {
                mainIdx++;
                const label = String(mainIdx);
                const title = resolveTitle(step.title, params);
                const body = step.body(params);
                sections.push(`${renderStepHeader(phase.stepFormat, label, title)}\n${body.trimEnd()}`);
                let subIdx = 0;
                for (const sub of step.subSteps) {
                    if (sub.condition && !sub.condition(params))
                        continue;
                    subIdx++;
                    const subLabel = `${label}.${subIdx}`;
                    const subTitle = resolveTitle(sub.title, params);
                    const subBody = sub.body(params);
                    sections.push(`${renderStepHeader(phase.stepFormat, subLabel, subTitle)}\n${subBody.trimEnd()}`);
                }
            }
        }
        return sections.join("\n\n");
    }
    /**
     * Return a structured preview of all steps and their active state for the
     * given params. Useful in development, tests, and debugging to verify exactly
     * which steps would be emitted and what labels they carry.
     *
     * @example
     *   const pv = _plan.preview({ ...params, unmatchedFiles: [] });
     *   // pv.phases[0].steps[1].subSteps[0] =>
     *   //   { key: "TRACE_CALLERS", label: "2.?", active: false,
     *   //     conditionDesc: "non-frontend unmatched backend files present" }
     */
    preview(params) {
        let mainIdx = this._startFrom - 1;
        let activeCount = 0;
        let totalCount = 0;
        const phases = this._phases.map((phase) => ({
            key: phase.key,
            title: phase.title,
            steps: phase.steps.map((step) => {
                mainIdx++;
                activeCount++;
                totalCount++;
                const label = String(mainIdx);
                let subIdx = 0;
                const subSteps = step.subSteps.map((sub) => {
                    const active = !sub.condition || sub.condition(params);
                    totalCount++;
                    if (active) {
                        subIdx++;
                        activeCount++;
                    }
                    return {
                        key: sub.key,
                        label: active ? `${label}.${subIdx}` : `${label}.?`,
                        title: resolveTitle(sub.title, params),
                        active,
                        ...(sub.conditionDesc !== undefined
                            ? { conditionDesc: sub.conditionDesc }
                            : {}),
                    };
                });
                return {
                    key: step.key,
                    label,
                    title: resolveTitle(step.title, params),
                    subSteps,
                };
            }),
        }));
        return { phases, activeStepCount: activeCount, totalStepCount: totalCount };
    }
}
// ─── Internal helpers ────────────────────────────────────────────────────────
function resolveTitle(title, params) {
    return typeof title === "function" ? title(params) : title;
}
function renderStepHeader(format, label, title) {
    return format === "bold"
        ? `**Step ${label} — ${title}**`
        : `### Step ${label}: ${title}`;
}
