import { homedir } from "node:os";
import { sep } from "node:path";
import { deriveAgentStateBucket, getWorkspaceStateBucketPriority, } from "@kong-link/protocol/agent-state-bucket";
import { getParentAgentIdFromLabels, isDelegatedAgent } from "@kong-link/protocol/agent-labels";
import { SortablePager } from "./pagination/sortable-pager.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const FETCH_WORKSPACES_SORT_KEYS = [
    "status_priority",
    "activity_at",
    "name",
    "project_id",
];
export function summarizeFetchWorkspacesEntries(entries) {
    const workspaces = Array.from(entries, (entry) => ({
        id: entry.id,
        projectId: entry.projectId,
        projectDisplayName: entry.projectDisplayName,
        name: entry.name,
        status: entry.status,
        workspaceKind: entry.workspaceKind,
        activityAt: entry.activityAt,
    }));
    const statusCounts = new Map();
    for (const workspace of workspaces) {
        statusCounts.set(workspace.status, (statusCounts.get(workspace.status) ?? 0) + 1);
    }
    return {
        count: workspaces.length,
        projectIds: [...new Set(workspaces.map((workspace) => workspace.projectId))],
        statusCounts: Object.fromEntries(statusCounts),
        workspaces,
    };
}
export class WorkspaceDirectory {
    constructor(deps) {
        this.deps = deps;
        this.archivingByWorkspaceId = new Map();
        /**
         * Per-workspace last-seen winning bucket + entered-at. Persists across
         * `buildDescriptorMap` calls inside the daemon process; reset on cold start.
         * Server-internal; never crosses the wire.
         */
        this.bucketHistoryByWorkspaceId = new Map();
        this.pager = new SortablePager({
            validKeys: FETCH_WORKSPACES_SORT_KEYS,
            defaultSort: [{ key: "activity_at", direction: "desc" }],
            label: "fetch_workspaces",
            getId: (workspace) => workspace.id,
            getSortValue: (workspace, key) => {
                switch (key) {
                    case "status_priority":
                        return getWorkspaceStateBucketPriority(workspace.status);
                    case "activity_at":
                        return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
                    case "name":
                        return workspace.name.toLocaleLowerCase();
                    case "project_id":
                        return workspace.projectId.toLocaleLowerCase();
                    default:
                        throw new Error("unreachable");
                }
            },
        });
    }
    markArchiving(workspaceIds, archivingAt) {
        for (const workspaceId of workspaceIds) {
            this.archivingByWorkspaceId.set(workspaceId, archivingAt);
        }
    }
    clearArchiving(workspaceIds) {
        for (const workspaceId of workspaceIds) {
            this.archivingByWorkspaceId.delete(workspaceId);
        }
    }
    async buildDescriptorMap(options) {
        const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
            this.deps.listAgentPayloads(),
            this.deps.workspaceRegistry.list(),
            this.deps.projectRegistry.list(),
        ]);
        const activeProjects = new Map(persistedProjects
            .filter((project) => !project.archivedAt)
            .map((project) => [project.projectId, project]));
        const archivedProjectIds = new Set(persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId));
        const activeRecords = persistedWorkspaces.filter((workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId));
        const descriptorsByWorkspaceId = new Map();
        const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
        const workspaceIdsByDirectory = new Map(activeRecords.map((workspace) => [normalizeWorkspaceId(workspace.cwd), workspace.workspaceId]));
        const includedWorkspaces = activeRecords.filter((workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId));
        const workspaceDescriptors = await Promise.all(includedWorkspaces.map((workspace) => this.deps.buildWorkspaceDescriptor({
            workspace,
            projectRecord: activeProjects.get(workspace.projectId) ?? null,
            includeGitData: options.includeGitData,
        })));
        for (let i = 0; i < includedWorkspaces.length; i += 1) {
            const workspaceId = includedWorkspaces[i].workspaceId;
            descriptorsByWorkspaceId.set(workspaceId, {
                ...workspaceDescriptors[i],
                archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
            });
        }
        const activeAgents = agents.filter((agent) => !agent.archivedAt && this.deps.isProviderVisibleToClient(agent.provider));
        const activeAgentsById = new Map(activeAgents.map((agent) => [agent.id, agent]));
        for (const agent of activeAgents) {
            let workspaceAgent = agent;
            let bucket;
            if (isDelegatedAgent(agent)) {
                if (agent.status !== "running") {
                    continue;
                }
                const parentAgent = resolveDelegationRootAgent(agent, activeAgentsById);
                if (!parentAgent) {
                    continue;
                }
                workspaceAgent = parentAgent;
                bucket = "running";
            }
            else {
                bucket = deriveAgentStateBucket({
                    status: agent.status,
                    pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
                    requiresAttention: agent.requiresAttention,
                    attentionReason: agent.attentionReason ?? null,
                });
            }
            const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(workspaceAgent.cwd));
            if (workspaceId === undefined) {
                continue;
            }
            const existing = descriptorsByWorkspaceId.get(workspaceId);
            if (!existing) {
                continue;
            }
            if (getWorkspaceStateBucketPriority(bucket) < getWorkspaceStateBucketPriority(existing.status)) {
                existing.status = bucket;
            }
        }
        // Resolve the workspace-level `statusEnteredAt` (see aggregate semantics
        // on `resolveStatusEnteredAt`).
        const nowIso = new Date().toISOString();
        for (const [workspaceId, descriptor] of descriptorsByWorkspaceId) {
            const contributingAgents = agents.filter((agent) => !agent.archivedAt &&
                this.deps.isProviderVisibleToClient(agent.provider) &&
                workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd)) === workspaceId);
            const result = this.resolveStatusEnteredAt({
                workspaceId,
                winningBucket: descriptor.status,
                contributingAgents,
                previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null,
                nowIso,
            });
            descriptor.statusEnteredAt = result.statusEnteredAt;
            if (result.recordUpdate) {
                this.bucketHistoryByWorkspaceId.set(workspaceId, result.recordUpdate);
            }
            else if (result.recordDelete) {
                this.bucketHistoryByWorkspaceId.delete(workspaceId);
            }
        }
        return descriptorsByWorkspaceId;
    }
    // Aggregate the workspace-level `statusEnteredAt` from its contributing
    // agents. Aggregate semantics:
    //   - winning bucket = highest-priority across contributing agents;
    //   - entry time = best-effort timestamp from agents in the winning bucket;
    //   - priority unmasking: when the winning bucket transitions (e.g. a
    //     higher-priority bucket cleared), the new entry time is "now";
    //   - same-bucket emits reuse the previous entered-at;
    //   - empty workspaces that never had contributing agents get
    //     `statusEnteredAt: null`.
    //   - when archived agents leave a previously active workspace empty, keep
    //     the previous done timestamp or stamp the transition to done now.
    resolveStatusEnteredAt(params) {
        const { winningBucket, contributingAgents, previous, nowIso } = params;
        if (contributingAgents.length === 0) {
            if (!previous) {
                return { statusEnteredAt: null };
            }
            const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;
            return {
                statusEnteredAt: enteredAt,
                recordUpdate: { bucket: "done", enteredAt },
            };
        }
        if (!previous) {
            const newestInWinningBucket = this.findNewestAgentTimestampInBucket(contributingAgents, winningBucket);
            const enteredAt = newestInWinningBucket ?? nowIso;
            return {
                statusEnteredAt: enteredAt,
                recordUpdate: { bucket: winningBucket, enteredAt },
            };
        }
        if (previous.bucket !== winningBucket) {
            return {
                statusEnteredAt: nowIso,
                recordUpdate: { bucket: winningBucket, enteredAt: nowIso },
            };
        }
        return {
            statusEnteredAt: previous.enteredAt,
            recordUpdate: previous,
        };
    }
    // Best-effort newest timestamp across contributing agents whose derived
    // bucket matches `winningBucket`. Uses available agent fields:
    //   - `attentionTimestamp` when attention is set (covers attention/failed)
    //   - `updatedAt` as a general fallback for any bucket
    // Returns `null` if no matching agent has a parseable timestamp.
    findNewestAgentTimestampInBucket(contributingAgents, winningBucket) {
        const candidates = contributingAgents
            .filter((agent) => {
            const derived = deriveAgentStateBucket({
                status: agent.status,
                pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
                requiresAttention: agent.requiresAttention,
                attentionReason: agent.attentionReason ?? null,
            });
            return derived === winningBucket;
        })
            .map((agent) => {
            // Prefer attentionTimestamp when the agent has attention set — this is
            // the most accurate "entered current status" signal.
            if (agent.attentionTimestamp) {
                return agent.attentionTimestamp;
            }
            // Fall back to updatedAt as a general proxy for recent activity.
            return agent.updatedAt;
        })
            .filter((value) => typeof value === "string" && value.length > 0)
            .sort();
        return candidates.at(-1) ?? null;
    }
    resolveRegisteredWorkspaceIdForCwd(cwd, workspaces) {
        const normalizedCwd = normalizeWorkspaceId(cwd);
        const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
        if (exact) {
            return exact.workspaceId;
        }
        const userHome = homedir();
        let bestMatch = null;
        for (const workspace of workspaces) {
            if (workspace.cwd === userHome)
                continue;
            if (workspace.archivedAt)
                continue;
            const prefix = workspace.cwd.endsWith(sep) ? workspace.cwd : `${workspace.cwd}${sep}`;
            if (!normalizedCwd.startsWith(prefix)) {
                continue;
            }
            if (!bestMatch || workspace.cwd.length > bestMatch.cwd.length) {
                bestMatch = workspace;
            }
        }
        return bestMatch?.workspaceId ?? normalizedCwd;
    }
    async listDescriptors() {
        return Array.from((await this.buildDescriptorMap({
            includeGitData: true,
        })).values());
    }
    matchesFilter(input) {
        const { workspace, filter } = input;
        if (!filter) {
            return true;
        }
        if (filter.projectId && filter.projectId.trim().length > 0) {
            if (workspace.projectId !== filter.projectId.trim()) {
                return false;
            }
        }
        if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
            if (!workspace.id.startsWith(filter.idPrefix.trim())) {
                return false;
            }
        }
        if (filter.query && filter.query.trim().length > 0) {
            const query = filter.query.trim().toLocaleLowerCase();
            const haystacks = [workspace.name, workspace.projectId, workspace.id];
            if (!haystacks.some((value) => value.toLocaleLowerCase().includes(query))) {
                return false;
            }
        }
        return true;
    }
    async listFetchEntries(request) {
        const filter = request.filter;
        const sort = this.pager.normalizeSort(request.sort);
        let entries = await this.listDescriptors();
        const listedCount = entries.length;
        entries = entries.filter((workspace) => this.matchesFilter({ workspace, filter }));
        const filteredCount = entries.length;
        entries.sort((left, right) => this.pager.compare(left, right, sort));
        const cursorToken = request.page?.cursor;
        if (cursorToken) {
            const cursor = this.pager.decode(cursorToken, sort);
            entries = entries.filter((workspace) => this.pager.compareWithCursor(workspace, cursor, sort) > 0);
        }
        const limit = request.page?.limit ?? 200;
        const pagedEntries = entries.slice(0, limit);
        const hasMore = entries.length > limit;
        const nextCursor = hasMore && pagedEntries.length > 0
            ? this.pager.encode(pagedEntries[pagedEntries.length - 1], sort)
            : null;
        this.deps.logger.debug({
            requestId: request.requestId,
            filter: request.filter ?? null,
            sort,
            page: request.page ?? null,
            listedCount,
            filteredCount,
            returnedCount: pagedEntries.length,
            hasMore,
            nextCursor,
        }, "fetch_workspaces_entries_listed");
        return {
            entries: pagedEntries,
            pageInfo: {
                nextCursor,
                prevCursor: request.page?.cursor ?? null,
                hasMore,
            },
        };
    }
}
function resolveDelegationRootAgent(agent, activeAgentsById) {
    const seen = new Set([agent.id]);
    let current = agent;
    while (true) {
        const parentAgentId = getParentAgentIdFromLabels(current.labels);
        if (!parentAgentId) {
            return current;
        }
        if (seen.has(parentAgentId)) {
            return null;
        }
        const parent = activeAgentsById.get(parentAgentId);
        if (!parent) {
            return null;
        }
        seen.add(parentAgentId);
        current = parent;
    }
}
//# sourceMappingURL=workspace-directory.js.map