import { buildRouteIndex } from "../project-index/scan-routes.js";
import { inferOperationId } from "../../lib/openapi/operation-id.js";
import { writeRouteInspectFile } from "./write-route-inspect.js";
import { writeRouteManifestFile } from "./write-route-manifest.js";
export async function runDoctor(options) {
    const target = options.target ?? "routes";
    const writeInspect = options.writeInspect ?? false;
    const writeManifest = options.writeManifest ?? false;
    const routeEntries = await buildRouteIndex(options.rootDir);
    const diagnostics = analyzeRoutes(routeEntries);
    const routes = routeEntries.map((entry) => toDoctorRouteRecord(entry));
    const summary = summarizeDiagnostics(diagnostics);
    const inspect = writeInspect
        ? await writeRouteInspectFile(options.rootDir, {
            schemaVersion: 1,
            target: "routes",
            routeFileCount: new Set(routeEntries.map((item) => item.filePath)).size,
            routeCount: routeEntries.length,
            summary,
            diagnostics,
            routes,
        })
        : undefined;
    const manifest = writeManifest
        ? await writeRouteManifestFile(options.rootDir, buildRouteManifestPayload(routes, diagnostics))
        : undefined;
    return {
        ok: !diagnostics.some((item) => item.level === "error"),
        target,
        routeFileCount: new Set(routeEntries.map((item) => item.filePath)).size,
        routeCount: routeEntries.length,
        summary,
        diagnostics,
        routes,
        inspect,
        manifest,
    };
}
function analyzeRoutes(routeEntries) {
    if (routeEntries.length === 0) {
        return [
            {
                level: "info",
                group: "tooling",
                blocking: false,
                code: "doctor-no-routes",
                message: "No route files were found under src/routes.",
            },
        ];
    }
    const diagnostics = [];
    const duplicateMap = new Map();
    for (const entry of routeEntries) {
        const routeKey = `${entry.method} ${entry.path}`;
        const bucket = duplicateMap.get(routeKey) ?? [];
        bucket.push(entry);
        duplicateMap.set(routeKey, bucket);
    }
    for (const [routeKey, entries] of duplicateMap) {
        if (entries.length < 2)
            continue;
        const locations = entries.map((entry) => entry.filePath).join("; ");
        diagnostics.push({
            level: "error",
            group: "routing",
            blocking: true,
            code: "duplicate-route",
            message: `Duplicate static route definition detected for ${routeKey}. Sources: ${locations}`,
            filePath: entries[0]?.filePath,
            fileRelativePath: entries[0]?.fileRelativePath,
            method: entries[0]?.method,
            path: entries[0]?.path,
        });
    }
    for (const entry of routeEntries) {
        if (entry.hidden) {
            continue;
        }
        if (!entry.hasDocsSummary) {
            diagnostics.push({
                level: "warn",
                group: "docs",
                blocking: false,
                code: "missing-docs-summary",
                message: `${entry.method} ${entry.path} is missing docs.summary.`,
                filePath: entry.filePath,
                fileRelativePath: entry.fileRelativePath,
                method: entry.method,
                path: entry.path,
            });
        }
        if (!entry.operationId) {
            diagnostics.push({
                level: "info",
                group: "tooling",
                blocking: false,
                code: "auto-operation-id",
                message: `${entry.method} ${entry.path} will use inferred operationId at runtime.`,
                filePath: entry.filePath,
                fileRelativePath: entry.fileRelativePath,
                method: entry.method,
                path: entry.path,
                suggestedValue: inferOperationId(entry.method, entry.path),
            });
        }
        if (entry.tags.length === 0) {
            diagnostics.push({
                level: "warn",
                group: "docs",
                blocking: false,
                code: "missing-tags",
                message: `${entry.method} ${entry.path} is missing docs.tags.`,
                filePath: entry.filePath,
                fileRelativePath: entry.fileRelativePath,
                method: entry.method,
                path: entry.path,
            });
        }
    }
    if (diagnostics.length === 0) {
        diagnostics.push({
            level: "info",
            group: "tooling",
            blocking: false,
            code: "doctor-routes-ok",
            message: `Route doctor passed for ${routeEntries.length} route(s).`,
        });
    }
    return diagnostics;
}
function toDoctorRouteRecord(entry) {
    return {
        filePath: entry.filePath,
        fileRelativePath: entry.fileRelativePath,
        method: entry.method,
        path: entry.path,
        prefix: entry.prefix,
        docsSummary: entry.docsSummary,
        operationId: entry.operationId,
        effectiveOperationId: entry.operationId ?? inferOperationId(entry.method, entry.path),
        operationIdSource: entry.operationId ? "explicit" : "inferred",
        tags: entry.tags,
        hidden: entry.hidden,
    };
}
function summarizeDiagnostics(diagnostics) {
    const summary = {
        errors: 0,
        warnings: 0,
        infos: 0,
        blocking: 0,
        byCode: {},
        byGroup: {},
    };
    for (const diagnostic of diagnostics) {
        if (diagnostic.level === "error")
            summary.errors += 1;
        if (diagnostic.level === "warn")
            summary.warnings += 1;
        if (diagnostic.level === "info")
            summary.infos += 1;
        if (diagnostic.blocking)
            summary.blocking += 1;
        summary.byCode[diagnostic.code] = (summary.byCode[diagnostic.code] ?? 0) + 1;
        summary.byGroup[diagnostic.group] = (summary.byGroup[diagnostic.group] ?? 0) + 1;
    }
    return summary;
}
function buildRouteManifestPayload(routes, diagnostics) {
    const missingDocsSummary = diagnostics.filter((item) => item.code === "missing-docs-summary").length;
    const missingTags = diagnostics.filter((item) => item.code === "missing-tags").length;
    const duplicateRoutes = diagnostics.filter((item) => item.code === "duplicate-route").length;
    const explicitOperationIds = routes.filter((item) => item.operationIdSource === "explicit").length;
    const inferredOperationIds = routes.filter((item) => item.operationIdSource === "inferred").length;
    const hiddenRoutes = routes.filter((item) => item.hidden).length;
    const publicRoutes = routes.length - hiddenRoutes;
    return {
        schemaVersion: 1,
        kind: "routes-manifest",
        target: "routes",
        routeFileCount: new Set(routes.map((item) => item.fileRelativePath)).size,
        routeCount: routes.length,
        summary: {
            publicRoutes,
            hiddenRoutes,
            explicitOperationIds,
            inferredOperationIds,
            missingDocsSummary,
            missingTags,
            duplicateRoutes,
        },
        routes: routes.map((item) => ({
            fileRelativePath: item.fileRelativePath,
            prefix: item.prefix,
            method: item.method,
            path: item.path,
            docsSummary: item.docsSummary,
            operationId: item.effectiveOperationId,
            operationIdSource: item.operationIdSource,
            tags: item.tags,
            hidden: item.hidden,
        })),
    };
}
