"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectProject = detectProject;
exports.runDoctor = runDoctor;
exports.displayResult = displayResult;
/**
 * DoctorEngine — Main orchestrator for mycontext doctor
 *
 * 1. Detects project type (Next.js / Turborepo / Node.js)
 * 2. Loads applicable rules
 * 3. Runs checks, collects diagnostics
 * 4. Computes score and grade
 * 5. Optionally auto-fixes
 */
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const index_1 = require("./rules/index");
const workspace_1 = require("../utils/workspace");
// ─── Project Detection ────────────────────────────────────────────
async function detectProject(root) {
    const absRoot = path.resolve(root);
    const pkgPath = path.join(absRoot, "package.json");
    let pkg = {};
    if (await fs.pathExists(pkgPath)) {
        try {
            pkg = await fs.readJson(pkgPath);
        }
        catch { /* ignore */ }
    }
    // Detect package manager
    const packageManager = await detectPackageManager(absRoot);
    // Detect monorepo / Turborepo
    const turboJson = path.join(absRoot, "turbo.json");
    const hasTurbo = await fs.pathExists(turboJson);
    const isMonorepo = hasTurbo || !!pkg.workspaces || await fs.pathExists(path.join(absRoot, "pnpm-workspace.yaml"));
    // Detect Next.js
    const deps = { ...pkg.dependencies, ...pkg.devDependencies };
    const hasNext = !!deps?.next;
    // Detect TypeScript
    const hasTs = await fs.pathExists(path.join(absRoot, "tsconfig.json"));
    // Determine project type
    let type = "node";
    if (hasTurbo || (isMonorepo && hasTurbo)) {
        type = "turbo";
    }
    else if (hasNext) {
        type = "nextjs";
    }
    // Detect workspaces
    let workspaces;
    if (isMonorepo) {
        const wm = new workspace_1.WorkspaceManager(absRoot);
        workspaces = await wm.getWorkspaces();
    }
    return {
        type,
        name: pkg.name || path.basename(absRoot),
        root: absRoot,
        version: pkg.version,
        packageManager,
        isMonorepo,
        workspaces,
        typescript: hasTs,
        nextVersion: deps?.next,
        reactVersion: deps?.react,
        turboVersion: deps?.turbo,
    };
}
async function detectPackageManager(root) {
    if (await fs.pathExists(path.join(root, "pnpm-lock.yaml")))
        return "pnpm";
    if (await fs.pathExists(path.join(root, "yarn.lock")))
        return "yarn";
    if (await fs.pathExists(path.join(root, "bun.lockb")))
        return "bun";
    if (await fs.pathExists(path.join(root, "package-lock.json")))
        return "npm";
    return "unknown";
}
// ─── Rule Context Factory ─────────────────────────────────────────
const IGNORE_DIRS = new Set([
    "node_modules", ".next", ".git", "dist", "build", ".turbo",
    ".pnpm-store", "coverage", ".cache", ".vercel", "__pycache__",
    ".mycontext",
]);
function createRuleContext(root, project, isWorkspace = false, redTeam = false) {
    return {
        root,
        project,
        isWorkspace,
        async readFile(relativePath) {
            try {
                const abs = path.join(root, relativePath);
                return await fs.readFile(abs, "utf8");
            }
            catch {
                return null;
            }
        },
        async fileExists(relativePath) {
            return fs.pathExists(path.join(root, relativePath));
        },
        async findFiles(pattern, maxDepth = 6) {
            const results = [];
            async function walk(dir, depth) {
                if (depth > maxDepth)
                    return;
                try {
                    const entries = await fs.readdir(dir, { withFileTypes: true });
                    for (const entry of entries) {
                        if (IGNORE_DIRS.has(entry.name))
                            continue;
                        const rel = path.relative(root, path.join(dir, entry.name));
                        if (entry.isDirectory()) {
                            await walk(path.join(dir, entry.name), depth + 1);
                        }
                        else if (pattern.test(rel)) {
                            results.push(rel);
                        }
                    }
                }
                catch { /* ignore permission errors */ }
            }
            await walk(root, 0);
            return results.sort((a, b) => a.localeCompare(b));
        },
        async readJson(relativePath) {
            try {
                return await fs.readJson(path.join(root, relativePath));
            }
            catch {
                return null;
            }
        },
        async writeFile(relativePath, content) {
            try {
                await fs.outputFile(path.join(root, relativePath), content);
                return true;
            }
            catch {
                return false;
            }
        },
        async deleteFile(relativePath) {
            try {
                await fs.remove(path.join(root, relativePath));
                return true;
            }
            catch {
                return false;
            }
        },
        redTeam,
    };
}
// ─── Score Calculation ─────────────────────────────────────────────
function calculateScore(diagnostics, totalRules, ruleResults) {
    if (totalRules === 0)
        return { score: 100, grade: "A+" };
    const warnings = diagnostics.filter(d => d.severity === "warning").length;
    // Categorize rules into three tiers:
    // 1. Fully passed (0 diagnostics)
    // 2. Warning-only (has warnings but no errors) — partial credit
    // 3. Error rules (has at least 1 error) — failed
    const fullyPassed = ruleResults.filter(r => r.passed).length;
    const warningOnly = ruleResults.filter(r => !r.passed && r.diagnostics.every(d => d.severity === "warning")).length;
    const errorRules = ruleResults.filter(r => !r.passed && r.diagnostics.some(d => d.severity === "error")).length;
    // Base score: fully passed = 100%, warning-only = 50%, error = 0%
    const effectivePass = fullyPassed + warningOnly * 0.5;
    const baseScore = 100 * (effectivePass / totalRules);
    // Warning penalty: diminishing returns, capped at 10pts
    //   5 warnings → ~3pts, 20 warnings → ~6pts, 50 warnings → ~8pts
    const warningPenalty = Math.min(warnings * 0.15, 10);
    // Error penalty: 5pts per rule with errors, capped at 15pts
    const errorPenalty = Math.min(errorRules * 5, 15);
    const score = Math.max(0, Math.min(100, Math.round(baseScore - warningPenalty - errorPenalty)));
    let grade;
    if (score >= 95)
        grade = "A+";
    else if (score >= 90)
        grade = "A";
    else if (score >= 85)
        grade = "A-";
    else if (score >= 80)
        grade = "B+";
    else if (score >= 75)
        grade = "B";
    else if (score >= 70)
        grade = "B-";
    else if (score >= 65)
        grade = "C+";
    else if (score >= 60)
        grade = "C";
    else if (score >= 55)
        grade = "C-";
    else if (score >= 50)
        grade = "D";
    else
        grade = "F";
    return { score, grade };
}
// ─── Main Doctor Engine ───────────────────────────────────────────
async function runDoctor(directory, options = {}) {
    const start = Date.now();
    const project = await detectProject(directory);
    // Determine which roots to scan
    const scanRoots = [];
    if (project.isMonorepo && project.workspaces) {
        // Filter by --project if specified
        const workspaces = options.project
            ? project.workspaces.filter(ws => ws.name === options.project ||
                ws.path.endsWith(options.project) ||
                ws.name.includes(options.project))
            : project.workspaces;
        // Scan root for turbo rules
        scanRoots.push({ root: project.root, project });
        // Scan each workspace
        for (const ws of workspaces) {
            const wsProject = {
                ...project,
                type: ws.type,
                name: ws.name,
                root: ws.absolutePath,
                isMonorepo: false,
                workspaces: undefined,
            };
            // Re-detect deps for this workspace
            const wsPkg = await fs.readJson(path.join(ws.absolutePath, "package.json")).catch(() => ({}));
            const wsDeps = { ...wsPkg.dependencies, ...wsPkg.devDependencies };
            wsProject.nextVersion = wsDeps?.next;
            wsProject.reactVersion = wsDeps?.react;
            scanRoots.push({ root: ws.absolutePath, project: wsProject });
        }
    }
    else {
        scanRoots.push({ root: project.root, project });
    }
    // Filter rules by category and project type
    const applicableRules = index_1.allRules.filter(rule => {
        if (options.category && rule.category !== options.category)
            return false;
        return true;
    });
    // Run rules across all scan roots
    const allDiagnostics = [];
    const ruleResults = [];
    const diagnosticsByRoot = new Map();
    for (const { root, project: scanProject } of scanRoots) {
        const isWorkspace = project.isMonorepo && root !== project.root;
        const ctx = createRuleContext(root, scanProject, isWorkspace, options.redTeam);
        const rootDiagnostics = [];
        // Only run rules that apply to this project type
        const rules = applicableRules.filter((rule) => rule.appliesTo.includes(scanProject.type) ||
            (scanProject.type === "turbo" && rule.appliesTo.includes("turbo")));
        for (const rule of rules) {
            try {
                const diagnostics = await rule.check(ctx);
                // Prefix file paths with workspace path for monorepos
                const prefixedDiags = diagnostics.map((d) => ({
                    ...d,
                    filePath: project.isMonorepo && root !== project.root
                        ? path.relative(project.root, path.join(root, d.filePath))
                        : d.filePath,
                })).sort((a, b) => {
                    const fileCmp = a.filePath.localeCompare(b.filePath);
                    if (fileCmp !== 0)
                        return fileCmp;
                    const lineA = a.line ?? 0;
                    const lineB = b.line ?? 0;
                    if (lineA !== lineB)
                        return lineA - lineB;
                    return a.ruleId.localeCompare(b.ruleId);
                });
                const existing = ruleResults.find((r) => r.ruleId === rule.id);
                if (existing) {
                    existing.diagnostics.push(...prefixedDiags);
                    existing.passed = existing.passed && prefixedDiags.length === 0;
                }
                else {
                    ruleResults.push({
                        ruleId: rule.id,
                        ruleName: rule.name,
                        category: rule.category,
                        passed: prefixedDiags.length === 0,
                        diagnostics: prefixedDiags,
                    });
                }
                allDiagnostics.push(...prefixedDiags);
                rootDiagnostics.push(...diagnostics); // Store original paths for the fix loop
            }
            catch (err) {
                // Rule threw — treat as a pass (we don't want engine bugs to fail the scan)
            }
        }
        diagnosticsByRoot.set(root, rootDiagnostics);
    }
    // Auto-fix if requested
    let fixedCount = 0;
    if ((options.fix || options.prune) && !options.dryRun) {
        for (const { root, project: scanProject } of scanRoots) {
            const isWorkspace = project.isMonorepo && root !== project.root;
            const ctx = createRuleContext(root, scanProject, isWorkspace);
            const rules = applicableRules.filter((r) => r.appliesTo.includes(scanProject.type) ||
                (scanProject.type === "turbo" && r.appliesTo.includes("turbo")));
            const rootDiags = diagnosticsByRoot.get(root) || [];
            for (const rule of rules) {
                if (!rule.fix)
                    continue;
                // Skip dead code rules if prune is not enabled and vice versa
                if (rule.category === "dead" && !options.prune)
                    continue;
                if (rule.category !== "dead" && !options.fix)
                    continue;
                const ruleDiags = rootDiags.filter(d => d.ruleId === rule.id && d.autoFixable);
                for (const diag of ruleDiags) {
                    try {
                        const fixed = await rule.fix(ctx, diag);
                        if (fixed)
                            fixedCount++;
                    }
                    catch { /* ignore fix errors */ }
                }
            }
        }
    }
    const { score, grade } = calculateScore(allDiagnostics, ruleResults.length, ruleResults);
    const sortedDiagnostics = [...allDiagnostics].sort((a, b) => {
        const sevCmp = a.severity.localeCompare(b.severity);
        if (sevCmp !== 0)
            return sevCmp;
        const fileCmp = a.filePath.localeCompare(b.filePath);
        if (fileCmp !== 0)
            return fileCmp;
        const lineA = a.line ?? 0;
        const lineB = b.line ?? 0;
        if (lineA !== lineB)
            return lineA - lineB;
        return a.ruleId.localeCompare(b.ruleId);
    });
    return {
        score,
        grade,
        diagnostics: sortedDiagnostics,
        project,
        ruleResults,
        fixedCount: (options.fix || options.prune) ? fixedCount : undefined,
        duration: Date.now() - start,
    };
}
// ─── Display ──────────────────────────────────────────────────────
function displayResult(result, options = {}) {
    const { score, grade, diagnostics, project, ruleResults } = result;
    // Score-only mode
    if (options.score) {
        if (options.json) {
            console.log(JSON.stringify({ score, grade }));
        }
        else {
            console.log(`${score}`);
        }
        return;
    }
    // JSON mode
    if (options.json) {
        console.log(JSON.stringify(result, null, 2));
        return;
    }
    // Header
    console.log();
    console.log(chalk_1.default.bold("🩺 MyContext Doctor"));
    console.log();
    console.log(`${chalk_1.default.dim("📂 Project:")} ${project.name}`);
    console.log(`${chalk_1.default.dim("🔍 Type:")}    ${formatProjectType(project)}`);
    if (project.isMonorepo && project.workspaces) {
        console.log(`${chalk_1.default.dim("📦 Workspaces:")} ${project.workspaces.length} detected`);
    }
    console.log();
    // Group diagnostics by category
    const categories = ["security", "nextjs", "turbo", "node", "dead", "i18n"];
    for (const cat of categories) {
        const catResults = ruleResults.filter(r => r.category === cat);
        if (catResults.length === 0)
            continue;
        console.log(chalk_1.default.bold(formatCategory(cat)));
        console.log(chalk_1.default.dim("─".repeat(50)));
        for (const rr of catResults) {
            if (rr.passed) {
                console.log(`  ${chalk_1.default.green("✓")} ${chalk_1.default.dim(rr.ruleId.padEnd(35))} ${chalk_1.default.green("pass")}`);
            }
            else {
                for (const diag of rr.diagnostics) {
                    const icon = diag.severity === "error" ? chalk_1.default.red("✗") : chalk_1.default.yellow("⚠");
                    const loc = diag.line ? chalk_1.default.dim(`:${diag.line}`) : "";
                    // Always show filePath for actionable feedback
                    const fileHint = diag.filePath ? ` ${chalk_1.default.dim(`(${diag.filePath}${loc})`)}` : "";
                    console.log(`  ${icon} ${chalk_1.default.dim(diag.ruleId.padEnd(35))} ${diag.message}${fileHint}`);
                    if (options.verbose && diag.help) {
                        console.log(`    ${chalk_1.default.dim("💡")} ${chalk_1.default.dim(diag.help)}`);
                    }
                }
            }
        }
        console.log();
    }
    // Score
    const errors = diagnostics.filter(d => d.severity === "error").length;
    const warnings = diagnostics.filter(d => d.severity === "warning").length;
    const passed = ruleResults.filter(r => r.passed).length;
    console.log(chalk_1.default.bold("Score: ") + gradeColor(grade, `${score}/100 (${grade})`));
    console.log(`${chalk_1.default.red(`Errors: ${errors}`)} | ` +
        `${chalk_1.default.yellow(`Warnings: ${warnings}`)} | ` +
        `${chalk_1.default.green(`Passed: ${passed}/${ruleResults.length}`)}`);
    console.log(chalk_1.default.dim(`Completed in ${result.duration}ms`));
    // Fix summary
    if (result.fixedCount !== undefined) {
        console.log();
        console.log(chalk_1.default.green(`🔧 Fixed ${result.fixedCount} issues`));
    }
    else {
        const fixable = diagnostics.filter(d => d.autoFixable).length;
        if (fixable > 0) {
            console.log();
            console.log(chalk_1.default.dim(`Run ${chalk_1.default.cyan("mycontext doctor --fix")} to auto-fix ${fixable} issues`));
        }
    }
    console.log();
}
function formatProjectType(project) {
    if (project.type === "turbo")
        return `Turborepo monorepo`;
    if (project.type === "nextjs")
        return `Next.js ${project.nextVersion || ""}`.trim();
    return "Node.js";
}
function formatCategory(cat) {
    switch (cat) {
        case "nextjs": return "⚡ Next.js";
        case "turbo": return "🌀 Turborepo";
        case "node": return "📦 Node.js / Structure";
        case "dead": return "💀 Dead Code";
        case "security": return "🛡️ Security";
        case "i18n": return "🌍 Internationalization";
        default: return cat;
    }
}
function gradeColor(grade, text) {
    if (grade.startsWith("A"))
        return chalk_1.default.green(text);
    if (grade.startsWith("B"))
        return chalk_1.default.cyan(text);
    if (grade.startsWith("C"))
        return chalk_1.default.yellow(text);
    return chalk_1.default.red(text);
}
//# sourceMappingURL=DoctorEngine.js.map