/**
 * MaintenanceService — supervised self-test and self-fix pipeline.
 *
 * Flow:
 *   1. Cron triggers a maintenance run for a repo
 *   2. Runs configured checks (typecheck, test, lint) via shell
 *   3. Parses failures from output
 *   4. If failures found → creates a Plan with proposed fix tasks
 *   5. Broadcasts results via WS so the user can supervise
 *   6. Tasks stay "proposed" until the user approves & starts them
 *
 * The user stays in the loop: they see every run, every failure, and
 * every proposed fix before any agent thread touches code.
 */
import { eq, desc } from "drizzle-orm";
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { scheduledJobRuns } from "../db/schema.js";
import { uuidv7 } from "../db/uuidv7.js";
import { newTaskId } from "./plans.js";
const DEFAULT_CHECKS = [
    { name: "Typecheck", command: "bun run typecheck" },
    { name: "Tests", command: "bun run test" },
    { name: "Lint", command: "bun run lint" },
];
const DEFAULT_TIMEOUT_MS = 120_000;
// ── Service ──────────────────────────────────────────────────────────
export class MaintenanceService {
    db;
    planService;
    repoService;
    constructor(db, planService, repoService) {
        this.db = db;
        this.planService = planService;
        this.repoService = repoService;
    }
    /**
     * Run maintenance checks for ALL accessible repositories.
     * Skips repos whose paths don't exist locally.
     */
    async runAll(jobId, config) {
        const repos = this.repoService.list();
        const results = [];
        for (const repo of repos) {
            if (!existsSync(repo.localPath))
                continue;
            try {
                const result = await this.runForRepo(repo.id, jobId, config);
                results.push(result);
            }
            catch (err) {
                console.error(`Maintenance failed for ${repo.name}:`, err);
            }
        }
        return results;
    }
    /**
     * Run maintenance checks for a specific repository.
     * Returns structured results; creates a fix plan if anything failed.
     */
    async runForRepo(repoId, jobId, config) {
        const repo = this.repoService.getById(repoId);
        if (!repo)
            throw new Error(`Repository not found: ${repoId}`);
        if (!existsSync(repo.localPath)) {
            throw new Error(`Repository path not accessible: ${repo.localPath}`);
        }
        const checks = config?.checks ?? DEFAULT_CHECKS;
        const timeoutMs = config?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
        const runId = uuidv7();
        const startedAt = new Date().toISOString();
        // Persist run as "running"
        this.db.insert(scheduledJobRuns).values({
            id: runId,
            jobId,
            status: "running",
            triggeredBy: "maintenance",
            startedAt,
        }).run();
        const results = [];
        for (const check of checks) {
            const checkStart = Date.now();
            let output = "";
            let passed = false;
            try {
                output = execSync(check.command, {
                    cwd: repo.localPath,
                    timeout: timeoutMs,
                    encoding: "utf-8",
                    stdio: ["pipe", "pipe", "pipe"],
                    maxBuffer: 1024 * 1024, // 1MB
                    env: { ...process.env, CI: "true", FORCE_COLOR: "0" },
                    windowsHide: true,
                });
                passed = true;
            }
            catch (err) {
                if (err && typeof err === "object" && "stdout" in err) {
                    const execErr = err;
                    output = [execErr.stdout ?? "", execErr.stderr ?? ""].filter(Boolean).join("\n");
                }
                else {
                    output = err instanceof Error ? err.message : String(err);
                }
                passed = false;
            }
            results.push({
                name: check.name,
                command: check.command,
                passed,
                output: output.slice(0, 50_000), // cap stored output
                durationMs: Date.now() - checkStart,
            });
        }
        const allPassed = results.every((r) => r.passed);
        const completedAt = new Date().toISOString();
        // Create fix plan if there were failures
        let planId;
        if (!allPassed) {
            planId = this.createFixPlan(repo, results, config?.preferredProvider);
        }
        // Persist run result
        const outputSummary = results
            .map((r) => `${r.passed ? "✓" : "✗"} ${r.name} (${r.durationMs}ms)`)
            .join("\n");
        const errorSummary = results
            .filter((r) => !r.passed)
            .map((r) => `${r.name}: ${r.output.slice(0, 2000)}`)
            .join("\n---\n");
        this.db.update(scheduledJobRuns).set({
            status: allPassed ? "completed" : "failed",
            output: outputSummary,
            error: allPassed ? null : errorSummary.slice(0, 50_000),
            planId: planId ?? null,
            completedAt,
        }).where(eq(scheduledJobRuns.id, runId)).run();
        return {
            id: runId,
            jobId,
            repoId: repo.id,
            repoName: repo.name,
            checks: results,
            allPassed,
            planId,
            startedAt,
            completedAt,
        };
    }
    /** Get recent runs, optionally filtered by jobId. */
    listRuns(jobId, limit = 50) {
        if (jobId) {
            return this.db
                .select()
                .from(scheduledJobRuns)
                .where(eq(scheduledJobRuns.jobId, jobId))
                .orderBy(desc(scheduledJobRuns.startedAt))
                .limit(limit)
                .all();
        }
        return this.db
            .select()
            .from(scheduledJobRuns)
            .orderBy(desc(scheduledJobRuns.startedAt))
            .limit(limit)
            .all();
    }
    /** Get a single run by ID. */
    getRun(id) {
        return this.db
            .select()
            .from(scheduledJobRuns)
            .where(eq(scheduledJobRuns.id, id))
            .get();
    }
    // ── Private ──────────────────────────────────────────────────────
    /**
     * Parse check failures and create a Plan with proposed fix tasks.
     * Tasks are left as "proposed" — the user must approve before they run.
     */
    createFixPlan(repo, results, _preferredProvider) {
        const failures = results.filter((r) => !r.passed);
        const tasks = failures.map((failure) => ({
            id: newTaskId(),
            title: `Fix ${failure.name} failures`,
            description: [
                `The \`${failure.command}\` check failed. Fix the errors below.`,
                "",
                "```",
                failure.output.slice(0, 8000),
                "```",
                "",
                "Run the same command again after fixing to verify.",
            ].join("\n"),
            status: "proposed",
        }));
        const plan = this.planService.create({
            repoId: repo.id,
            title: `Maintenance: ${failures.length} check(s) failed — ${new Date().toLocaleDateString()}`,
            tasks,
        });
        return plan.id;
    }
}
//# sourceMappingURL=maintenance.js.map