import fs from "node:fs/promises";
import fss from "node:fs";
import path from "node:path";
import { debug } from "../core/logger.js";
import { safeJsonParse } from "../core/helpers.js";
/**
 * 系统可观测模块 — 采集任务指标、检测异常告警、生成周期报告和成本估算。
 * 基于 TaskContext JSON 文件聚合计算，零外部依赖。
 */
// ── 各任务类型的单次 Token 消耗估算值 ──
// 用于成本预估和预算检查，单位为 token 数
const TOKEN_ESTIMATES = {
    feature: 8_000, // 新功能：涉及上下文理解和代码生成，估算较高
    bugfix: 5_000, // 缺陷修复：上下文范围相对集中
    refactor: 10_000, // 重构：需要全局理解和多文件修改，估算最高
    hotfix: 4_000, // 紧急修复：范围明确，耗时较短
};
/** 未知任务类型的默认 Token 估算值 */
const DEFAULT_TOKEN_ESTIMATE = 3_000;
/**
 * 从指定目录加载所有 TaskContext JSON 文件。
 * 跳过损坏或格式不正确的文件，忽略无 task_id 或 created_at 的无效条目。
 * @param stateDir - 任务状态文件目录路径
 * @returns 有效的任务上下文列表（附加文件 mtime 信息）
 */
async function loadTasks(stateDir) {
    const tasks = [];
    let files;
    try {
        files = fss.readdirSync(stateDir);
    }
    catch (e) {
        debug("observability", "读取任务状态目录失败", e);
        return tasks;
    }
    const jsonFiles = files.filter((f) => f.endsWith(".json"));
    for (const file of jsonFiles) {
        try {
            const filePath = path.join(stateDir, file);
            const raw = await fs.readFile(filePath, "utf-8");
            const parsed = safeJsonParse(raw);
            if (parsed && typeof parsed.task_id === "string" && typeof parsed.created_at === "string") {
                const stat = fss.statSync(filePath);
                parsed._mtimeMs = stat.mtimeMs;
                tasks.push(parsed);
            }
        }
        catch {
            // 跳过损坏或不可读的文件
        }
    }
    return tasks;
}
/**
 * 计算单个任务的持续时间（毫秒）。
 * 已完成任务使用文件修改时间作为结束时间的近似值，避免 "now - created" 一直增长的问题。
 * 进行中任务使用当前时间作为结束时间。
 * @param task - 任务上下文
 * @param now - 当前时间戳（毫秒）
 * @param fileMtimeMs - 任务文件最后修改时间（毫秒），用于已完成任务的精确耗时计算
 * @returns 任务持续时间（毫秒）
 */
function taskDurationMs(task, now, fileMtimeMs) {
    const created = new Date(task.created_at).getTime();
    // 已完成任务：用文件 mtime 近似任务完成时刻，比 "now - created_at" 更准确
    if (task.status === "done" && fileMtimeMs && fileMtimeMs > created) {
        return fileMtimeMs - created;
    }
    // 进行中任务：用当前时间作为截止点
    return now - created;
}
// ── 公开接口 ──
/**
 * 采集任务目录下的所有 TaskContext，计算聚合指标。
 * 包括通过率、重试率、平均耗时和知识匹配率。
 * @param stateDir - 任务状态文件目录路径
 * @returns 指标快照，包含通过率、重试率、平均耗时、知识匹配率
 */
