/**
 * skill_health composer rule — 健康度体检的"复合 rule"。
 *
 * 单次 LLM 调用产出全部已注册维度的 checklist;composer.checkAll() 返回 N+1 条
 * outcome:N = 注册维度数(每维一条),1 = summary(top_suggestions / overall_health
 * / feature_points 落在这条)。
 *
 * doctor engine 检测 kind='composer' 时把 outcome[] 展开成 N+1 条独立 DoctorRuleResult,
 * 共享 groupId = composer.id,renderer 按 groupId 分组渲染。
 *
 * 工厂模式 makeSkillHealthComposer(execFactory) 给测试注入 mock executor。
 * 默认 import 时不注册;register.ts 才做 registerRule 副作用。
 */
import { existsSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { tDoctorMessage } from '../messages.js';
import { createExecutor } from '../../executors/index.js';
import { getRegisteredHealthDimensions } from './dimension-registry.js';
import { buildHealthPrompt } from './prompt-builder.js';
import { deriveOverallHealth, extractJson, parseHealthOutput } from './parser.js';
export const SKILL_HEALTH_COMPOSER_ID = 'skill_health';
const defaultExecutorFactory = createExecutor;
// ---------------------------------------------------------------------------
// Status mapping
// ---------------------------------------------------------------------------
function mapDimLevelToStatus(level, missing) {
    if (missing)
        return 'skipped';
    switch (level) {
        case '不健康': return 'fail';
        case '亚健康': return 'warn';
        case '健康': return 'pass';
        case '不适用': return 'skipped';
    }
}
// ---------------------------------------------------------------------------
// Per-dim outcome rendering
// ---------------------------------------------------------------------------
function dimMessage(dim, r, lang) {
    if (r._missing) {
        return tDoctorMessage('cli.doctor.health.dim.missing', lang, { dim: dim.displayName });
    }
    const errs = r.findings.filter((f) => f.level === '错误').length;
    const warns = r.findings.filter((f) => f.level === '警告').length;
    const sugs = r.findings.filter((f) => f.level === '建议').length;
    return tDoctorMessage('cli.doctor.health.dim.message', lang, {
        level: r.level, err: errs, warn: warns, sug: sugs,
    });
}
function dimHint(r) {
    // 优先从 finding-level suggestion 取（新契约），fallback 到维度级 suggestions（旧契约兼容）
    const fromFinding = r.findings.find((f) => f.suggestion)?.suggestion;
    if (fromFinding)
        return fromFinding;
    if (r.suggestions.length > 0)
        return r.suggestions[0];
    return undefined;
}
// ---------------------------------------------------------------------------
// Sub-files / siblings discovery (与上一版本相同)
// ---------------------------------------------------------------------------
function listSubFiles(skillRoot) {
    if (!skillRoot || !existsSync(skillRoot))
        return [];
    const out = [];
    const walk = (dir, rel, depth) => {
        if (depth > 2)
            return;
        let entries = [];
        try {
            entries = readdirSync(dir);
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (entry.startsWith('.'))
                continue;
            if (entry === 'SKILL.md' && rel === '')
                continue;
            if (entry === 'node_modules')
                continue;
            const full = join(dir, entry);
            const relPath = rel ? `${rel}/${entry}` : entry;
            let st;
            try {
                st = statSync(full);
            }
            catch {
                continue;
            }
            if (st.isDirectory())
                walk(full, relPath, depth + 1);
            else if (st.isFile())
                out.push(relPath);
        }
    };
    walk(skillRoot, '', 0);
    return out.sort();
}
function listSiblingSkills(skillRoot) {
    if (!skillRoot || !existsSync(skillRoot))
        return [];
    const parent = dirname(skillRoot);
    if (!existsSync(parent))
        return [];
    const self = skillRoot.split('/').pop() ?? '';
    let entries = [];
    try {
        entries = readdirSync(parent);
    }
    catch {
        return [];
    }
    return entries
        .filter((e) => !e.startsWith('.') && e !== self)
        .filter((e) => {
        try {
            return statSync(join(parent, e)).isDirectory();
        }
        catch {
            return false;
        }
    })
        .sort();
}
// ---------------------------------------------------------------------------
// Composer factory
// ---------------------------------------------------------------------------
export function makeSkillHealthComposer(execFactory = defaultExecutorFactory) {
    return {
        id: SKILL_HEALTH_COMPOSER_ID,
        kind: 'composer',
        severity: 'fatal',
        labelKey: 'cli.doctor.rule.skill_health_check',
        checkAll: (ctx) => composerCheckAll(ctx, execFactory),
    };
}
async function composerCheckAll(ctx, execFactory) {
    const dims = getRegisteredHealthDimensions();
    // Programmatic opt-out: runHealthCheck=false → 给 summary 一条 skipped,
    // 各维度不展开避免污染报告。CLI 的 --static-only 会直接过滤掉 composer rule。
    if (!ctx.runHealthCheck) {
        return [{
                subId: '_summary',
                severity: 'info',
                labelKey: 'cli.doctor.rule.skill_health_check',
                status: 'skipped',
                message: tDoctorMessage('cli.doctor.health.skipped', ctx.lang),
            }];
    }
    // 没注册任何维度 → 短路
    if (dims.length === 0) {
        return [{
                subId: '_summary',
                severity: 'info',
                labelKey: 'cli.doctor.rule.skill_health_check',
                status: 'skipped',
                message: tDoctorMessage('cli.doctor.health.no_dimensions', ctx.lang),
            }];
    }
    const content = ctx.artifact.content;
    if (!content || content.trim().length === 0) {
        // skill_readable rule 也会 fail;这里只是 short-circuit 防止白白调 LLM
        return [{
                subId: '_summary',
                severity: 'info',
                labelKey: 'cli.doctor.rule.skill_health_check',
                status: 'skipped',
                message: tDoctorMessage('cli.doctor.health.skipped', ctx.lang),
            }];
    }
    const skillName = ctx.artifact.name.replace(/\.md$/, '').split('/').pop() ?? ctx.artifact.name;
    const skillRoot = ctx.artifact.skillRoot ?? null;
    const subFiles = listSubFiles(skillRoot);
    const siblingSkills = listSiblingSkills(skillRoot);
    const prompt = buildHealthPrompt({ skillName, skillContent: content, skillRoot, subFiles, siblingSkills }, dims);
    const executor = execFactory(ctx.executorName);
    let res;
    try {
        res = await executor({
            model: ctx.model,
            prompt,
            cwd: skillRoot ?? ctx.cwd,
            skillDir: skillRoot ?? null,
            timeoutMs: ctx.timeoutMs,
            effort: ctx.effort ?? 'medium',
            lean: true,
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        return [errorSummaryOutcome(ctx, 'executor', msg)];
    }
    if (!res.ok) {
        return [errorSummaryOutcome(ctx, 'executor', res.error ?? 'unknown')];
    }
    if (!res.output || res.output.trim().length === 0) {
        return [errorSummaryOutcome(ctx, 'empty_output', '')];
    }
    const cleaned = extractJson(res.output);
    if (cleaned === null) {
        return [errorSummaryOutcome(ctx, 'extract', '', { rawOutput: res.output.slice(0, 4000) })];
    }
    const parseResult = parseHealthOutput(cleaned, dims);
    const overall = parseResult.overall ?? deriveOverallHealth(parseResult.byDimId);
    // 诊断:如果所有维度都被标 _missing,说明 LLM 输出 JSON 里 dim_id 字段缺失或写错。
    // 此时应报 fail(解析失败),不能当作"全部通过"。
    const allMissing = [...parseResult.byDimId.values()].every((r) => r._missing);
    if (allMissing) {
        return [errorSummaryOutcome(ctx, 'extract', 'LLM output missing all dimension IDs', { rawSample: cleaned.slice(0, 3000) })];
    }
    // 汇总 finding 计数(框架算,不信 LLM 算术)
    const finding = { '错误': 0, '警告': 0, '建议': 0 };
    const dimCount = { '健康': 0, '亚健康': 0, '不健康': 0, '不适用': 0 };
    for (const r of parseResult.byDimId.values()) {
        if (r._missing)
            continue;
        dimCount[r.level] += 1;
        for (const f of r.findings)
            finding[f.level] += 1;
    }
    // N 条维度 outcome
    const dimOutcomes = dims.map((dim) => {
        const r = parseResult.byDimId.get(dim.id);
        return {
            subId: dim.id,
            labelKey: dim.labelKey,
            severity: dim.severity,
            status: mapDimLevelToStatus(r.level, r._missing),
            message: dimMessage(dim, r, ctx.lang),
            hint: dimHint(r),
            detail: {
                dimensionId: dim.id,
                displayName: dim.displayName,
                level: r.level,
                findings: r.findings,
                suggestions: r.suggestions,
                missing: r._missing ?? false,
            },
        };
    });
    // 1 条 summary outcome
    const summaryOutcome = {
        subId: '_summary',
        severity: 'info',
        labelKey: 'cli.doctor.health.summary.label',
        status: 'pass', // summary 永远 pass(信息性,不影响 outcome)
        message: tDoctorMessage('cli.doctor.health.summary.message', ctx.lang, {
            overall,
            h: dimCount['健康'], sh: dimCount['亚健康'], bad: dimCount['不健康'], na: dimCount['不适用'],
            err: finding['错误'], warn: finding['警告'], sug: finding['建议'],
        }),
        hint: parseResult.topSuggestions.slice(0, 3).join(' | ')
            || tDoctorMessage('cli.doctor.health.summary.no_top', ctx.lang),
        detail: {
            overall,
            dimensionLevelCount: dimCount,
            findingCountByLevel: finding,
            topSuggestions: parseResult.topSuggestions,
            featurePoints: parseResult.featurePoints,
            durationMs: res.durationMs,
            durationApiMs: res.durationApiMs,
            // turns:claude-cli 在内部跑的 LLM 轮次(每轮 = 1 次 messages.create)
            // numTurns:assistant turn 数 / fullNumTurns:含 tool turn 的总数
            llmTurns: { numTurns: res.numTurns, fullNumTurns: res.fullNumTurns },
            tokens: {
                input: res.inputTokens, output: res.outputTokens,
                cacheRead: res.cacheReadTokens, cacheCreation: res.cacheCreationTokens,
            },
            costUSD: res.costUSD,
            executor: { name: ctx.executorName, model: ctx.model },
        },
    };
    return [...dimOutcomes, summaryOutcome];
}
function errorSummaryOutcome(ctx, phase, errMsg, extra = {}) {
    const messageKey = phase === 'executor'
        ? 'cli.doctor.health.fail.executor'
        : phase === 'empty_output'
            ? 'cli.doctor.health.fail.empty_output'
            : 'cli.doctor.health.fail.parse';
    const hintKey = phase === 'executor'
        ? 'cli.doctor.health.hint.executor'
        : 'cli.doctor.health.hint.parse';
    return {
        subId: '_summary',
        severity: 'fatal',
        labelKey: 'cli.doctor.health.summary.label',
        status: 'fail',
        message: tDoctorMessage(messageKey, ctx.lang, { error: errMsg.slice(0, 200) }),
        hint: tDoctorMessage(hintKey, ctx.lang),
        detail: { phase, error: errMsg, ...extra },
    };
}
//# sourceMappingURL=composer.js.map