import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import * as path from "node:path";
import { AGENTS_MD_FILENAME } from "../codex/agents-md-installer.js";
import { CLAUDE_MD_FILENAME } from "../claude/claude-md-installer.js";
import { migrateInstructionFiles } from "../core/instruction-files-migration.js";
import { probeKaneReadiness } from "../core/kane-cli.js";
import { probeSonarReadiness } from "../core/sonar-integration.js";
import { createDetectorRegistry } from "../detection/index.js";
import { checkApplyFreshness, checkYamlRuntime, } from "./doctor-apply-freshness.js";
import { checkKaneProvider } from "./doctor-kane.js";
import { checkLearningsLedger } from "./doctor-learnings-ledger.js";
import { checkLearningsMergeDriver } from "./doctor-learnings-merge-driver.js";
import { checkSonarProvider } from "./doctor-sonar.js";
import { checkLegacyCodexOverlay } from "./doctor-legacy-overlay.js";
import { checkLisaOwnedArtifacts } from "./doctor-lisa-owned-artifacts.js";
import { checkLegacyMonitorThresholds } from "./doctor-monitor-thresholds.js";
import { checkRepositoryReadiness } from "./doctor-readiness.js";
import { checkReusableWorkflowRefs } from "./doctor-reusable-workflow-refs.js";
import { checkWorkerEpoch } from "./doctor-worker-epoch.js";
import { checkSerializeLegsContract } from "./doctor-serialize-legs-contract.js";
import { checkApplyFailure } from "./doctor-apply-failure.js";
import { renderDoctorResult } from "./doctor-render.js";
import { checkSkipJobsMigration } from "./doctor-skip-jobs-migration.js";
import { checkDeclaredContexts } from "./doctor-declared-contexts.js";
import { checkTraceabilityGate } from "./doctor-traceability-gate.js";
import { checkHookCopyParity } from "./doctor-hook-copy-parity.js";
import { checkWorktreeHygiene } from "./doctor-worktree-hygiene.js";
import { checkWorktreeWorkAtRisk } from "./doctor-worktree-work-at-risk.js";
import { STARTERS } from "./starters.js";
import { runUpdateCheck } from "./update-check.js";
const VERSION_CHECK_NAME = "Lisa version current?";
const STARTER_HEALTH_NAME = "Starter health";
const PROJECT_CONFIG_CHECK_NAME = "Project Lisa config present?";
const INSTRUCTION_FILES_CHECK_NAME = "Instruction files canonical?";
const DEFAULT_DEPENDENCIES = {
    fetchImpl: fetch,
    runUpdateCheck,
    setExitCode: code => {
        process.exitCode = code;
    },
    write: message => console.log(message),
    probeKaneReadiness,
    probeSonarReadiness,
};
/**
 * Check Lisa's installed version against npm.
 * Exported so the console live-status probe can reuse the same check as
 * `lisa doctor` — never invent a second npm update-check path.
 * @param deps - Runtime dependencies
 * @param offline - Skip network check
 * @returns Doctor check result
 */
export async function checkVersion(deps, offline) {
    if (offline) {
        return {
            name: VERSION_CHECK_NAME,
            status: "ok",
            detail: "Skipped network check in offline mode",
        };
    }
    const result = await deps.runUpdateCheck();
    if (result.isOutdated && result.latest) {
        return {
            name: VERSION_CHECK_NAME,
            status: "warn",
            detail: `Installed ${result.current}; latest is ${result.latest}`,
        };
    }
    return {
        name: VERSION_CHECK_NAME,
        status: result.latest ? "ok" : "warn",
        detail: result.latest
            ? `Installed ${result.current}; latest is ${result.latest}`
            : `Latest version unavailable${result.reason ? ` (${result.reason})` : ""}`,
    };
}
/**
 * Validate Lisa project configuration files.
 * @param targetPath - Project path to inspect
 * @returns Doctor check result
 */
async function checkProjectConfig(targetPath) {
    const configPaths = [".lisa.config.json", ".lisa.config.local.json"]
        .map(fileName => path.join(targetPath, fileName))
        .filter(configPath => existsSync(configPath));
    if (configPaths.length === 0) {
        return {
            name: PROJECT_CONFIG_CHECK_NAME,
            status: "warn",
            detail: "No .lisa.config.json or .lisa.config.local.json found",
        };
    }
    for (const configPath of configPaths) {
        try {
            JSON.parse(await readFile(configPath, "utf8"));
        }
        catch (error) {
            return {
                name: PROJECT_CONFIG_CHECK_NAME,
                status: "fail",
                detail: `${path.basename(configPath)} is not parseable JSON: ${error instanceof Error ? error.message : String(error)}`,
            };
        }
    }
    return {
        name: PROJECT_CONFIG_CHECK_NAME,
        status: "ok",
        detail: configPaths.map(configPath => path.basename(configPath)).join(", "),
    };
}
/**
 * Detect the target project type.
 * @param targetPath - Project path to inspect
 * @returns Doctor check result
 */