export async function collectMetrics(stateDir) {
    const tasks = await loadTasks(stateDir);
    debug("可观测", "开始采集指标, stateDir=" + stateDir);
    const now = Date.now();
    if (tasks.length === 0) {
        return {
            timestamp: new Date(now).toISOString(),
            total_tasks: 0,
            pass_rate: 0,
            retry_rate: 0,
            avg_duration_ms: 0,
            knowledge_hit_rate: 0,
        };
    }
    const total = tasks.length;
    const doneCount = tasks.filter((t) => t.status === "done").length;
    const passRate = doneCount / total;
    // 统计有过失败历史的任务（即经历过重试的任务）
    const retriedCount = tasks.filter((t) => t.execution && t.execution.failure_history && t.execution.failure_history.length > 0).length;
    const retryRate = retriedCount / total;
    // 计算所有任务的平均持续时间
    const durations = tasks.map((t) => taskDurationMs(t, now, t._mtimeMs));
    const avgDuration = durations.reduce((a, b) => a + b, 0) / total;
    // 知识匹配率：在有过 expansion 的任务中，匹配到知识条目的比例
    const tasksWithExpansion = tasks.filter((t) => !!t.expansion);
    const knowledgeHitCount = tasksWithExpansion.filter((t) => t.expansion?.matched_knowledge && t.expansion.matched_knowledge.length > 0).length;
    // 避免除零：无 expansion 记录时知识匹配率为 0
    const knowledgeHitRate = tasksWithExpansion.length > 0 ? knowledgeHitCount / tasksWithExpansion.length : 0;
    // 保留 3 位小数精度
    return {
        timestamp: new Date(now).toISOString(),
        total_tasks: total,
        pass_rate: Math.round(passRate * 1000) / 1000,
        retry_rate: Math.round(retryRate * 1000) / 1000,
        avg_duration_ms: Math.round(avgDuration),
        knowledge_hit_rate: Math.round(knowledgeHitRate * 1000) / 1000,
    };
}
/**
 * 基于阈值检测指标异常，生成告警列表。
 * 告警规则：
 *   - 失败率 >= 30% → critical
 *   - 失败率 >= 15% → warning
 *   - 重试率 > 40% → warning
 *   - 平均耗时 > 5 分钟 → warning
 *   - 知识匹配率 < 50% → warning
 * @param snapshot - 采集到的指标快照
 * @returns 告警条目列表（按检测顺序排列）
 */
export function checkAlerts(snapshot) {
    debug("可观测性", "checkAlerts() 开始执行");
    const alerts = [];
    // 失败率超过 30% 时触发严重告警
    if (snapshot.pass_rate < 0.7 && snapshot.total_tasks > 0) {
        alerts.push({
            level: "critical",
            metric: "fail_rate",
            current: Math.round((1 - snapshot.pass_rate) * 1000) / 1000,
            threshold: 0.3,
            message: "近期任务失败率过高",
        });
    }
    // 失败率超过 15% 时触发警告告警
    else if (snapshot.pass_rate < 0.85 && snapshot.total_tasks > 0) {
        alerts.push({
            level: "warning",
            metric: "fail_rate",
            current: Math.round((1 - snapshot.pass_rate) * 1000) / 1000,
            threshold: 0.15,
            message: "任务失败率呈上升趋势",
        });
    }
    // 重试率超过 40% 说明任务首次执行成功率偏低
    if (snapshot.retry_rate > 0.4) {
        alerts.push({
            level: "warning",
            metric: "retry_rate",
            current: snapshot.retry_rate,
            threshold: 0.4,
            message: "任务重试率偏高",
        });
    }
    // 平均耗时超过 5 分钟（300,000ms）说明任务可能过重
    if (snapshot.avg_duration_ms > 300_000) {
        alerts.push({
            level: "warning",
            metric: "avg_duration_ms",
            current: snapshot.avg_duration_ms,
            threshold: 300_000,
            message: "平均任务耗时过长",
        });
    }
    // 知识匹配率低于 50% 提示需要补充项目知识
    if (snapshot.knowledge_hit_rate < 0.5 && snapshot.total_tasks > 0) {
        alerts.push({
            level: "warning",
            metric: "knowledge_hit_rate",
            current: snapshot.knowledge_hit_rate,
            threshold: 0.5,
            message: "知识匹配率偏低，考虑补充项目知识",
        });
    }
    return alerts;
    if (alerts.length > 0) {
        debug("可观测", "检测到 " + alerts.length + " 条告警");
    }
}
/**
 * 从真实 TaskContext 状态目录生成交付指标治理样本。
 * @description 该函数用于 sf_deliver 主链路，把 observability 聚合指标转换为
 *              metric_governance.evaluateMetrics 可消费的 MetricSample。
 *              样本来自 .soloforge/state 中的真实 TaskContext，不允许调用方传空数组冒充通过。
 *              历史样本不足时只输出"数据源可用"样本，避免把缺少历史数据伪装成业务指标通过。
 * @param stateDir - TaskContext 状态目录
 * @param minCompletedTasks - 启用质量指标门控所需的最小已完成任务数
 */
