import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { BestAgentError } from "@best-agent/foundation";
const MAX_COORDINATOR_ITERATIONS = 3;
const MAX_COORDINATOR_WORKERS = 5;
const MAX_SCRATCH_FILES = 8;
const MAX_SCRATCH_FILE_BYTES = 2_000;
export class CoordinatorExecutor {
    #deps;
    constructor(deps) {
        this.#deps = deps;
    }
    async execute(input) {
        const planProfile = this.#deps.resolveProfile("plan");
        if (!planProfile) {
            throw new BestAgentError("AGENT_PROFILE_NOT_FOUND", "Agent profile plan was not found.");
        }
        const { scratchDir, workflowRunId } = this.#deps.createScratchRun(input.turn.threadId);
        let totalIterations = 0;
        let totalWorkers = 0;
        let outcome = "success";
        let sawWorkerFailure = false;
        try {
            const initialPlan = await this.#planWorkers({
                activeSkillIds: input.activeSkillIds,
                hostProfile: input.hostProfile,
                planProfile,
                prompt: input.prompt,
                scratchDir,
                turn: input.turn,
            });
            this.#deps.emitCoordinatorPlan({
                plannedIterations: initialPlan.plannedIterations,
                threadId: input.turn.threadId,
                turnId: input.turn.id,
                workerCount: initialPlan.workers.length,
                workflowRunId,
            });
            let currentWorkers = initialPlan.workers;
            const plannedIterations = initialPlan.plannedIterations;
            for (let iteration = 1; iteration <= plannedIterations; iteration += 1) {
                totalIterations = iteration;
                const results = await this.#runWorkerIteration({
                    activeSkillIds: input.activeSkillIds,
                    hostProfile: input.hostProfile,
                    iteration,
                    scratchDir,
                    tasks: currentWorkers,
                    turn: input.turn,
                    workflowRunId,
                });
                totalWorkers += results.length;
                if (results.some((result) => result.status === "failed")) {
                    sawWorkerFailure = true;
                }
                const synthesis = await this.#synthesize({
                    activeSkillIds: input.activeSkillIds,
                    hostProfile: input.hostProfile,
                    iteration,
                    maxIterations: plannedIterations,
                    planProfile,
                    prompt: input.prompt,
                    results,
                    scratchDir,
                    turn: input.turn,
                });
                const forcedCompletion = synthesis.decision === "iterate" && iteration >= plannedIterations;
                const effectiveDecision = forcedCompletion ? "complete" : synthesis.decision;
                this.#deps.emitCoordinatorSynthesize({
                    decision: effectiveDecision,
                    iteration,
                    threadId: input.turn.threadId,
                    totalWorkers,
                    turnId: input.turn.id,
                    workflowRunId,
                });
                if (effectiveDecision === "complete") {
                    if (sawWorkerFailure || forcedCompletion) {
                        outcome = "partial";
                    }
                    const output = forcedCompletion
                        ? [
                            `[Coordinator reached iteration limit (${iteration}/${plannedIterations}); synthesis may be incomplete]`,
                            "",
                            synthesis.finalOutput,
                        ].join("\n")
                        : synthesis.finalOutput;
                    return output;
                }
                currentWorkers = synthesis.workers;
            }
            outcome = sawWorkerFailure ? "partial" : "failed";
            throw new BestAgentError("COORDINATOR_INVALID", "Coordinator exhausted its iteration budget without producing a completion.");
        }
        catch (error) {
            if (outcome !== "partial") {
                outcome = "failed";
            }
            throw error;
        }
        finally {
            this.#deps.emitCoordinatorComplete({
                outcome,
                threadId: input.turn.threadId,
                totalIterations,
                totalWorkers,
                turnId: input.turn.id,
                workflowRunId,
            });
            await this.#deps.cleanupScratchRun(input.turn.threadId, workflowRunId);
        }
    }
    async #planWorkers(input) {
        const parsed = await this.#executeStructuredCoordinatorTurn({
            activeSkillIds: input.activeSkillIds,
            extraMessages: [
                {
                    role: "user",
                    content: buildCoordinatorPlanPrompt(input.prompt, input.scratchDir),
                },
            ],
            hostProfile: input.hostProfile,
            label: "coordinator plan",
            phaseInstruction: "You are generating the coordinator lifecycle plan for workflow:coordinate. Return strict JSON only.",
            profile: input.planProfile,
            turn: input.turn,
        });
        const plannedIterations = clampInteger(parsed.plannedIterations, 1, MAX_COORDINATOR_ITERATIONS, 1);
        const workers = this.#normalizeWorkerPlans(parsed, "coordinator plan");
        return {
            plannedIterations,
            workers,
        };
    }
    async #runWorkerIteration(input) {
        const groups = buildWorkerGroups(input.tasks);
        const results = [];
        for (const group of groups) {
            const settled = await Promise.all(group.map((task) => this.#deps.executeWorker({
                activeSkillIds: input.activeSkillIds,
                hostProfile: input.hostProfile,
                iteration: input.iteration,
                ownedPaths: task.ownedPaths,
                ownershipMode: task.ownershipMode,
                profile: task.profile,
                scratchDir: input.scratchDir,
                turn: input.turn,
                workerId: task.id,
                workflowRunId: input.workflowRunId,
                workerObjective: task.objective,
            })));
            results.push(...settled);
        }
        return results;
    }
    async #synthesize(input) {
        const parsed = await this.#executeStructuredCoordinatorTurn({
            activeSkillIds: input.activeSkillIds,
            extraMessages: [
                {
                    role: "user",
                    content: buildCoordinatorSynthesisPrompt({
                        iteration: input.iteration,
                        maxIterations: input.maxIterations,
                        prompt: input.prompt,
                        results: input.results,
                        scratchDir: input.scratchDir,
                    }),
                },
            ],
            hostProfile: input.hostProfile,
            label: "coordinator synthesis",
            phaseInstruction: "You are synthesizing coordinator worker output for workflow:coordinate. Return strict JSON only.",
            profile: input.planProfile,
            turn: input.turn,
        });
        const finalOutput = typeof parsed.finalOutput === "string" ? parsed.finalOutput.trim() : "";
        if (!finalOutput) {
            throw new BestAgentError("COORDINATOR_INVALID", "Coordinator synthesis must include a non-empty finalOutput.");
        }
        return {
            decision: parsed.decision === "iterate" ? "iterate" : "complete",
            finalOutput,
            workers: this.#normalizeWorkerPlans(parsed, "coordinator synthesis", parsed.decision === "iterate"),
        };
    }
    async #executeStructuredCoordinatorTurn(input) {
        const response = await this.#deps.executeCoordinatorModelTurn({
            activeSkillIds: input.activeSkillIds,
            extraMessages: input.extraMessages,
            hostProfile: input.hostProfile,
            phaseInstruction: input.phaseInstruction,
            profile: input.profile,
            turn: input.turn,
        });
        try {
            return parseJsonRecord(response, input.label);
        }
        catch (error) {
            if (!isCoordinatorJsonParseFailure(error)) {
                throw error;
            }
            const retryResponse = await this.#deps.executeCoordinatorModelTurn({
                activeSkillIds: input.activeSkillIds,
                extraMessages: [
                    ...input.extraMessages,
                    {
                        role: "user",
                        content: buildCoordinatorJsonRepairPrompt(input.label, response),
                    },
                ],
                hostProfile: input.hostProfile,
                phaseInstruction: `${input.phaseInstruction} Previous response was invalid JSON; return one strict JSON object only.`,
                profile: input.profile,
                turn: input.turn,
            });
            return parseJsonRecord(retryResponse, input.label);
        }
    }
    #normalizeWorkerPlans(record, label, required = true) {
        const workerRecords = Array.isArray(record.workers)
            ? record.workers
            : Array.isArray(record.tasks)
                ? record.tasks
                : [];
        if (workerRecords.length === 0) {
            if (required) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} must include at least one worker.`);
            }
            return [];
        }
        return workerRecords.slice(0, MAX_COORDINATOR_WORKERS).map((value, index) => {
            if (!value || typeof value !== "object" || Array.isArray(value)) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} worker entries must be objects.`);
            }
            const worker = value;
            const profileName = typeof worker.profile === "string"
                ? worker.profile.trim()
                : typeof worker.profileName === "string"
                    ? worker.profileName.trim()
                    : typeof worker.profile_name === "string"
                        ? worker.profile_name.trim()
                        : "explore";
            const profile = this.#deps.resolveProfile(profileName);
            if (!profile) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} selected unknown profile ${profileName}.`);
            }
            const objective = typeof worker.objective === "string"
                ? worker.objective.trim()
                : typeof worker.goal === "string"
                    ? worker.goal.trim()
                    : "";
            if (!objective) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} workers must include a non-empty objective.`);
            }
            const ownershipMode = worker.ownershipMode === "disjoint-write" || worker.ownershipMode === "read-only"
                ? worker.ownershipMode
                : worker.ownership_mode === "disjoint-write" || worker.ownership_mode === "read-only"
                    ? worker.ownership_mode
                    : this.#deps.defaultOwnershipModeForProfile(profile);
            const expectedOwnership = this.#deps.defaultOwnershipModeForProfile(profile);
            if (ownershipMode !== expectedOwnership) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} worker ${profile.name} must use ownership mode ${expectedOwnership}.`);
            }
            const ownedPaths = Array.isArray(worker.ownedPaths)
                ? worker.ownedPaths
                : Array.isArray(worker.owned_paths)
                    ? worker.owned_paths
                    : [];
            const normalizedOwnedPaths = ownedPaths
                .filter((path) => typeof path === "string")
                .map((path) => path.trim())
                .filter((path) => path.length > 0);
            if (ownershipMode === "disjoint-write" && normalizedOwnedPaths.length === 0) {
                throw new BestAgentError("COORDINATOR_INVALID", `${label} disjoint-write workers must include ownedPaths.`);
            }
            return {
                id: typeof worker.id === "string" && worker.id.trim().length > 0
                    ? worker.id.trim()
                    : `worker-${index + 1}`,
                objective,
                ...(normalizedOwnedPaths.length > 0 ? { ownedPaths: normalizedOwnedPaths } : {}),
                ownershipMode,
                profile,
            };
        });
    }
}
function parseJsonRecord(text, label) {
    const trimmed = text.trim();
    const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
    const candidate = fencedMatch?.[1]?.trim() ?? trimmed;
    const start = candidate.indexOf("{");
    const end = candidate.lastIndexOf("}");
    if (start === -1 || end === -1 || end < start) {
        throw new BestAgentError("COORDINATOR_INVALID", `Failed to parse ${label}: expected a JSON object.`);
    }
    let parsed;
    try {
        parsed = JSON.parse(stripJsonTrailingCommas(candidate.slice(start, end + 1)));
    }
    catch {
        throw new BestAgentError("COORDINATOR_INVALID", `Failed to parse ${label}: expected valid JSON.`);
    }
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new BestAgentError("COORDINATOR_INVALID", `Failed to parse ${label}: expected an object.`);
    }
    return parsed;
}
function isCoordinatorJsonParseFailure(error) {
    return (error instanceof BestAgentError &&
        error.code === "COORDINATOR_INVALID" &&
        error.message.startsWith("Failed to parse "));
}
function buildCoordinatorJsonRepairPrompt(label, response) {
    const excerpt = response.trim().slice(0, 1_500);
    return [
        `The previous ${label} response was invalid JSON.`,
        "Regenerate the answer as one strict JSON object with no markdown fences or prose.",
        `Invalid response excerpt:\n${excerpt || "<empty response>"}`,
    ].join("\n\n");
}
function stripJsonTrailingCommas(value) {
    return value.replace(/,\s*([}\]])/gu, "$1");
}
function clampInteger(value, min, max, fallback) {
    const numeric = typeof value === "number" ? value : Number(value);
    if (!Number.isFinite(numeric)) {
        return fallback;
    }
    return Math.max(min, Math.min(max, Math.round(numeric)));
}
function buildWorkerGroups(tasks) {
    const groups = [];
    let current = [];
    for (const task of tasks) {
        if (current.length >= MAX_COORDINATOR_WORKERS || hasParallelConflict([...current, task])) {
            groups.push(current);
            current = [];
        }
        current.push(task);
    }
    if (current.length > 0) {
        groups.push(current);
    }
    return groups;
}
function hasParallelConflict(tasks) {
    if (tasks.length <= 1) {
        return false;
    }
    if (tasks.every((task) => task.ownershipMode === "read-only")) {
        return false;
    }
    if (!tasks.every((task) => task.ownershipMode === "disjoint-write")) {
        return true;
    }
    const normalized = tasks.map((task) => (task.ownedPaths ?? []).map((path) => resolve(path)).sort());
    if (normalized.some((paths) => paths.length === 0)) {
        return true;
    }
    for (let index = 0; index < normalized.length; index += 1) {
        for (let candidateIndex = index + 1; candidateIndex < normalized.length; candidateIndex += 1) {
            if (ownedPathsOverlap(normalized[index], normalized[candidateIndex])) {
                return true;
            }
        }
    }
    return false;
}
function ownedPathsOverlap(left, right) {
    return left.some((leftPath) => right.some((rightPath) => leftPath === rightPath ||
        leftPath.startsWith(`${rightPath}/`) ||
        rightPath.startsWith(`${leftPath}/`)));
}
function buildCoordinatorPlanPrompt(prompt, scratchDir) {
    return [
        "Plan the first coordinator fan-out as strict JSON.",
        `Original request:\n${prompt}`,
        `Shared scratch directory: ${scratchDir}`,
        "Return only this object shape:",
        '{"plannedIterations":1,"workers":[{"id":"worker-1","profile":"explore|plan|build|verify","ownershipMode":"read-only|disjoint-write","ownedPaths":["relative/path.ts"],"objective":"specific task"}]}',
        `Rules:
