/**
 * ThreadService — CRUD + lifecycle management for agent threads.
 *
 * Agent threads are parallel running agent sessions, each powered by a
 * CliProviderAdapter (jait, codex, or claude-code). Threads persist their
 * status, configuration, and activity log in SQLite via Drizzle.
 */
import { and, eq, desc, gt } from "drizzle-orm";
import { agentThreads, agentThreadActivities } from "../db/schema.js";
import { uuidv7 } from "../db/uuidv7.js";
function serializeSkillIds(skillIds) {
    if (skillIds === undefined)
        return undefined;
    if (skillIds === null)
        return null;
    const normalized = [...new Set(skillIds.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean))];
    return JSON.stringify(normalized);
}
function parseSkillIds(raw) {
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return null;
        const normalized = [...new Set(parsed.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean))];
        return normalized;
    }
    catch {
        return null;
    }
}
function parseRoutingPlan(raw) {
    if (!raw)
        return null;
    try {
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
function hydrateThreadRow(row) {
    if (!row)
        return undefined;
    return {
        ...row,
        skillIds: parseSkillIds(row.skillIds),
        routingPlan: parseRoutingPlan(row.routingPlan),
    };
}
// ── Service ──────────────────────────────────────────────────────────
export class ThreadService {
    db;
    constructor(db) {
        this.db = db;
    }
    // ── CRUD ─────────────────────────────────────────────────────────
    create(params) {
        const id = uuidv7();
        const now = new Date().toISOString();
        this.db
            .insert(agentThreads)
            .values({
            id,
            userId: params.userId ?? null,
            sessionId: params.sessionId ?? null,
            title: params.title,
            providerId: params.providerId,
            model: params.model ?? null,
            runtimeMode: params.runtimeMode ?? "full-access",
            kind: params.kind ?? "delivery",
            skillIds: serializeSkillIds(params.skillIds) ?? null,
            workingDirectory: params.workingDirectory ?? null,
            branch: params.branch ?? null,
            prBaseBranch: params.prBaseBranch ?? null,
            status: "idle",
            createdAt: now,
            updatedAt: now,
        })
            .run();
        return this.getById(id);
    }
    getById(id) {
        return hydrateThreadRow(this.db
            .select()
            .from(agentThreads)
            .where(eq(agentThreads.id, id))
            .get());
    }
    list(userId, limit) {
        const normalizedLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0
            ? Math.floor(limit)
            : undefined;
        const base = this.db.select().from(agentThreads);
        if (userId) {
            const query = base
                .where(eq(agentThreads.userId, userId))
                .orderBy(desc(agentThreads.updatedAt));
            const rows = normalizedLimit ? query.limit(normalizedLimit).all() : query.all();
            return rows.map((row) => hydrateThreadRow(row));
        }
        const query = base.orderBy(desc(agentThreads.updatedAt));
        const rows = normalizedLimit ? query.limit(normalizedLimit).all() : query.all();
        return rows.map((row) => hydrateThreadRow(row));
    }
    listBySession(sessionId, limit) {
        const normalizedLimit = typeof limit === "number" && Number.isFinite(limit) && limit > 0
            ? Math.floor(limit)
            : undefined;
        const query = this.db
            .select()
            .from(agentThreads)
            .where(eq(agentThreads.sessionId, sessionId))
            .orderBy(desc(agentThreads.updatedAt));
        const rows = normalizedLimit ? query.limit(normalizedLimit).all() : query.all();
        return rows.map((row) => hydrateThreadRow(row));
    }
    listRunning() {
        return this.db
            .select()
            .from(agentThreads)
            .where(eq(agentThreads.status, "running"))
            .orderBy(desc(agentThreads.updatedAt))
            .all()
            .map((row) => hydrateThreadRow(row));
    }
    update(id, params) {
        const now = new Date().toISOString();
        const updates = { updatedAt: now };
        if (params.title !== undefined)
            updates.title = params.title;
        if (params.providerId !== undefined)
            updates.providerId = params.providerId;
        if (params.model !== undefined)
            updates.model = params.model;
        if (params.runtimeMode !== undefined)
            updates.runtimeMode = params.runtimeMode;
        if (params.kind !== undefined)
            updates.kind = params.kind;
        if (params.skillIds !== undefined)
            updates.skillIds = serializeSkillIds(params.skillIds) ?? null;
        if (params.workingDirectory !== undefined)
            updates.workingDirectory = params.workingDirectory;
        if (params.branch !== undefined)
            updates.branch = params.branch;
        if (params.prUrl !== undefined)
            updates.prUrl = params.prUrl;
        if (params.prNumber !== undefined)
            updates.prNumber = params.prNumber;
        if (params.prTitle !== undefined)
            updates.prTitle = params.prTitle;
        if (params.prBaseBranch !== undefined)
            updates.prBaseBranch = params.prBaseBranch;
        if (params.prState !== undefined)
            updates.prState = params.prState;
        if (params.status !== undefined)
            updates.status = params.status;
        if (params.providerSessionId !== undefined)
            updates.providerSessionId = params.providerSessionId;
        if (params.error !== undefined)
            updates.error = params.error;
        if (params.completedAt !== undefined)
            updates.completedAt = params.completedAt;
        if (params.executionNodeId !== undefined)
            updates.executionNodeId = params.executionNodeId;
        if (params.executionNodeName !== undefined)
            updates.executionNodeName = params.executionNodeName;
        if (params.routingPlan !== undefined)
            updates.routingPlan = params.routingPlan ? JSON.stringify(params.routingPlan) : null;
        if (params.changeFiles !== undefined)
            updates.changeFiles = params.changeFiles;
        if (params.changeInsertions !== undefined)
            updates.changeInsertions = params.changeInsertions;
        if (params.changeDeletions !== undefined)
            updates.changeDeletions = params.changeDeletions;
        this.db
            .update(agentThreads)
            .set(updates)
            .where(eq(agentThreads.id, id))
            .run();
        return this.getById(id);
    }
    delete(id) {
        // Delete activities first, then the thread
        this.db
            .delete(agentThreadActivities)
            .where(eq(agentThreadActivities.threadId, id))
            .run();
        this.db.delete(agentThreads).where(eq(agentThreads.id, id)).run();
    }
    // ── Status transitions ──────────────────────────────────────────
    markRunning(id, providerSessionId) {
        return this.update(id, {
            status: "running",
            providerSessionId,
            error: null,
            completedAt: null,
        });
    }
    markCompleted(id) {
        return this.update(id, {
            status: "completed",
            // Keep providerSessionId alive so the thread can be resumed
            // (e.g. to fix push failures). It gets cleared on PR merge or manual close.
            error: null,
            completedAt: new Date().toISOString(),
        });
    }
    markCompletedAndClearSession(id) {
        return this.update(id, {
            status: "completed",
            providerSessionId: null,
            error: null,
            completedAt: new Date().toISOString(),
        });
    }
    /** Clear the provider session — called when a PR is merged or the thread is manually closed. */
    clearSession(id) {
        return this.update(id, {
            providerSessionId: null,
        });
    }
    markError(id, error) {
        return this.update(id, {
            status: "error",
            providerSessionId: null,
            error,
        });
    }
    markInterrupted(id) {
        return this.update(id, {
            status: "interrupted",
            providerSessionId: null,
        });
    }
    // ── Activities ──────────────────────────────────────────────────
    addActivity(threadId, kind, summary, payload) {
        const id = uuidv7();
        const now = new Date().toISOString();
        this.db
            .insert(agentThreadActivities)
            .values({
            id,
            threadId,
            kind,
            summary,
            payload: payload != null ? JSON.stringify(payload) : null,
            createdAt: now,
        })
            .run();
        return { id, threadId, kind, summary, payload, createdAt: now };
    }
    getActivities(threadId, limit, after) {
        const filters = [eq(agentThreadActivities.threadId, threadId)];
        if (after) {
            filters.push(gt(agentThreadActivities.createdAt, after));
        }
        const baseQuery = this.db
            .select()
            .from(agentThreadActivities)
            .where(filters.length === 1 ? filters[0] : and(...filters))
            .orderBy(desc(agentThreadActivities.createdAt));
        const query = typeof limit === "number" && Number.isFinite(limit) && limit > 0
            ? baseQuery.limit(limit)
            : baseQuery;
        const rows = query.all();
        return rows.map((r) => ({
            id: r.id,
            threadId: r.threadId,
            kind: r.kind,
            summary: r.summary,
            payload: r.payload ? JSON.parse(r.payload) : undefined,
            createdAt: r.createdAt,
        }));
    }
    // ── Provider event → activity log mapping ───────────────────────
    logProviderEvent(threadId, event) {
        switch (event.type) {
            case "token":
                // Don't log individual tokens — too noisy
                return undefined;
            case "turn.started":
                // Don't log turn.started — it's handled by the route for status transitions
                return undefined;
            case "tool.start":
                return this.addActivity(threadId, "tool.start", `Using ${event.tool}`, {
                    tool: event.tool,
                    args: event.args,
                    callId: event.callId,
                    parentCallId: event.parentCallId,
                });
            case "tool.result":
                return this.addActivity(threadId, event.ok ? "tool.result" : "tool.error", `${event.tool}: ${event.message}`, { tool: event.tool, ok: event.ok, message: event.message, callId: event.callId, parentCallId: event.parentCallId, data: event.data });
            case "tool.output":
                // Don't persist per-delta output — too noisy (like tokens).
                // The frontend can reconstruct final output from tool.result.
                return undefined;
            case "tool.approval-required":
                return this.addActivity(threadId, "tool.approval", `Approval required: ${event.tool}`, { tool: event.tool, args: event.args, requestId: event.requestId });
            case "message":
                // User messages are already persisted by the route handler (/start, /send).
                // Only persist assistant messages from provider events to avoid duplicates.
                if (event.role === "user")
                    return undefined;
                return this.addActivity(threadId, "message", event.content.slice(0, 500), {
                    role: event.role,
                    content: event.content,
                });
            case "session.started":
                return this.addActivity(threadId, "session", "Session started");
            case "turn.completed":
                return this.addActivity(threadId, "session", "Turn completed — ready for input");
            case "session.completed":
                return this.addActivity(threadId, "session", "Session completed");
            case "session.error":
                return this.addActivity(threadId, "error", event.error);
            case "activity":
                return this.addActivity(threadId, event.kind, event.summary, event.payload);
            default:
                return undefined;
        }
    }
}
//# sourceMappingURL=threads.js.map