/**
 * @module        services/WorkerService
 * @description   Worker 查询 + 控制 service
 *
 * @layer         services
 *
 * 职责：
 *   - 读 runtime/worker-worker-<N>-current.json marker → 结构化 WorkerInfo
 *   - 列项目 workers / 跨项目聚合
 *   - launch / kill —— Phase 2 暂不负责真 spawn，返 pending port（Phase 3 填）
 *
 * 不负责：
 *   - marker 写入（worker-manager 的事）
 *   - 日志 tail（LogService）
 */
import { resolve } from 'node:path';
import { domainError } from '../shared/errors.js';
import { err, ok } from '../shared/result.js';
import { projectDir, projectsDir, runtimeDir, slotFromMarkerFilename, stateFile, WorkerMarkerFilenameRe, workerMarkerFile, } from '../shared/runtimePaths.js';
import { WorkerMarkerSchema } from '../shared/runtimeSchemas.js';
/**
 * 读 state.json 里某 slot 的 status + seq。权威的"worker 是否在跑"信号。
 * 文件不存在 / 解析失败 → 返 null，WorkerService 回落到只看 marker。
 */
function readSlotStateJson(fs, project, slotName) {
    const path = stateFile(project);
    if (!fs.exists(path))
        return null;
    try {
        const raw = fs.readFile(path);
        const parsed = JSON.parse(raw);
        const w = parsed.workers?.[slotName];
        if (!w)
            return null;
        return {
            status: String(w.status ?? 'idle'),
            seq: w.seq ?? null,
            lastHeartbeat: w.lastHeartbeat ?? null,
            runStatus: parsed.sessions?.[slotName]?.currentRun?.status ?? null,
        };
    }
    catch {
        return null;
    }
}
const STUCK_THRESHOLD_MS = 5 * 60 * 1000;
const ACK_TIMEOUT_MS = 60 * 1000;
export class WorkerService {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    /** 列单项目的 worker slots（按 slot 数字升序）。 */
    async listByProject(project) {
        if (!isValidProject(project))
            return err(invalidProject());
        const dir = runtimeDir(project);
        if (!this.deps.fs.exists(dir))
            return ok([]);
        const slots = this.listMarkerSlots(project);
        const infos = [];
        for (const { slot, file } of slots) {
            infos.push(await this.buildInfo(project, slot, file));
        }
        return ok(infos.sort((a, b) => a.slot - b.slot));
    }
    /** 单 slot detail。 */
    async getBySlot(project, slot) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!Number.isInteger(slot) || slot <= 0) {
            return err(domainError('validation', 'INVALID_SLOT', 'slot must be a positive integer'));
        }
        const candidates = this.resolveMarkerPath(project, slot);
        if (!candidates) {
            return err(domainError('not-found', 'WORKER_MARKER_NOT_FOUND', `worker-${slot} marker not found`));
        }
        return ok(await this.buildInfo(project, slot, candidates));
    }
    /** 跨项目聚合 —— Workers 页用。 */
    async aggregate() {
        const root = projectsDir();
        if (!this.deps.fs.exists(root)) {
            return ok({ alerts: [], active: [], capacity: [] });
        }
        let projects;
        try {
            projects = this.deps.fs.readDir(root).filter((e) => e.isDirectory).map((e) => e.name);
        }
        catch (cause) {
            return err(domainError('internal', 'PROJECTS_READ_FAIL', 'Failed to read projects directory', { cause }));
        }
        projects.sort();
        const result = { alerts: [], active: [], capacity: [] };
        for (const project of projects) {
            const slots = this.listMarkerSlots(project);
            const stats = {
                project,
                total: slots.length,
                running: 0,
                starting: 0,
                stuck: 0,
                crashed: 0,
                idle: 0,
            };
            for (const { slot, file } of slots) {
                const info = await this.buildInfo(project, slot, file);
                stats[info.state]++;
                if (info.state === 'stuck' || info.state === 'crashed') {
                    result.alerts.push({ ...info, project });
                }
                else if (info.state === 'running' || info.state === 'starting') {
                    result.active.push({ ...info, project });
                }
            }
            result.capacity.push(stats);
        }
        result.alerts.sort((a, b) => {
            if (a.state !== b.state)
                return a.state === 'stuck' ? -1 : 1;
            return (b.runtimeMs ?? 0) - (a.runtimeMs ?? 0);
        });
        result.active.sort((a, b) => (b.runtimeMs ?? 0) - (a.runtimeMs ?? 0));
        return ok(result);
    }
    /** launch 转发到注入的 executor。未注入返 internal。 */
    async launch(project, seq) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!Number.isInteger(seq) || seq <= 0) {
            return err(domainError('validation', 'INVALID_SEQ', 'seq must be a positive integer'));
        }
        if (!this.deps.executor) {
            return err(domainError('internal', 'WORKER_EXECUTOR_MISSING', 'WorkerExecutor must be injected (Phase 3 task)'));
        }
        if (!this.deps.fs.exists(projectDir(project))) {
            return err(domainError('not-found', 'PROJECT_NOT_FOUND', `Project ${project} not found`));
        }
        try {
            await this.deps.executor.launch(project, seq);
        }
        catch (cause) {
            return err(domainError('external', 'WORKER_LAUNCH_FAIL', 'worker launch failed', {
                cause,
                details: { message: cause instanceof Error ? cause.message : String(cause) },
            }));
        }
        return ok(undefined);
    }
    async kill(project, slot) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!Number.isInteger(slot) || slot <= 0) {
            return err(domainError('validation', 'INVALID_SLOT', 'slot must be a positive integer'));
        }
        if (!this.deps.executor) {
            return err(domainError('internal', 'WORKER_EXECUTOR_MISSING', 'WorkerExecutor not injected'));
        }
        try {
            await this.deps.executor.kill(project, slot);
        }
        catch (cause) {
            return err(domainError('external', 'WORKER_KILL_FAIL', 'worker kill failed', {
                cause,
                details: { message: cause instanceof Error ? cause.message : String(cause) },
            }));
        }
        return ok(undefined);
    }
    // ─── 内部 ─────────────────────────────────────────────────────────
    /** 扫 runtime/ 下 marker 文件，匹配双前缀 + 单前缀，返 slot 数字 + 实际路径 */
    listMarkerSlots(project) {
        const dir = runtimeDir(project);
        if (!this.deps.fs.exists(dir))
            return [];
        try {
            return this.deps.fs
                .readDir(dir)
                .filter((e) => e.isFile && WorkerMarkerFilenameRe.test(e.name))
                .map((e) => {
                const slot = slotFromMarkerFilename(e.name);
                return { slot: slot ?? 0, file: resolve(dir, e.name) };
            })
                .filter((x) => x.slot > 0);
        }
        catch {
            return [];
        }
    }
    /** 优先双前缀，再试老单前缀 */
    resolveMarkerPath(project, slot) {
        const doublePrefix = workerMarkerFile(project, `worker-${slot}`);
        if (this.deps.fs.exists(doublePrefix))
            return doublePrefix;
        // 老兼容：worker-<N>-current.json（单前缀）
        const single = resolve(runtimeDir(project), `worker-${slot}-current.json`);
        if (this.deps.fs.exists(single))
            return single;
        return null;
    }
    async buildInfo(project, slot, markerPath) {
        const now = this.deps.clock.now();
        let pid = null;
        let card = null;
        let stage = null;
        let startedAt = null;
        let markerUpdatedAt = null;
        try {
            const raw = this.deps.fs.readFile(markerPath);
            const parsed = WorkerMarkerSchema.safeParse(JSON.parse(raw));
            if (parsed.success) {
                const d = parsed.data;
                pid = d.pid ?? null;
                stage = d.stage;
                startedAt = d.dispatchedAt;
                const seq = parseSeqFromCardId(d.cardId);
                if (seq !== null) {
                    let title = `#${seq}`;
                    if (this.deps.cardTitleLookup) {
                        try {
                            const t = await this.deps.cardTitleLookup(project, seq);
                            if (t)
                                title = t;
                        }
                        catch {
                            /* lookup 失败用 fallback */
                        }
                    }
                    card = { seq, title };
                }
            }
        }
        catch {
            /* marker 读不了就按最小信息返 */
        }
        const stat = this.deps.fs.stat(markerPath);
        if (stat)
            markerUpdatedAt = new Date(stat.mtimeMs).toISOString();
        const alive = isPidAlive(pid);
        const ageMs = markerUpdatedAt ? now - new Date(markerUpdatedAt).getTime() : null;
        // v0.50.5：state.json 是"slot 是否在跑"的权威源。worker 完成后 supervisor 会把
        // slot.status 置 idle + seq=null，但 marker 文件不清（hook 还要靠它反查 current-card）。
        // 仅看 marker 会把已完成的 slot 当 running → 5 分钟后 stuck。这里先读 state.json：
        //   - slot.status === 'idle' → 报 idle，卡片信息清空（marker 残留忽略）
        //   - 否则保留原来的"marker 导向"状态机
        const slotName = `worker-${slot}`;
        const slotJson = readSlotStateJson(this.deps.fs, project, slotName);
        const slotIsIdle = slotJson?.status === 'idle' || slotJson?.seq === null;
        // v0.53.4: liveness 不再用 marker 文件 mtime 判定。marker 是派发时写一次、运行期间
        // 永不更新的,任何运行 >STUCK_THRESHOLD_MS(5min)的健康 worker 都会被误判 stuck —
        // 长任务(30min+)/ 长工具调用(vitest/build)下几乎必误报。改用 tick 持续刷新的真实
        // 信号:ACP run 仍是 'running',或 worker.lastHeartbeat 新鲜(tick 每 ~30s 刷新)。
        // 这样长任务不误报,只有 tick 停更(进程/调度真出问题)才算 stuck。
        const liveFresh = slotJson?.runStatus === 'running' ||
            (slotJson?.lastHeartbeat != null &&
                now - new Date(slotJson.lastHeartbeat).getTime() < STUCK_THRESHOLD_MS);
        let state;
        if (slotIsIdle) {
            state = 'idle';
        }
        else if (!alive) {
            state = card !== null ? 'crashed' : 'idle';
        }
        else if (card === null) {
            state = 'idle';
        }
        else {
            const starting = ageMs !== null && ageMs <= ACK_TIMEOUT_MS;
            if (starting)
                state = 'starting';
            else if (!liveFresh)
                state = 'stuck';
            else
                state = 'running';
        }
        // idle 状态下不再展示卡片（前端 "active 列表" 会把 idle 过滤掉；但 detail 页也不该还显示上次的卡）
        const cardOut = state === 'idle' ? null : card;
        const runtimeMs = startedAt && state !== 'idle' ? now - new Date(startedAt).getTime() : null;
        return {
            slot,
            pid,
            state,
            card: cardOut,
            stage: state === 'idle' ? null : stage,
            startedAt: state === 'idle' ? null : startedAt,
            runtimeMs,
            markerUpdatedAt,
        };
    }
}
// ─── Helpers ──────────────────────────────────────────────────────
function parseSeqFromCardId(cardId) {
    const m = cardId.match(/^(?:md-)?(\d+)$/);
    if (!m)
        return null;
    const n = Number.parseInt(m[1] ?? '', 10);
    return Number.isFinite(n) && n > 0 ? n : null;
}
function isPidAlive(pid) {
    if (!pid)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function isValidProject(project) {
    return typeof project === 'string' && /^[a-zA-Z0-9_-]+$/.test(project);
}
function invalidProject() {
    return domainError('validation', 'INVALID_PROJECT_NAME', 'Invalid project name');
}
//# sourceMappingURL=WorkerService.js.map