/**
 * Skill layer endpoints.
 *
 * Skills are the callable top of the memory stack (Phase 11). The
 * viewer uses these to render the skill library and to retire stale
 * skills when a user clicks "archive".
 */
import { buildSingleFileZip } from "../../core/util/tiny-zip.js";
import { parseJson, writeError } from "./registry.js";
export function registerSkillRoutes(routes, deps) {
    routes.set("GET /api/v1/skills", async (ctx) => {
        const params = ctx.url.searchParams;
        const status = params.get("status") ?? undefined;
        const q = (params.get("q") || "").trim().toLowerCase();
        const ownerAgentKind = params.get("ownerAgentKind") || undefined;
        const ownerProfileId = params.get("ownerProfileId") || undefined;
        // Viewer needs prev/next pagination — ask for one extra page so we
        // can tell the client whether there's more without a count query.
        const pageSize = limitOrUndefined(params.get("limit")) ?? 50;
        const offset = Math.max(0, Number(params.get("offset") ?? 0) || 0);
        let all = await deps.core.listSkills({
            status,
            limit: q ? 5000 : pageSize + offset + 1,
            ownerAgentKind,
            ownerProfileId,
        });
        if (q) {
            all = all.filter((s) => s.name.toLowerCase().includes(q) || s.invocationGuide.toLowerCase().includes(q));
        }
        const page = all.slice(offset, offset + pageSize);
        const hasMore = all.length > offset + pageSize;
        const total = q ? all.length : await deps.core.countSkills({
            status,
            ownerAgentKind,
            ownerProfileId,
        });
        return {
            skills: page,
            limit: pageSize,
            offset,
            total,
            nextOffset: hasMore ? offset + pageSize : undefined,
        };
    });
    routes.set("GET /api/v1/skills/get", async (ctx) => {
        const id = ctx.url.searchParams.get("id");
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const skill = await deps.core.getSkill(id);
        if (skill === null) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        return skill;
    });
    routes.setPattern("GET /api/v1/skills/:id", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const skill = await deps.core.getSkill(id);
        if (skill === null) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        return skill;
    });
    /**
     * `GET /api/v1/skills/:id/timeline` — evolution history for one skill.
     *
     * We assemble the timeline by walking the recent `api_logs` rows for
     * the `skill_generate` / `skill_evolve` tool names and keeping those
     * whose payload mentions this skill id. Mirrors the legacy
     * `memos-local-openclaw` "version history" table shown in the Skills
     * drawer.
     */
    routes.setPattern("GET /api/v1/skills/:id/timeline", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const picks = await Promise.all(["skill_generate", "skill_evolve"].map((tool) => deps.core.listApiLogs({ toolName: tool, limit: 500, offset: 0 })));
        const entries = [];
        for (const res of picks) {
            for (const row of res.logs) {
                const out = safeJson(row.outputJson);
                const inp = safeJson(row.inputJson);
                const skillId = out?.skillId ??
                    inp?.skillId;
                if (skillId !== id)
                    continue;
                const kind = out?.kind ??
                    row.toolName;
                const phase = inp?.phase;
                const summary = out?.name ??
                    out?.reason;
                entries.push({
                    ts: row.calledAt,
                    kind,
                    phase,
                    durationMs: row.durationMs,
                    success: row.success,
                    summary,
                });
            }
        }
        entries.sort((a, b) => b.ts - a.ts);
        return { skillId: id, entries };
    });
    /**
     * `GET /api/v1/skills/:id/usage` — cross-link payload for the skill
     * drawer. Resolves `sourcePolicyIds` / `sourceWorldModelIds` to
     * {id, name/title} pairs so the viewer can render clickable chips
     * without fetching every policy / world-model.
     */
    routes.setPattern("GET /api/v1/skills/:id/usage", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const skill = await deps.core.getSkill(id);
        if (!skill) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        const sourcePolicies = await Promise.all(skill.sourcePolicyIds.map(async (pid) => {
            const p = await deps.core.getPolicy(pid);
            return p
                ? { id: p.id, title: p.title, status: p.status, gain: p.gain }
                : { id: pid, title: null, status: null, gain: null };
        }));
        const sourceWorldModels = await Promise.all(skill.sourceWorldModelIds.map(async (wid) => {
            const w = await deps.core.getWorldModel(wid);
            return w ? { id: w.id, title: w.title } : { id: wid, title: null };
        }));
        return {
            sourcePolicies,
            sourceWorldModels,
        };
    });
    routes.set("POST /api/v1/skills/archive", async (ctx) => {
        // Accept either `{id}` (JSON-RPC flavour) or `{skillId}` (viewer).
        const body = parseJson(ctx);
        const target = body.id ?? body.skillId;
        if (!target) {
            writeError(ctx, 400, "invalid_argument", "id or skillId is required");
            return;
        }
        await deps.core.archiveSkill(target, body.reason);
        return { ok: true };
    });
    /**
     * `POST /api/v1/skills/reactivate` — flip an archived skill back to
     * active. Accepts either `{id}` or `{skillId}` to match the viewer's
     * existing archive flow.
     */
    routes.set("POST /api/v1/skills/reactivate", async (ctx) => {
        const body = parseJson(ctx);
        const target = body.id ?? body.skillId;
        if (!target) {
            writeError(ctx, 400, "invalid_argument", "id or skillId is required");
            return;
        }
        const updated = await deps.core.reactivateSkill(target);
        if (!updated) {
            writeError(ctx, 404, "not_found", `skill not found: ${target}`);
            return;
        }
        return updated;
    });
    /**
     * `DELETE /api/v1/skills/:id` — hard delete (vs `archive` which just
     * flips status). Idempotent.
     */
    routes.setPattern("DELETE /api/v1/skills/:id", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        return await deps.core.deleteSkill(id);
    });
    /**
     * `PATCH /api/v1/skills/:id` — viewer's edit modal. Mutable fields:
     * `name`, `invocationGuide`. Returns the updated DTO.
     */
    routes.setPattern("PATCH /api/v1/skills/:id", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const body = parseJson(ctx);
        const updated = await deps.core.updateSkill(id, {
            name: typeof body.name === "string" ? body.name : undefined,
            invocationGuide: typeof body.invocationGuide === "string" ? body.invocationGuide : undefined,
        });
        if (!updated) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        return updated;
    });
    /**
     * `POST /api/v1/skills/:id/share` — set/clear the share state.
     * Same body shape as `POST /api/v1/traces/:id/share`.
     */
    routes.setPattern("POST /api/v1/skills/:id/share", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const body = parseJson(ctx);
        const scope = body.scope === undefined ? "public" : body.scope;
        const updated = await deps.core.shareSkill(id, {
            scope: scope ?? null,
            target: body.target ?? null,
            sharedAt: scope ? Date.now() : null,
        });
        if (!updated) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        return updated;
    });
    /**
     * `GET /api/v1/skills/:id/download` — bundle the skill's invocation
     * guide as a one-file ZIP (`SKILL.md`). Lets the user grab the
     * guide for sharing into another agent or backing up locally.
     */
    routes.setPattern("GET /api/v1/skills/:id/download", async (ctx) => {
        const id = ctx.params.id;
        if (!id) {
            writeError(ctx, 400, "invalid_argument", "id is required");
            return;
        }
        const skill = await deps.core.getSkill(id);
        if (!skill) {
            writeError(ctx, 404, "not_found", `skill not found: ${id}`);
            return;
        }
        const md = renderSkillMarkdown(skill);
        const packageName = sanitizeFilename(skill.name || skill.id);
        const buf = buildSingleFileZip(`${packageName}/SKILL.md`, md);
        const filename = packageName + ".zip";
        ctx.res.writeHead(200, {
            "content-type": "application/zip",
            "content-length": String(buf.length),
            "content-disposition": `attachment; filename="${filename}"`,
        });
        ctx.res.end(buf);
    });
}
function renderSkillMarkdown(skill) {
    const name = skill.name || skill.id;
    const description = skillDescription(skill.invocationGuide, name);
    const frontmatter = `---\n` +
        `name: ${yamlString(name)}\n` +
        `description: ${yamlString(description)}\n` +
        `---\n\n`;
    const head = `# ${name}\n\n`;
    const meta = `> id: ${skill.id}\n> status: ${skill.status}\n> version: ${skill.version}\n\n`;
    const guide = skill.invocationGuide?.trim() || "(no invocation guide)";
    return frontmatter + head + meta + guide + "\n";
}
function skillDescription(invocationGuide, name) {
    const fallback = `Use ${name} when this learned skill matches the current task.`;
    const lines = (invocationGuide ?? "")
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean);
    const firstBodyLine = lines.find((line) => !line.startsWith("#") && !line.startsWith(">")) ?? fallback;
    return firstBodyLine.replace(/\s+/g, " ").slice(0, 240);
}
function yamlString(raw) {
    return JSON.stringify(raw.replace(/\s+/g, " ").trim());
}
function sanitizeFilename(raw) {
    const trimmed = raw.trim();
    const safe = trimmed.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
    return safe.length > 0 ? safe.slice(0, 80) : "skill";
}
function limitOrUndefined(raw) {
    if (raw === null)
        return undefined;
    const n = Number(raw);
    return Number.isFinite(n) && n > 0 ? Math.min(n, 500) : undefined;
}
function safeJson(text) {
    if (!text)
        return null;
    try {
        return JSON.parse(text);
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=skill.js.map