export async function collectDeliveryMetricSamples(stateDir, minCompletedTasks = 3) {
    debug("可观测", "采集交付指标治理样本, stateDir=" + stateDir);
    const snapshot = await collectMetrics(stateDir);
    const sampledAt = snapshot.timestamp;
    const completedTaskCount = Math.round(snapshot.pass_rate * snapshot.total_tasks);
    const samples = [
        {
            metric_id: "observability.data_source_available",
            value: snapshot.total_tasks,
            sampled_at: sampledAt,
            sample_status: "passed",
        },
    ];
    if (completedTaskCount < minCompletedTasks) {
        return samples;
    }
    samples.push({
        metric_id: "delivery.pass_rate",
        value: snapshot.pass_rate,
        sampled_at: sampledAt,
        sample_status: snapshot.pass_rate >= 0.85 ? "passed" : snapshot.pass_rate >= 0.7 ? "degraded" : "manual_required",
    });
    samples.push({
        metric_id: "delivery.retry_rate",
        value: snapshot.retry_rate,
        sampled_at: sampledAt,
        sample_status: snapshot.retry_rate <= 0.4 ? "passed" : "degraded",
    });
    samples.push({
        metric_id: "delivery.avg_duration_ms",
        value: snapshot.avg_duration_ms,
        sampled_at: sampledAt,
        sample_status: snapshot.avg_duration_ms <= 300_000 ? "passed" : "degraded",
    });
    samples.push({
        metric_id: "delivery.knowledge_hit_rate",
        value: snapshot.knowledge_hit_rate,
        sampled_at: sampledAt,
        sample_status: snapshot.knowledge_hit_rate >= 0.5 ? "passed" : "degraded",
    });
    return samples;
}
/**
 * 生成指定周期内的完整可观测性报告。
 * 包含聚合指标、告警、每日趋势数据和中文摘要。
 * @param stateDir - 任务状态文件目录路径
 * @param periodDays - 报告回溯天数，默认 30 天
 * @returns 可观测性报告，包含时间范围、指标快照、告警、趋势和摘要
 */