async function checkProjectType(targetPath) {
    const detectorRegistry = createDetectorRegistry();
    const detectedTypes = detectorRegistry.expandAndOrderTypes(await detectorRegistry.detectAll(targetPath));
    if (detectedTypes.length === 0) {
        return {
            name: "Project type detection",
            status: "warn",
            detail: "No Lisa project type detected",
        };
    }
    return {
        name: "Project type detection",
        status: "ok",
        detail: detectedTypes.join(", "),
    };
}
/**
 * Confirm starter repositories are reachable and marked as templates.
 * @param deps - Runtime dependencies
 * @param offline - Skip network checks
 * @returns Doctor check result
 */
async function checkStarterHealth(deps, offline) {
    if (offline) {
        return {
            name: STARTER_HEALTH_NAME,
            status: "ok",
            detail: "Skipped GitHub starter checks in offline mode",
        };
    }
    const checks = await Promise.all(Object.values(STARTERS).map(async (starter) => {
        const failurePrefix = `${starter.repo}:`;
        try {
            const response = await deps.fetchImpl(`https://api.github.com/repos/${starter.owner}/${starter.repo}`);
            if (!response.ok) {
                return `${failurePrefix} http-${response.status}`;
            }
            const body = (await response.json());
            if (body.is_template !== true) {
                return `${failurePrefix} not-template`;
            }
        }
        catch {
            return `${failurePrefix} unreachable`;
        }
        return null;
    }));
    const failures = checks.filter((check) => check !== null);
    return {
        name: STARTER_HEALTH_NAME,
        status: failures.length === 0 ? "ok" : "warn",
        detail: failures.length === 0
            ? "Starter repositories are reachable templates"
            : failures.join(", "),
    };
}
/**
 * Check basic wiki contract files when a wiki exists.
 * @param targetPath - Project path to inspect
 * @returns Doctor check result
 */
function checkWiki(targetPath) {
    const wikiPath = path.join(targetPath, "wiki");
    if (!existsSync(wikiPath)) {
        return {
            name: "Wiki health",
            status: "ok",
            detail: "No wiki directory present",
        };
    }
    const required = [
        "lisa-wiki.config.json",
        "schema/llm-wiki-contract.md",
        "index.md",
    ];
    const missing = required.filter(fileName => {
        return !existsSync(path.join(wikiPath, fileName));
    });
    return {
        name: "Wiki health",
        status: missing.length === 0 ? "ok" : "fail",
        detail: missing.length === 0
            ? "Required wiki files are present"
            : `Missing ${missing.join(", ")}`,
    };
}
/**
 * Determine whether a path looks like an agent-governed project, i.e. one that
 * should carry the canonical `AGENTS.md` / `CLAUDE.md` pointer pattern. True
 * when a Lisa config or either instruction file already exists — this keeps the
 * mutating migration from touching unrelated, non-Lisa directories.
 * @param targetPath - Project path to inspect
 * @returns True when the path is an agent/Lisa project
 */
function looksLikeAgentProject(targetPath) {
    return [
        ".lisa.config.json",
        ".lisa.config.local.json",
        AGENTS_MD_FILENAME,
        CLAUDE_MD_FILENAME,
    ].some(fileName => existsSync(path.join(targetPath, fileName)));
}
/**
 * Ensure the project's agent instruction files follow Lisa's canonical pattern
 * (canonical `AGENTS.md` + a thin `CLAUDE.md` that `@AGENTS.md`-imports it) and
 * carry no legacy agy baked-rules block. This check is mutating: it repairs
 * existing projects in place, non-destructively (host content is preserved).
 * @param targetPath - Project path to inspect and repair
 * @returns Doctor check result describing what, if anything, was changed
 */
async function checkInstructionFiles(targetPath) {
    if (!looksLikeAgentProject(targetPath)) {
        return {
            name: INSTRUCTION_FILES_CHECK_NAME,
            status: "ok",
            detail: "Not a Lisa/agent project; skipped",
        };
    }
    try {
        const result = await migrateInstructionFiles(targetPath);
        return {
            name: INSTRUCTION_FILES_CHECK_NAME,
            status: "ok",
            detail: result.changed
                ? `Repaired: ${result.actions.join("; ")}`
                : "Already canonical (AGENTS.md source of truth, CLAUDE.md imports it)",
        };
    }
    catch (error) {
        return {
            name: INSTRUCTION_FILES_CHECK_NAME,
            status: "fail",
            detail: `Could not reconcile instruction files: ${error instanceof Error ? error.message : String(error)}`,
        };
    }
}
/**
 * Run Lisa doctor checks.
 * @param targetPath - Optional project path
 * @param options - Parsed command options
 * @param dependencies - Optional collaborators for tests
 * @returns Doctor result
 */
