import { listSpecs } from '../storage/spec-store.js';
import { getKnowledge } from '../storage/knowledge-store/knowledge.js';
// SPEC-1011 Bug F: fallback resolver using disk fingerprints
import { resolveProjectFromPath } from '../storage/project-resolver.js';
import { hashProjectPath, readJson } from '../storage/base-store.js';
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { join } from 'node:path';
import { readFile } from 'node:fs/promises';
import { shouldSuggestBtw, buildBtwHint } from '../engine/context-advisor/btw-advisor.js';
import { autoCompleteSpecs } from './git/auto-complete-ops.js';
import { computeSessionAgeMinutes } from '../engine/session-handoff-generator.js';
import { loadSessionState } from '../storage/session-state-store.js';
import { consumeUpdateBanner } from '../engine/update-notifier.js';
import { getPolicy, getRecords } from '../storage/approval-store.js';
import { calculateSLAStatus } from '../engine/approval-workflow.js';
import { readPlanuConfig } from '../engine/planu-config-writer.js';
import { computePlanuDriftScore } from '../engine/dashboard/drift-score.js';
import { findStaleBranches, findStaleWorktrees, findStaleStashes, } from '../engine/housekeeping/index.js';
import { loadTokenWastePolicy } from '../engine/token-optimizer/index.js';
import { getProjectGraphFreshnessHint } from '../engine/project-graph/index.js';
// SPEC-1011 Bug G: fire-and-forget status.json reconciliation
import { reconcileStatusFromDisk } from '../engine/status-reconciler/index.js';
const execFile = promisify(execFileCb);
// ---------------------------------------------------------------------------
// Git helpers
// ---------------------------------------------------------------------------
async function getGitState(projectPath) {
    try {
        const [statusResult, branchResult] = await Promise.all([
            execFile('git', ['status', '--short'], { cwd: projectPath }),
            execFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath }),
        ]);
        const lines = statusResult.stdout.trim().split('\n').filter(Boolean);
        const staged = lines.filter((l) => {
            const ch = l[0];
            return ch !== ' ' && ch !== '?';
        }).length;
        const modified = lines.filter((l) => {
            const ch = l[1];
            return ch !== ' ' && ch !== '?';
        }).length;
        const branch = branchResult.stdout.trim();
        return { staged, modified, branch };
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// CI state helpers
// ---------------------------------------------------------------------------
async function getCiState(projectPath) {
    try {
        const sessionPath = join(projectPath, 'planu', 'session-state.json');
        const state = await readJson(sessionPath, {});
        return state.lastCiStatus ?? null;
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Spec helpers
// ---------------------------------------------------------------------------
function parseSpecNumber(id) {
    const match = /SPEC-(\d+)/i.exec(id);
    return match?.[1] !== undefined ? parseInt(match[1], 10) : 0;
}
async function getStatusSpecSnapshot(projectId) {
    try {
        const specs = await listSpecs(projectId);
        const inProgress = specs
            .filter((s) => s.status === 'implementing' || s.status === 'review')
            .sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
        const approved = specs
            .filter((s) => s.status === 'approved')
            .sort((a, b) => parseSpecNumber(a.id) - parseSpecNumber(b.id));
        const activeItem = inProgress[0];
        const nextItem = approved[0];
        const active = activeItem !== undefined ? { id: activeItem.id, title: activeItem.title } : null;
        const next = nextItem !== undefined ? { id: nextItem.id, title: nextItem.title } : null;
        return { active, next, queueCount: approved.length };
    }
    catch {
        return { active: null, next: null, queueCount: 0 };
    }
}
async function getSLABreaches(projectId) {
    try {
        const policy = await getPolicy(projectId);
        if (policy === null) {
            return [];
        }
        const specs = await listSpecs(projectId);
        const reviewSpecs = specs.filter((s) => s.status === 'review');
        const breaches = [];
        for (const spec of reviewSpecs) {
            const records = await getRecords(projectId, spec.id);
            void records;
            const sla = calculateSLAStatus(spec.updatedAt, policy);
            if (sla.slaBreached) {
                breaches.push({
                    specId: spec.id,
                    slaBreachHours: sla.slaBreachHours,
                    escalateTo: policy.escalateTo,
                });
            }
        }
        return breaches;
    }
    catch {
        return [];
    }
}
// ---------------------------------------------------------------------------
// Suggestion logic
// ---------------------------------------------------------------------------
function buildSuggestion(snapshot) {
    if (snapshot.active !== null) {
        return `Continue ${snapshot.active.id} — it may be blocking others`;
    }
    if (snapshot.next !== null) {
        return `Start ${snapshot.next.id} (highest priority approved)`;
    }
    return 'Create a spec to get started';
}
// ---------------------------------------------------------------------------
// Output builder
// ---------------------------------------------------------------------------
function buildOutput(params) {
    const { snapshot, git, ci, slaBreaches, workMode, updateBanner, autoCompleted, sessionTip, tokenWasteHint, graphFreshnessHint, } = params;
    const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
    if (snapshot.active !== null) {
        const title = snapshot.active.title.slice(0, 40);
        lines.push(`ACTIVE    ${snapshot.active.id} ${title} (in-progress)`);
    }
    else {
        lines.push(`ACTIVE    (none)`);
    }
    if (snapshot.next !== null) {
        const title = snapshot.next.title.slice(0, 40);
        lines.push(`NEXT      ${snapshot.next.id} ${title} (approved)`);
    }
    else {
        lines.push(`NEXT      (no approved specs)`);
    }
    lines.push(`QUEUE     ${snapshot.queueCount} approved specs ready`);
    if (git !== null) {
        lines.push(`GIT       ${git.staged} staged, ${git.modified} modified`);
    }
    if (ci !== null) {
        lines.push(`CI        ${ci}`);
    }
    if (slaBreaches !== undefined && slaBreaches.length > 0) {
        for (const b of slaBreaches) {
            lines.push(`SLA BREACH  ${b.specId} exceeded by ${String(b.slaBreachHours)}h` +
                (b.escalateTo !== undefined ? ` — escalate: ${b.escalateTo}` : ''));
        }
    }
    if (autoCompleted !== undefined && autoCompleted.length > 0) {
        lines.push(`AUTO-DONE  ${autoCompleted.join(', ')} (branch merged to develop)`);
    }
    if (sessionTip !== undefined) {
        lines.push(`SESSION TIP  ${sessionTip}`);
    }
    if (tokenWasteHint !== undefined) {
        lines.push(`TOKEN WASTE ${tokenWasteHint}`);
    }
    if (graphFreshnessHint !== undefined) {
        lines.push(`GRAPH     ${graphFreshnessHint}`);
    }
    lines.push(`SUGGEST   ${buildSuggestion(snapshot)}`);
    if (workMode !== undefined) {
        lines.push(`WORK MODE ${workMode}`);
    }
    if (updateBanner !== undefined) {
        lines.push('');
        lines.push(updateBanner);
    }
    return lines.join('\n');
}
// ---------------------------------------------------------------------------
// Public handler
// ---------------------------------------------------------------------------
export async function handlePlanStatus(args) {
    const projectId = args.projectId ?? hashProjectPath(args.projectPath);
    const planuConfigPath = join(args.projectPath, 'planu.json');
    // SPEC-1011 Bug F: Use resilient resolver that falls back to disk fingerprints.
    // If knowledge.json is missing but planu/ or data/projects/{id}/ exist, the project
    // is recovered silently and a minimal knowledge entry is written.
    // Skip the resolver fallback when projectId is explicitly provided — caller already
    // knows the project; resolver would compute hash redundantly and break test contracts.
    let knowledgeExists = (await getKnowledge(projectId)) !== null;
    if (!knowledgeExists && args.projectId === undefined) {
        knowledgeExists = (await resolveProjectFromPath(args.projectPath)) !== null;
    }
    if (!knowledgeExists) {
        return {
            content: [
                {
                    type: 'text',
                    text: `Project not initialized at ${args.projectPath}. Run init_project first.`,
                },
            ],
            isError: true,
        };
    }
    // SPEC-1011 Bug G: reconcile status.json with disk reality (fire-and-forget, non-blocking).
    // Skip when projectId is explicitly provided to preserve callers' contract that projectPath
    // is not re-hashed; the reconciler is best-effort observability and not load-bearing here.
    if (args.projectId === undefined) {
        void reconcileStatusFromDisk(args.projectPath, projectId).catch(() => {
            /* best-effort */
        });
    }
    const [snapshot, git, ci, slaBreaches, planuConfig, autoCompleted, sessionState, driftScore, pendingCleanup, tokenWastePolicy, graphFreshnessHint,] = await Promise.all([
        getStatusSpecSnapshot(projectId),
        getGitState(args.projectPath),
        getCiState(args.projectPath),
        getSLABreaches(projectId),
        readPlanuConfig(planuConfigPath, readFile),
        // SPEC-720: autoCompleteSpecs now returns {completed, blocked} — extract completed for display
        autoCompleteSpecs(projectId, args.projectPath)
            .then((r) => r.completed)
            .catch(() => []),
        loadSessionState(args.projectPath).catch(() => null),
        // SPEC-748: compute drift score from pending.json drift_review entries vs done specs
        computePlanuDriftScore(args.projectPath).catch(() => null),
        // SPEC-751: pending cleanup counters
        (async () => {
            try {
                const [branchResult, worktrees, stashes] = await Promise.all([
                    findStaleBranches({ projectPath: args.projectPath }),
                    findStaleWorktrees({ projectPath: args.projectPath }),
                    findStaleStashes({ projectPath: args.projectPath }),
                ]);
                return {
                    branches: branchResult.stale.length,
                    worktrees: worktrees.length,
                    stashes: stashes.length,
                };
            }
            catch {
                return { branches: 0, worktrees: 0, stashes: 0 };
            }
        })(),
        loadTokenWastePolicy(args.projectPath)
            .then((loaded) => loaded.policy)
            .catch(() => null),
        getProjectGraphFreshnessHint({ projectId, projectPath: args.projectPath }).catch(() => undefined),
    ]);
    // SPEC-1012: Detect stale release status (best-effort, non-blocking)
    let staleStatusWarnings = [];
    try {
        const { detectReleaseStatusDrift } = await import('../engine/drift-detection/release-status-drift.js');
        staleStatusWarnings = await detectReleaseStatusDrift(args.projectPath);
    }
    catch {
        /* best-effort — never block planu_status */
    }
    const sessionAgeMinutes = sessionState !== null ? computeSessionAgeMinutes(sessionState.savedAt) : null;
    const SESSION_TIP_THRESHOLD_MIN = 60;
    const sessionTip = sessionAgeMinutes !== null && sessionAgeMinutes > SESSION_TIP_THRESHOLD_MIN
        ? `Last checkpoint ${String(sessionAgeMinutes)}min ago — run session_handoff then start a fresh session to cut token costs`
        : undefined;
    const tokenWasteThresholdMin = tokenWastePolicy?.context.staleSessionMinutes;
    const tokenWasteHint = tokenWasteThresholdMin !== undefined &&
        sessionAgeMinutes !== null &&
        sessionAgeMinutes > tokenWasteThresholdMin
        ? `checkpoint is stale; package or hand off context before continuing`
        : undefined;
    const workMode = planuConfig?.workMode;
    const updateBanner = consumeUpdateBanner() ?? undefined;
    const text = buildOutput({
        snapshot,
        git,
        ci,
        slaBreaches,
        workMode,
        updateBanner,
        autoCompleted,
        sessionTip,
        tokenWasteHint,
        graphFreshnessHint,
    });
    // SPEC-256: Always show /btw hint as educational tip (no context percent available from MCP layer)
    const DEMO_CONTEXT_PERCENT = 65;
    const btwHint = shouldSuggestBtw(DEMO_CONTEXT_PERCENT)
        ? buildBtwHint('Context at ' +
            String(DEMO_CONTEXT_PERCENT) +
            '% — use /btw for side questions to keep implementation context clean')
        : undefined;
    return {
        content: [{ type: 'text', text }],
        structuredContent: {
            activeSpec: snapshot.active,
            nextSpec: snapshot.next,
            queueCount: snapshot.queueCount,
            git,
            ci,
            slaBreaches: slaBreaches.length > 0 ? slaBreaches : undefined,
            workMode: workMode ?? 'standard',
            // SPEC-748: drift score
            planu_drift_score: driftScore?.score ?? 0,
            lastAuditAt: driftScore?.lastAuditAt ?? null,
            ...(autoCompleted.length > 0 ? { autoCompleted } : {}),
            ...(sessionTip !== undefined ? { sessionTip } : {}),
            ...(tokenWasteHint !== undefined ? { tokenWasteHint } : {}),
            ...(graphFreshnessHint !== undefined ? { graphFreshnessHint } : {}),
            ...(btwHint !== undefined ? { btw_hint: btwHint } : {}),
            ...(updateBanner !== undefined ? { update_banner: updateBanner } : {}),
            // SPEC-751: pending cleanup counters
            pending_cleanup: pendingCleanup,
            // SPEC-1012: stale status warnings (released but not done)
            ...(staleStatusWarnings.length > 0 ? { staleStatusWarnings } : {}),
        },
    };
}
//# sourceMappingURL=status-handler.js.map