export async function generateReport(stateDir, periodDays = 30) {
    debug("可观测", "生成报告, 回溯天数=" + periodDays);
    const now = new Date();
    // 计算报告起始时间点
    const start = new Date(now.getTime() - periodDays * 24 * 60 * 60 * 1000);
    const period = {
        start: start.toISOString(),
        end: now.toISOString(),
    };
    const snapshot = await collectMetrics(stateDir);
    const alerts = checkAlerts(snapshot);
    // 按创建日期将任务分桶，构建每日趋势数据
    const tasks = await loadTasks(stateDir);
    const dailyMap = new Map();
    for (const task of tasks) {
        const taskDate = new Date(task.created_at);
        if (taskDate >= start && taskDate <= now) {
            const dayKey = taskDate.toISOString().slice(0, 10); // YYYY-MM-DD 格式作为分桶键
            const bucket = dailyMap.get(dayKey) ?? [];
            bucket.push(task);
            dailyMap.set(dayKey, bucket);
        }
    }
    // 按日期升序排列
    const sortedDays = Array.from(dailyMap.keys()).sort();
    const passRateValues = [];
    const retryRateValues = [];
    const avgDurationValues = [];
    for (const day of sortedDays) {
        const dayTasks = dailyMap.get(day);
        const dayTotal = dayTasks.length;
        const dayDone = dayTasks.filter((t) => t.status === "done").length;
        passRateValues.push(dayTotal > 0 ? Math.round((dayDone / dayTotal) * 1000) / 1000 : 0);
        const dayRetried = dayTasks.filter((t) => t.execution && t.execution.failure_history && t.execution.failure_history.length > 0).length;
        retryRateValues.push(dayTotal > 0 ? Math.round((dayRetried / dayTotal) * 1000) / 1000 : 0);
        const nowMs = now.getTime();
        const dayDurations = dayTasks.map((t) => taskDurationMs(t, nowMs, t._mtimeMs));
        avgDurationValues.push(dayTotal > 0 ? Math.round(dayDurations.reduce((a, b) => a + b, 0) / dayTotal) : 0);
    }
    const trends = [
        { metric: "pass_rate", values: passRateValues },
        { metric: "retry_rate", values: retryRateValues },
        { metric: "avg_duration_ms", values: avgDurationValues },
    ];
    const passPercent = (snapshot.pass_rate * 100).toFixed(1);
    const retryPercent = (snapshot.retry_rate * 100).toFixed(1);
    const knowledgePercent = (snapshot.knowledge_hit_rate * 100).toFixed(1);
    const summary = `过去${periodDays}天共执行 ${snapshot.total_tasks} 个任务，通过率 ${passPercent}%，重试率 ${retryPercent}%，知识匹配率 ${knowledgePercent}%`;
    return { period, snapshot, alerts, trends, summary };
}
/**
 * 根据任务数量和类型粗略估算 Token 消耗。
 * @param stateDir - 任务状态文件目录路径
 * @returns 包含总估算 Token 数和按任务类型分组的估算明细
 */
export async function estimateCost(stateDir) {
    debug("可观测性", "estimateCost() 估算成本, stateDir=%s", stateDir);
    const tasks = await loadTasks(stateDir);
    const byTaskType = {};
    let total = 0;
    for (const task of tasks) {
        const taskType = task.classification?.task_type ?? "unknown";
        const perTask = TOKEN_ESTIMATES[taskType] ?? DEFAULT_TOKEN_ESTIMATE;
        byTaskType[taskType] = (byTaskType[taskType] ?? 0) + perTask;
        total += perTask;
    }
    return { total_estimated_tokens: total, by_task_type: byTaskType };
}
// ── 成本预算检查 ──
/** 从 ISO 日期字符串提取 YYYY-MM-DD 日期键 */
function dateKey(iso) {
    return iso.slice(0, 10); // YYYY-MM-DD
}
/**
 * 计算指定天数前的零点时间戳（毫秒）。
 * @param days - 回溯天数
 * @returns 对应日期零点的时间戳
 */
function daysAgoMs(days) {
    const d = new Date();
    d.setHours(0, 0, 0, 0);
    return d.getTime() - days * 24 * 60 * 60 * 1000;
}
/**
 * 检查 Token 用量是否超出预算，生成超支或接近预算（>=80%）的告警。
 * 分别检查日、周、月三个维度的预算。
 * @param stateDir - 任务状态文件目录路径
 * @param budget - 成本预算配置（含日/周/月 Token 限额）
 * @returns 成本告警列表
 */
