import path from "node:path";
import { promises as fs } from "node:fs";
import { buildChecksToRun, buildFirstReadContract, buildSearchGuidance } from "./guidance.js";
import { buildPhaseId, getStatePaths, loadActiveRoleHints, updateActiveRoleHints } from "./state.js";
import { ensureDir, isIgnoredArtifactName, pathExists, readJson, readText, renderDisplayPath, writeText } from "../utils/fs.js";
import { parseYaml } from "../utils/yaml.js";
export const DISPATCH_ROLES = ["planner", "implementer", "reviewer", "tester"];
const STRUCTURED_ROLE_FIELDS = ["role", "status", "summary", "agreements", "conflicts", "validations", "risks", "touched_files", "next_steps"];
export function buildDispatchId(goal, timestamp) {
    return `dispatch-${buildPhaseId(goal, timestamp)}`;
}
export function getDispatchPaths(targetRoot, dispatchId) {
    const root = path.join(getStatePaths(targetRoot).root, "dispatch");
    if (!dispatchId) {
        return {
            root,
            latestManifestPath: path.join(root, "latest-manifest.json"),
            latestMarkdownPath: path.join(root, "latest.md"),
            latestCollectionPath: path.join(root, "latest-collect.json")
        };
    }
    const dispatchDir = path.join(root, dispatchId);
    return {
        root,
        latestManifestPath: path.join(root, "latest-manifest.json"),
        latestMarkdownPath: path.join(root, "latest.md"),
        latestCollectionPath: path.join(root, "latest-collect.json"),
        dispatchDir,
        manifestPath: path.join(dispatchDir, "manifest.json"),
        markdownPath: path.join(dispatchDir, "dispatch.md"),
        rolesDir: path.join(dispatchDir, "roles"),
        resultsDir: path.join(dispatchDir, "results"),
        collectLatestPath: path.join(dispatchDir, "collect-latest.json"),
        collectHistoryDir: path.join(dispatchDir, "collect-history"),
        reconcileLatestJsonPath: path.join(dispatchDir, "reconcile-latest.json"),
        reconcileLatestMarkdownPath: path.join(dispatchDir, "reconcile-latest.md")
    };
}
export async function ensureDispatchLayout(targetRoot, dispatchId) {
    const paths = getDispatchPaths(targetRoot, dispatchId);
    await ensureDir(paths.rolesDir);
    await ensureDir(paths.resultsDir);
    await ensureDir(paths.collectHistoryDir);
    return paths;
}
export function buildDispatchManifest(options) {
    const dispatchId = buildDispatchId(options.goal, options.createdAt);
    const packetSetDirectory = path.dirname(options.packetRelativePaths.planner);
    const roleAssignments = DISPATCH_ROLES.map((role) => ({
        role,
        intendedTool: options.roleTools[role],
        ...(options.roleSpecialists?.[role]
            ? {
                specialistId: options.roleSpecialists[role]?.specialistId,
                specialistReasons: options.roleSpecialists[role]?.reasons
            }
            : {}),
        required: role === "implementer" || role === "reviewer" || options.decision.requiredRoles.includes(role),
        packetPath: options.packetRelativePaths[role],
        expectedMarkdownPath: path.join(".agent", "state", "dispatch", dispatchId, "results", `${role}.md`),
        expectedJsonPath: path.join(".agent", "state", "dispatch", dispatchId, "results", `${role}.json`),
        status: "pending"
    }));
    return {
        artifactType: "shrey-junior/dispatch-manifest",
        version: 1,
        createdAt: options.createdAt,
        dispatchId,
        goal: options.goal,
        profile: options.profileName,
        mode: options.executionMode,
        status: "pending",
        routingSummary: {
            taskType: options.decision.taskType,
            primaryTool: options.decision.primaryTool,
            reviewTool: options.decision.reviewTool,
            riskLevel: options.decision.riskLevel,
            fileArea: options.decision.fileArea,
            changeSize: options.decision.changeSize,
            requiredRoles: options.decision.requiredRoles
        },
        authorityFiles: options.context.authorityOrder,
        promotedDocs: options.context.promotedAuthorityDocs,
        readFirst: buildFirstReadContract({
            targetRoot: options.targetRoot,
            authorityOrder: options.context.authorityOrder,
            promotedAuthorityDocs: options.context.promotedAuthorityDocs,
            contract: {
                instructionSurfaces: options.context.stableContracts.filter((item) => item.includes(".github/instructions/")),
                agentSurfaces: [".github/agents/shrey-junior.md"],
                roleSurfaces: [],
                activeRole: "planner",
                supportingRoles: []
            }
        }),
        checksToRun: buildChecksToRun(options.context.validationSteps),
        searchGuidance: buildSearchGuidance({
            taskType: options.decision.taskType,
            fileArea: options.decision.fileArea
        }),
        packetSetDirectory,
        roleAssignments,
        ...(options.continuity.latestPhase?.phaseId ? { sourcePhaseId: options.continuity.latestPhase.phaseId } : {}),
        ...(options.continuity.latestPhase?.tool ? { previousTool: options.continuity.latestPhase.tool } : {}),
        ...(options.continuity.latestHandoff?.toTool ? { latestHandoffTo: options.continuity.latestHandoff.toTool } : {})
    };
}
export async function writeDispatchManifest(targetRoot, manifest) {
    const paths = await ensureDispatchLayout(targetRoot, manifest.dispatchId);
    const manifestPayload = `${JSON.stringify(manifest, null, 2)}\n`;
    const markdown = renderDispatchMarkdown(targetRoot, manifest);
    await writeText(paths.manifestPath, manifestPayload);
    await writeText(paths.markdownPath, markdown);
    await writeText(paths.latestManifestPath, manifestPayload);
    await writeText(paths.latestMarkdownPath, markdown);
    for (const assignment of manifest.roleAssignments) {
        await writeText(path.join(paths.rolesDir, `${assignment.role}.json`), `${JSON.stringify(assignment, null, 2)}\n`);
    }
    const activeRoleHints = await loadActiveRoleHints(targetRoot);
    if (activeRoleHints) {
        await updateActiveRoleHints(targetRoot, {
            activeRole: activeRoleHints.activeRole,
            authoritySource: activeRoleHints.authoritySource,
            projectType: activeRoleHints.projectType,
            supportingRoles: activeRoleHints.supportingRoles,
            latestDispatchManifest: renderDisplayPath(targetRoot, paths.latestManifestPath)
        });
    }
    return {
        manifestPath: paths.manifestPath,
        markdownPath: paths.markdownPath
    };
}
export function renderDispatchMarkdown(targetRoot, manifest) {
    const lines = [
        `# Dispatch ${manifest.dispatchId}`,
        "",
        `Goal: ${manifest.goal}`,
        `Profile: \`${manifest.profile}\``,
        `Mode: \`${manifest.mode}\``,
        `Status: \`${manifest.status}\``,
        ...(manifest.sourcePhaseId ? [`Source phase: \`${manifest.sourcePhaseId}\``] : []),
        ...(manifest.previousTool ? [`Previous tool: \`${manifest.previousTool}\``] : []),
        ...(manifest.latestHandoffTo ? [`Latest handoff target: \`${manifest.latestHandoffTo}\``] : []),
        "",
        "## Promoted Canonical Docs",
        "",
        ...(manifest.promotedDocs.length > 0
            ? manifest.promotedDocs.map((docPath) => `- \`${renderDisplayPath(targetRoot, docPath)}\``)
            : ["- none promoted"]),
        "",
        "## Authority Files",
        "",
        ...manifest.authorityFiles.slice(0, 10).map((filePath) => `- \`${renderDisplayPath(targetRoot, filePath)}\``),
        "",
        "## Read First",
        "",
        ...manifest.readFirst.map((filePath) => `- \`${filePath}\``),
        "",
        "## Checks To Run",
        "",
        ...manifest.checksToRun.map((item) => `- ${item}`),
        "",
        "## External Lookup Rules",
        "",
        "- inspect the repo codebase first before external search",
        "- prefer promoted repo docs or canonical linked docs before internet search",
        ...manifest.searchGuidance.useExternalLookupWhen.map((item) => `- use external lookup when: ${item}`),
        ...manifest.searchGuidance.avoidExternalLookupWhen.map((item) => `- avoid external lookup when: ${item}`),
        "",
        "## Role Assignments",
        "",
        ...manifest.roleAssignments.map((assignment) => `- ${assignment.role}: tool=\`${assignment.intendedTool}\`${assignment.specialistId ? ` specialist=\`${assignment.specialistId}\`` : ""} required=\`${assignment.required}\` packet=\`${assignment.packetPath}\` result=\`${assignment.expectedMarkdownPath}\``)
    ];
    return `${lines.join("\n")}\n`;
}
export async function loadLatestDispatchManifest(targetRoot) {
    const latestManifestPath = getDispatchPaths(targetRoot).latestManifestPath;
    if (latestManifestPath && (await pathExists(latestManifestPath))) {
        return readJson(latestManifestPath);
    }
    const manifests = await listDispatchManifests(targetRoot);
    return manifests[0] ?? null;
}
export async function listDispatchManifests(targetRoot) {
    const root = getDispatchPaths(targetRoot).root;
    if (!(await pathExists(root))) {
        return [];
    }
    const entries = await fs.readdir(root, { withFileTypes: true });
    const candidates = entries
        .filter((entry) => entry.isDirectory() && !isIgnoredArtifactName(entry.name))
        .map((entry) => entry.name)
        .sort()
        .reverse();
    const manifests = [];
    for (const candidate of candidates) {
        const manifestPath = getDispatchPaths(targetRoot, candidate).manifestPath;
        if (manifestPath && (await pathExists(manifestPath))) {
            manifests.push(await readJson(manifestPath));
        }
    }
    return manifests;
}
export async function loadLatestDispatchCollection(targetRoot, dispatchId) {
    if (!dispatchId) {
        const latestCollectionPath = getDispatchPaths(targetRoot).latestCollectionPath;
        if (latestCollectionPath && (await pathExists(latestCollectionPath))) {
            return readJson(latestCollectionPath);
        }
    }
    const manifest = dispatchId ? await loadDispatchManifest(targetRoot, dispatchId) : await loadLatestDispatchManifest(targetRoot);
    if (!manifest) {
        return null;
    }
    const collectLatestPath = getDispatchPaths(targetRoot, manifest.dispatchId).collectLatestPath;
    if (!collectLatestPath || !(await pathExists(collectLatestPath))) {
        return null;
    }
    return readJson(collectLatestPath);
}
export async function loadDispatchManifest(targetRoot, dispatchId) {
    const manifestPath = getDispatchPaths(targetRoot, dispatchId).manifestPath;
    if (!manifestPath || !(await pathExists(manifestPath))) {
        return null;
    }
    return readJson(manifestPath);
}
export async function collectDispatchOutputs(targetRoot, manifest) {
    const roleResults = await Promise.all(manifest.roleAssignments.map((assignment) => loadRoleResult(targetRoot, assignment)));
    const completedRoles = roleResults.filter((result) => result.status === "complete").map((result) => result.role);
    const missingRoles = roleResults.filter((result) => result.status === "pending").map((result) => result.role);
    const fallbackRoles = roleResults.filter((result) => result.sourceKind === "heuristic-markdown").map((result) => result.role);
    const malformedRoles = roleResults.filter((result) => result.sourceKind === "invalid").map((result) => result.role);
    const partialRoles = roleResults
        .filter((result) => result.missingFields.length > 0)
        .map((result) => ({ role: result.role, missingFields: result.missingFields }));
    const hasBlocked = roleResults.some((result) => result.status === "blocked");
    const hasActive = roleResults.some((result) => result.status === "active");
    const structuredCount = roleResults.filter((result) => result.sourceKind === "structured-json" || result.sourceKind === "structured-frontmatter").length;
    const heuristicCount = fallbackRoles.length;
    const parsingBasis = structuredCount === 0 && heuristicCount === 0
        ? "empty"
        : structuredCount > 0 && heuristicCount === 0
            ? "structured"
            : structuredCount === 0
                ? "heuristic"
                : "mixed";
    return {
        artifactType: "shrey-junior/dispatch-collect",
        version: 1,
        createdAt: new Date().toISOString(),
        dispatchId: manifest.dispatchId,
        overallStatus: hasBlocked
            ? "blocked"
            : missingRoles.length === 0
                ? "complete"
                : hasActive || completedRoles.length > 0
                    ? "active"
                    : "pending",
        parsingBasis,
        completedRoles,
        missingRoles,
        fallbackRoles,
        malformedRoles,
        partialRoles,
        roleResults
    };
}
export async function writeDispatchCollection(targetRoot, manifest, collection) {
    const paths = await ensureDispatchLayout(targetRoot, manifest.dispatchId);
    const latestPath = paths.collectLatestPath;
    const historyPath = path.join(paths.collectHistoryDir, `${collection.createdAt.replace(/[:.]/g, "-")}.json`);
    await writeText(latestPath, `${JSON.stringify(collection, null, 2)}\n`);
    await writeText(historyPath, `${JSON.stringify(collection, null, 2)}\n`);
    await writeText(paths.latestCollectionPath, `${JSON.stringify(collection, null, 2)}\n`);
    const updatedManifest = applyCollectionToManifest(manifest, collection);
    await writeDispatchManifest(targetRoot, updatedManifest);
    return { latestPath, historyPath };
}
export function applyCollectionToManifest(manifest, collection) {
    const statusByRole = new Map(collection.roleResults.map((result) => [result.role, result.status]));
    return {
        ...manifest,
        status: collection.overallStatus,
        roleAssignments: manifest.roleAssignments.map((assignment) => ({
            ...assignment,
            status: statusByRole.get(assignment.role) ?? assignment.status
        }))
    };
}
async function loadRoleResult(targetRoot, assignment) {
    const jsonPath = path.join(targetRoot, assignment.expectedJsonPath);
    const markdownPath = path.join(targetRoot, assignment.expectedMarkdownPath);
    const parsingWarnings = [];
    if (await pathExists(jsonPath)) {
        try {
            const payload = JSON.parse(await readText(jsonPath));
            return buildStructuredRoleResult(assignment, payload, assignment.expectedJsonPath, "structured-json");
        }
        catch (error) {
            parsingWarnings.push(`invalid structured json at ${assignment.expectedJsonPath}: ${error.message}`);
            if (!(await pathExists(markdownPath))) {
                return invalidRoleResult(assignment, assignment.expectedJsonPath, parsingWarnings);
            }
        }
    }
    if (await pathExists(markdownPath)) {
        const content = (await readText(markdownPath)).trim();
        if (!content) {
            return emptyRoleResult(assignment.role, assignment.status);
        }
        const frontmatter = extractFrontmatter(content);
        if (frontmatter) {
            try {
                const payload = parseYaml(frontmatter.body);
                const structured = buildStructuredRoleResult(assignment, payload, assignment.expectedMarkdownPath, "structured-frontmatter");
                structured.parsingWarnings.unshift(...parsingWarnings);
                return structured;
            }
            catch (error) {
                parsingWarnings.push(`invalid structured frontmatter at ${assignment.expectedMarkdownPath}: ${error.message}`);
            }
        }
        return {
            role: assignment.role,
            status: /\bblocked\b/i.test(content) ? "blocked" : /\bactive\b/i.test(content) ? "active" : "complete",
            summary: extractMarkdownSummary(content) ?? `${assignment.role} markdown output captured.`,
            validations: extractTaggedLines(content, "validation"),
            risks: extractTaggedLines(content, "risk"),
            agreements: extractTaggedLines(content, "agreement"),
            conflicts: extractTaggedLines(content, "conflict"),
            touchedFiles: extractTaggedLines(content, "touched_file"),
            nextSteps: extractTaggedLines(content, "next_step"),
            sourcePaths: [assignment.expectedMarkdownPath],
            sourceKind: "heuristic-markdown",
            missingFields: [],
            parsingWarnings
        };
    }
    return emptyRoleResult(assignment.role, assignment.status);
}
function emptyRoleResult(role, fallbackStatus) {
    return {
        role,
        status: fallbackStatus === "active" ? "active" : "pending",
        summary: "No role output captured yet.",
        validations: [],
        risks: [],
        agreements: [],
        conflicts: [],
        touchedFiles: [],
        nextSteps: [],
        sourcePaths: [],
        sourceKind: "missing",
        missingFields: [],
        parsingWarnings: []
    };
}
function invalidRoleResult(assignment, sourcePath, parsingWarnings) {
    return {
        role: assignment.role,
        status: "blocked",
        summary: `Structured role output for ${assignment.role} is invalid and needs manual cleanup.`,
        validations: [],
        risks: ["invalid structured role output"],
        agreements: [],
        conflicts: [],
        touchedFiles: [],
        nextSteps: ["Repair the structured role result file and rerun collect."],
        sourcePaths: [sourcePath],
        sourceKind: "invalid",
        missingFields: [],
        parsingWarnings
    };
}
function buildStructuredRoleResult(assignment, payload, sourcePath, sourceKind) {
    const missingFields = STRUCTURED_ROLE_FIELDS.filter((field) => {
        if (field === "validations") {
            return stringArrayField(payload.validations) === undefined && stringArrayField(payload.validationsRun) === undefined;
        }
        if (field === "risks") {
            return (stringArrayField(payload.risks) === undefined &&
                stringArrayField(payload.warnings) === undefined &&
                stringArrayField(payload.openIssues) === undefined);
        }
        if (field === "touched_files") {
            return stringArrayField(payload.touched_files) === undefined && stringArrayField(payload.touchedFiles) === undefined;
        }
        if (field === "next_steps") {
            return stringArrayField(payload.next_steps) === undefined && stringArrayField(payload.nextSteps) === undefined;
        }
        return payload[field] === undefined;
    });
    const parsingWarnings = missingFields.length > 0 ? [`missing structured fields: ${missingFields.join(", ")}`] : [];
    return {
        role: parseRole(payload.role, assignment.role),
        status: parseRoleStatus(payload.status, "complete"),
        summary: stringField(payload.summary) ?? `Structured result captured for ${assignment.role}.`,
        validations: stringArrayField(payload.validations) ?? stringArrayField(payload.validationsRun) ?? [],
        risks: stringArrayField(payload.risks) ?? stringArrayField(payload.warnings) ?? stringArrayField(payload.openIssues) ?? [],
        agreements: stringArrayField(payload.agreements) ?? [],
        conflicts: stringArrayField(payload.conflicts) ?? [],
        touchedFiles: stringArrayField(payload.touched_files) ?? stringArrayField(payload.touchedFiles) ?? [],
        nextSteps: stringArrayField(payload.next_steps) ?? stringArrayField(payload.nextSteps) ?? [],
        sourcePaths: [sourcePath],
        sourceKind,
        missingFields,
        parsingWarnings
    };
}
function parseRole(value, fallback) {
    if (value === "planner" || value === "implementer" || value === "reviewer" || value === "tester") {
        return value;
    }
    return fallback;
}
function parseRoleStatus(value, fallback) {
    if (value === "pending" || value === "active" || value === "complete" || value === "blocked") {
        return value;
    }
    return fallback;
}
function stringField(value) {
    return typeof value === "string" && value.trim() ? value : undefined;
}
function stringArrayField(value) {
    if (!Array.isArray(value)) {
        return undefined;
    }
    return value.filter((item) => typeof item === "string" && Boolean(item.trim()));
}
function extractFrontmatter(content) {
    if (!content.startsWith("---\n")) {
        return null;
    }
    const closingIndex = content.indexOf("\n---\n", 4);
    if (closingIndex === -1) {
        return null;
    }
    return {
        body: content.slice(4, closingIndex),
        remainder: content.slice(closingIndex + 5).trim()
    };
}
function extractMarkdownSummary(content) {
    const line = content
        .split("\n")
        .map((item) => item.trim())
        .find((item) => Boolean(item) && !item.startsWith("#") && !item.startsWith("<!--") && !item.startsWith("- validation:") && !item.startsWith("- risk:"));
    return line;
}
function extractTaggedLines(content, tag) {
    const prefix = `${tag}:`;
    return content
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => {
        const normalized = line.replace(/^-+\s*/, "").toLowerCase();
        return normalized.startsWith(prefix);
    })
        .map((line) => line.replace(/^-+\s*/, "").slice(prefix.length).trim());
}