- Use 1-${MAX_COORDINATOR_WORKERS} workers.
- Clamp plannedIterations to 1-${MAX_COORDINATOR_ITERATIONS}.
- Use the smallest sufficient fan-out.
- Worker ownershipMode must be read-only when no workspace writes are required.
- Worker ownershipMode must be disjoint-write when concrete workspace edits are required.
- disjoint-write workers must include minimal ownedPaths.
- Do not include prose outside the JSON object.`,
    ].join("\n\n");
}
function buildCoordinatorSynthesisPrompt(input) {
    return [
        "Synthesize the current coordinator iteration as strict JSON.",
        `Original request:\n${input.prompt}`,
        `Current iteration: ${input.iteration} of ${input.maxIterations}`,
        `Scratch snapshot:\n${renderScratchSnapshot(input.scratchDir)}`,
        `Worker results:\n${renderWorkerResults(input.results)}`,
        "Return only this object shape:",
        '{"decision":"complete|iterate","finalOutput":"best effort user-facing answer","workers":[{"id":"worker-1","profile":"explore|plan|build|verify","ownershipMode":"read-only|disjoint-write","ownedPaths":["relative/path.ts"],"objective":"specific follow-up task"}]}',
        `Rules:
- finalOutput is always required.
- If more work is needed and iteration budget remains, set decision to iterate and include follow-up workers.
- If iteration budget is exhausted, still return decision=complete with the best possible finalOutput.
- Do not include prose outside the JSON object.`,
    ].join("\n\n");
}
function renderWorkerResults(results) {
    return results
        .map((result) => [
        `Worker ${result.workerId} (${result.profileName})`,
        `Status: ${result.status}`,
        `Objective: ${result.objective}`,
        result.handoff
            ? renderWorkerHandoff(result.handoff)
            : result.status === "completed"
                ? "Structured handoff:\n- no durable facts recorded"
                : undefined,
        result.message ? `Failure:\n${result.message}` : undefined,
    ]
        .filter((value) => Boolean(value && value.trim().length > 0))
        .join("\n"))
        .join("\n\n---\n\n");
}
function renderWorkerHandoff(handoff) {
    const lines = [
        "Structured handoff:",
        handoff.lastError ? `- blocker: ${handoff.lastError}` : undefined,
        handoff.activePaths && handoff.activePaths.length > 0
            ? `- active paths: ${handoff.activePaths.join(", ")}`
            : undefined,
        handoff.latestToolOutcomes && handoff.latestToolOutcomes.length > 0
            ? `- tool outcomes: ${handoff.latestToolOutcomes.join(" | ")}`
            : undefined,
        handoff.latestVerification
            ? `- verification: ${handoff.latestVerification.status ?? "unknown"} - ${handoff.latestVerification.summary}`
            : undefined,
    ].filter((value) => Boolean(value));
    if (handoff.evidence && handoff.evidence.length > 0) {
        lines.push("- evidence:");
        for (const record of handoff.evidence) {
            const paths = record.relatedFilePaths && record.relatedFilePaths.length > 0
                ? ` (${record.relatedFilePaths.join(", ")})`
                : "";
            lines.push(`  - [${record.evidenceId}] ${record.kind}: ${record.summary}${paths}`);
        }
    }
    if (lines.length === 1) {
        lines.push("- no durable facts recorded");
    }
    return lines.join("\n");
}
function renderScratchSnapshot(scratchDir) {
    const files = listScratchFiles(scratchDir).slice(0, MAX_SCRATCH_FILES);
    if (files.length === 0) {
        return "(empty)";
    }
    return files
        .map((path) => {
        const relative = path.slice(scratchDir.length + 1);
        const content = readFileSync(path, "utf8").slice(0, MAX_SCRATCH_FILE_BYTES);
        return `# ${relative}\n${content}`;
    })
        .join("\n\n");
}
function listScratchFiles(root) {
    const discovered = [];
    for (const entry of readdirSync(root)) {
        const path = join(root, entry);
        const stats = statSync(path);
        if (stats.isDirectory()) {
            discovered.push(...listScratchFiles(path));
            continue;
        }
        if (stats.isFile()) {
            discovered.push(path);
        }
    }
    return discovered.sort();
}
//# sourceMappingURL=coordinator-executor.js.map