export async function runDoctor(targetPath, options, dependencies = {}) {
    const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
    const resolvedTarget = path.resolve(targetPath ?? process.cwd());
    const checks = [
        await checkVersion(deps, options.offline === true),
        // Runs early and unconditionally: a repo that has silently stopped applying
        // templates fails every other check's premise, and the two lines below are
        // the ones that name the cause (CodySwannGT/lisa#2467).
        await checkApplyFreshness(resolvedTarget),
        checkYamlRuntime(),
        await checkProjectConfig(resolvedTarget),
        // Immediately after the config check, because it repairs the same file and
        // an operator reading the output wants both config findings together. It
        // is mutating: an undeclared gate is ADDED rather than merely reported,
        // since an undeclared gate means the job never runs and there is no signal
        // to notice (CodySwannGT/lisa#2677).
        await checkTraceabilityGate(resolvedTarget),
        // Immediately after, because both concern gate declarations and an
        // operator wants them together. That check repairs ONE declaration; this
        // one compares every declaration against the ruleset template that
        // enforces it, which is the layer the traceability check only reports.
        await checkDeclaredContexts(resolvedTarget),
        await checkKaneProvider(resolvedTarget, deps),
        await checkSonarProvider(resolvedTarget, deps),
        await checkLegacyMonitorThresholds(resolvedTarget),
        await checkLisaOwnedArtifacts(resolvedTarget),
        await checkReusableWorkflowRefs(resolvedTarget),
        // Immediately after the ref check, because both read the same caller
        // workflows and an operator editing one wants both findings together. This
        // one only REPORTS: `lisa apply` runs on postinstall, and a repair that
        // edits a caller workflow there and gets it wrong is silent
        // (CodySwannGT/lisa#2719).
        await checkSkipJobsMigration(resolvedTarget),
        await checkApplyFailure(resolvedTarget),
        await checkProjectType(resolvedTarget),
        await checkInstructionFiles(resolvedTarget),
        // Runs AFTER the instruction-files check because that check performs the
        // legacy ledger relocation. A project whose ledger merely needs moving is
        // repaired first and passes here; only a genuine second ledger survives to
        // be reported.
        await checkLearningsLedger(resolvedTarget),
        // Immediately after, and for the same reason that check runs late: both
        // resolve the configured ledger path, which the instruction-files check may
        // have just relocated. Reporting the merge arm second is also the order an
        // operator can act in — which ledger is canonical, then whether this
        // checkout can actually run the union merge that protects it.
        await checkLearningsMergeDriver(resolvedTarget),
        await checkWorkerEpoch(resolvedTarget),
        checkLegacyCodexOverlay(resolvedTarget),
        // Environment hygiene rather than project config: accumulated agent
        // worktrees are what make unrelated unit suites time out under ambient
        // load, so an operator debugging a red suite needs the count in front of
        // them (CodySwannGT/lisa#2490).
        // Static answer to a runtime failure that is deliberately silent: an
        // incomplete serialize opt-in warns at 2am on a green job and nowhere else.
        await checkSerializeLegsContract(resolvedTarget),
        // Sibling copies of one hook inside THIS tree, which the Lisa-owned
        // artifact check cannot see: its axis is host copy vs shipped package copy
        // for one destination, so a second copy at another path is outside it by
        // construction (CodySwannGT/lisa#2847).
        await checkHookCopyParity(resolvedTarget),
        await checkWorktreeHygiene(resolvedTarget),
        // Immediately after the count check, and deliberately separate from it.
        // Hygiene answers "how many checkouts is every crawler walking past"; this
        // answers "which of them hold work that exists nowhere else". Retiring a
        // worktree on the first answer without the second is how work is lost.
        await checkWorktreeWorkAtRisk(resolvedTarget),
        await checkStarterHealth(deps, options.offline === true),
        checkWiki(resolvedTarget),
        ...(options.readiness === true
            ? [await checkRepositoryReadiness(resolvedTarget)]
            : []),
    ];
    const result = await renderDoctorResult(checks, resolvedTarget, options, deps.write);
    if (checks.some(check => check.status === "fail")) {
        deps.setExitCode(1);
    }
    return result;
}
//# sourceMappingURL=doctor.js.map