export async function checkCostBudget(stateDir, budget) {
    debug("可观测性", "checkCostBudget() 开始执行");
    const tasks = await loadTasks(stateDir);
    const alerts = [];
    // 按任务类型估算各任务的 Token 消耗
    const dayMap = new Map();
    for (const task of tasks) {
        const taskType = task.classification?.task_type ?? "unknown";
        const perTask = TOKEN_ESTIMATES[taskType] ?? DEFAULT_TOKEN_ESTIMATE;
        const day = dateKey(task.created_at);
        dayMap.set(day, (dayMap.get(day) ?? 0) + perTask);
    }
    const today = dateKey(new Date().toISOString());
    const todayTokens = dayMap.get(today) ?? 0;
    // 周维度：最近 7 天（含今天），从 6 天前的零点开始计算
    let weeklyTokens = 0;
    for (const task of tasks) {
        const taskMs = new Date(task.created_at).getTime();
        if (taskMs >= daysAgoMs(6)) {
            const taskType = task.classification?.task_type ?? "unknown";
            weeklyTokens += TOKEN_ESTIMATES[taskType] ?? DEFAULT_TOKEN_ESTIMATE;
        }
    }
    // 月维度：最近 30 天（含今天），从 29 天前的零点开始计算
    let monthlyTokens = 0;
    for (const task of tasks) {
        const taskMs = new Date(task.created_at).getTime();
        if (taskMs >= daysAgoMs(29)) {
            const taskType = task.classification?.task_type ?? "unknown";
            monthlyTokens += TOKEN_ESTIMATES[taskType] ?? DEFAULT_TOKEN_ESTIMATE;
        }
    }
    // 检查单个时间周期的预算使用情况
    // 检查单个时间周期的预算使用情况并生成告警
    function checkPeriod(period, used, limit) {
        if (limit <= 0)
            return; // 未配置限额则跳过
        const ratio = used / limit;
        // 用量达到或超过预算 100% 时触发严重告警
        if (ratio >= 1) {
            alerts.push({
                level: "critical",
                period,
                used,
                limit,
                message: `${period === "daily" ? "今日" : period === "weekly" ? "本周" : "本月"} token 用量已超出预算（${used.toLocaleString()} / ${limit.toLocaleString()}）`,
            });
            // 用量达到预算 80% 时触发预警（提前告警）
        }
        else if (ratio >= 0.8) {
            alerts.push({
                level: "warning",
                period,
                used,
                limit,
                message: `${period === "daily" ? "今日" : period === "weekly" ? "本周" : "本月"} token 用量已达预算 ${Math.round(ratio * 100)}%（${used.toLocaleString()} / ${limit.toLocaleString()}）`,
            });
        }
    }
    checkPeriod("daily", todayTokens, budget.daily_token_limit);
    checkPeriod("weekly", weeklyTokens, budget.weekly_token_limit);
    checkPeriod("monthly", monthlyTokens, budget.monthly_token_limit);
    return alerts;
    if (alerts.length > 0) {
        debug("成本预算", "检测到 " + alerts.length + " 条预算告警");
    }
}
/**
 * 获取最近指定天数的每日 Token 用量趋势。
 * 缺失的日期填充为 0，确保时间线连续。
 * @param stateDir - 任务状态文件目录路径
 * @param days - 回溯天数，默认 30 天
 * @returns 按日期排列的每日 Token 消耗估算数组
 */
export async function getCostTrend(stateDir, days = 30) {
    debug("可观测性", "getCostTrend() 获取成本趋势, stateDir=%s, days=%d", stateDir, days);
    const tasks = await loadTasks(stateDir);
    // 构建日期 → Token 估算值的映射
    const dayMap = new Map();
    const cutoffMs = daysAgoMs(days - 1);
    for (const task of tasks) {
        const taskMs = new Date(task.created_at).getTime();
        if (taskMs < cutoffMs)
            continue;
        const taskType = task.classification?.task_type ?? "unknown";
        const perTask = TOKEN_ESTIMATES[taskType] ?? DEFAULT_TOKEN_ESTIMATE;
        const day = dateKey(task.created_at);
        dayMap.set(day, (dayMap.get(day) ?? 0) + perTask);
    }
    // 填充缺失日期为 0，保证趋势图表连续
    const result = [];
    const now = new Date();
    for (let i = days - 1; i >= 0; i--) {
        const d = new Date(now);
        d.setDate(d.getDate() - i);
        const key = dateKey(d.toISOString());
        result.push({
            date: key,
            estimated_tokens: dayMap.get(key) ?? 0,
        });
    }
    return result;
}
//# sourceMappingURL=observability.js.map