/**
 * Task Assignment Service
 *
 * This service provides task assignment functionality for the orchestration system.
 * It manages the assignment of tasks to agents with orchestrator metadata tracking
 * including branch, worktree, and session associations.
 *
 * Key features:
 * - Assign tasks to agents with orchestrator metadata
 * - Query agent workload (tasks assigned to an agent)
 * - Query unassigned tasks
 * - Query tasks by assignment status
 * - Track branch, worktree, sessionId in task metadata
 *
 * @module
 */
import { createTimestamp, TaskStatus, ElementType, asElementId } from '@stoneforge/core';
import { getOrchestratorTaskMeta, setOrchestratorTaskMeta, updateOrchestratorTaskMeta, closeTaskSessionHistory, generateBranchName, generateWorktreePath, createSlugFromTitle, } from '../types/index.js';
import { isAgentEntity, getAgentMetadata, } from '../api/orchestrator-api.js';
/**
 * All valid assignment status values
 */
export const AssignmentStatusValues = [
    'unassigned',
    'assigned',
    'in_progress',
    'completed',
    'merged',
];
// ============================================================================
// Task Assignment Service Implementation
// ============================================================================
/**
 * Implementation of the Task Assignment Service.
 *
 * This implementation uses the QuarryAPI for storage operations.
 */
export class TaskAssignmentServiceImpl {
    api;
    mergeRequestProvider;
    constructor(api, mergeRequestProvider) {
        this.api = api;
        this.mergeRequestProvider = mergeRequestProvider;
    }
    // ----------------------------------------
    // Assignment Operations
    // ----------------------------------------
    async assignToAgent(taskId, agentId, options) {
        // Get and validate the task
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        // Note: Previously there was a guard against reassignment here, but
        // the dispatch service needs to support task reassignment. The dispatch
        // layer is responsible for determining when reassignment is appropriate
        // (e.g., for handoffs). Callers who want to prevent reassignment should
        // check task.assignee before calling assignToAgent.
        // Get and validate the agent
        const agent = await this.api.get(asElementId(agentId));
        if (!agent || agent.type !== ElementType.ENTITY || !isAgentEntity(agent)) {
            throw new Error(`Agent not found: ${agentId}`);
        }
        // Generate branch and worktree names if not provided
        const slug = createSlugFromTitle(task.title);
        const branch = options?.branch ?? generateBranchName(agent.name, taskId, slug);
        const worktree = options?.worktree ?? generateWorktreePath(agent.name, slug);
        // Update task assignee
        await this.api.update(taskId, { assignee: agentId });
        // Get existing orchestrator metadata to preserve handoff history
        const existingMeta = getOrchestratorTaskMeta(task.metadata);
        // Build new orchestrator metadata, preserving handoff context if this is a reassignment
        const orchestratorMeta = {
            assignedAgent: agentId,
            branch: options?.branch ?? existingMeta?.handoffBranch ?? branch,
            worktree: options?.worktree ?? existingMeta?.handoffWorktree ?? worktree,
            sessionId: options?.sessionId,
            mergeStatus: 'pending',
            // Preserve handoff history from previous assignments
            handoffHistory: existingMeta?.handoffHistory,
        };
        // If marking as started, add start time
        if (options?.markAsStarted) {
            orchestratorMeta.startedAt = createTimestamp();
        }
        const currentMeta = task.metadata;
        const newMetadata = setOrchestratorTaskMeta(currentMeta, orchestratorMeta);
        // Update status to active if marking as started
        const updates = { metadata: newMetadata };
        if (options?.markAsStarted) {
            updates.status = TaskStatus.IN_PROGRESS;
        }
        return this.api.update(taskId, updates);
    }
    async unassignTask(taskId) {
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        // Clear assignee
        await this.api.update(taskId, { assignee: undefined });
        // Update orchestrator metadata - preserve branch info but clear agent-specific data
        const currentMeta = getOrchestratorTaskMeta(task.metadata);
        if (currentMeta) {
            const newMeta = updateOrchestratorTaskMeta(task.metadata, {
                assignedAgent: undefined,
                sessionId: undefined,
                worktree: undefined,
                startedAt: undefined,
            });
            return this.api.update(taskId, { metadata: newMeta });
        }
        return this.api.get(taskId);
    }
    async startTask(taskId, sessionId) {
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        const updates = {
            startedAt: createTimestamp(),
        };
        if (sessionId) {
            updates.sessionId = sessionId;
        }
        const newMeta = updateOrchestratorTaskMeta(task.metadata, updates);
        return this.api.update(taskId, {
            status: TaskStatus.IN_PROGRESS,
            metadata: newMeta,
        });
    }
    async completeTask(taskId, options) {
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        if (task.status === TaskStatus.CLOSED || task.status === TaskStatus.REVIEW) {
            throw new Error(`Cannot complete task ${taskId}: task is already in '${task.status}' status`);
        }
        const currentMeta = getOrchestratorTaskMeta(task.metadata);
        const branch = currentMeta?.branch;
        const currentSessionId = currentMeta?.sessionId;
        // Close the current session's history entry
        let metadataWithClosedSession = task.metadata;
        if (currentSessionId) {
            metadataWithClosedSession = closeTaskSessionHistory(metadataWithClosedSession, currentSessionId, createTimestamp());
        }
        // Build the base metadata updates
        const metaUpdates = {
            completedAt: createTimestamp(),
            mergeStatus: 'pending',
        };
        // Add optional completion info
        if (options?.summary) {
            metaUpdates.completionSummary = options.summary;
        }
        if (options?.commitHash) {
            metaUpdates.lastCommitHash = options.commitHash;
        }
        // PR creation is attempted when a provider is configured and not explicitly disabled
        let mergeRequestUrl;
        let mergeRequestId;
        if (branch && this.mergeRequestProvider && options?.createMergeRequest !== false) {
            const baseBranch = options?.baseBranch || 'main';
            let body = `## Task\n\n**ID:** ${task.id}\n**Title:** ${task.title}\n\n`;
            if (options?.summary) {
                body += `## Summary\n\n${options.summary}\n\n`;
            }
            body += `---\n_Created by Stoneforge Smithy_`;
            const mrResult = await this.mergeRequestProvider.createMergeRequest(task, {
                title: options?.mergeRequestTitle || task.title,
                body: options?.mergeRequestBody || body,
                sourceBranch: branch,
                targetBranch: baseBranch,
            });
            mergeRequestUrl = mrResult.url;
            mergeRequestId = mrResult.id;
            metaUpdates.mergeRequestUrl = mergeRequestUrl;
            metaUpdates.mergeRequestId = mergeRequestId;
            metaUpdates.mergeRequestProvider = this.mergeRequestProvider.name;
        }
        // When no provider is configured, skip PR creation but still complete the task.
        // The merge steward can create PRs later if needed.
        // Apply metadata updates on top of the closed session history
        const newMeta = updateOrchestratorTaskMeta(metadataWithClosedSession, metaUpdates);
        // Set status to REVIEW (not CLOSED) - merge steward will set CLOSED after merge
        // Clear assignee - task is now awaiting merge review, not actively being worked on
        const updatedTask = await this.api.update(taskId, {
            status: TaskStatus.REVIEW,
            assignee: undefined,
            metadata: newMeta,
        });
        return {
            task: updatedTask,
            mergeRequestUrl,
            mergeRequestId,
        };
    }
    async handoffTask(taskId, options) {
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        const currentMeta = getOrchestratorTaskMeta(task.metadata);
        const { sessionId, message, branch, worktree } = options;
        // Close the current session's history entry using the sessionId from orchestrator metadata
        // (the options.sessionId is the provider session ID, but we match on internal sessionId)
        const currentSessionId = currentMeta?.sessionId;
        let metadataWithClosedSession = task.metadata;
        if (currentSessionId) {
            metadataWithClosedSession = closeTaskSessionHistory(metadataWithClosedSession, currentSessionId, createTimestamp());
        }
        // Determine the branch and worktree to preserve
        const handoffBranch = branch || currentMeta?.branch;
        const handoffWorktree = worktree || currentMeta?.worktree;
        // Build handoff history (append to existing history)
        const existingHistory = currentMeta?.handoffHistory;
        const handoffEntry = {
            sessionId,
            message,
            branch: handoffBranch,
            worktree: handoffWorktree,
            handoffAt: createTimestamp(),
        };
        const handoffHistory = [...(existingHistory || []), handoffEntry];
        // Append handoff note to the task's description Document
        if (task.descriptionRef && message) {
            try {
                const doc = await this.api.get(asElementId(task.descriptionRef));
                if (doc) {
                    const handoffLine = `\n\n[AGENT HANDOFF NOTE]: ${message}`;
                    await this.api.update(asElementId(task.descriptionRef), {
                        content: doc.content + handoffLine,
                    });
                }
            }
            catch {
                // Non-fatal: handoff note is also preserved in handoffHistory
            }
        }
        // Update orchestrator metadata with handoff info
        const metaUpdates = {
            // Clear assignment but preserve branch/worktree for continuation
            assignedAgent: undefined,
            sessionId: undefined,
            startedAt: undefined,
            // Clear mergeStatus so merge steward doesn't pick up the task
            mergeStatus: undefined,
            // Store handoff context
            handoffBranch,
            handoffWorktree,
            lastSessionId: sessionId,
            handoffAt: createTimestamp(),
            handoffHistory,
        };
        // Apply metadata updates on top of the closed session history
        const newMeta = updateOrchestratorTaskMeta(metadataWithClosedSession, metaUpdates);
        // Update task: clear assignee, reset status to OPEN, update metadata
        // Note: We store the handoff note in metadata since tasks use descriptionRef
        // Setting status to OPEN ensures dispatch daemon can pick up the task
        return this.api.update(taskId, {
            assignee: undefined,
            status: TaskStatus.OPEN,
            metadata: newMeta,
        });
    }
    async updateSessionId(taskId, sessionId) {
        const task = await this.api.get(taskId);
        if (!task || task.type !== ElementType.TASK) {
            throw new Error(`Task not found: ${taskId}`);
        }
        const newMeta = updateOrchestratorTaskMeta(task.metadata, { sessionId });
        return this.api.update(taskId, { metadata: newMeta });
    }
    // ----------------------------------------
    // Workload Queries
    // ----------------------------------------
    async getAgentTasks(agentId, filter) {
        return this.listAssignments({
            ...filter,
            agentId,
        });
    }
    async getAgentWorkload(agentId) {
        const tasks = await this.getAgentTasks(agentId);
        const byStatus = {
            open: 0,
            in_progress: 0,
            blocked: 0,
            deferred: 0,
            backlog: 0,
            review: 0,
            closed: 0,
            tombstone: 0,
        };
        let inProgressCount = 0;
        let awaitingMergeCount = 0;
        for (const { task, orchestratorMeta } of tasks) {
            // Count by task status
            byStatus[task.status] = (byStatus[task.status] || 0) + 1;
            // Count in-progress (in_progress with start time)
            if (task.status === TaskStatus.IN_PROGRESS && orchestratorMeta?.startedAt) {
                inProgressCount++;
            }
            // Count awaiting merge (review status with pending merge status)
            if (task.status === TaskStatus.REVIEW) {
                const mergeStatus = orchestratorMeta?.mergeStatus;
                if (!mergeStatus || mergeStatus === 'pending' || mergeStatus === 'testing') {
                    awaitingMergeCount++;
                }
            }
        }
        return {
            agentId,
            totalTasks: tasks.length,
            byStatus,
            inProgressCount,
            awaitingMergeCount,
        };
    }
    async agentHasCapacity(agentId) {
        // Get the agent to check their max concurrent tasks
        const agent = await this.api.get(asElementId(agentId));
        if (!agent || !isAgentEntity(agent)) {
            return false;
        }
        const meta = getAgentMetadata(agent);
        const maxConcurrent = meta?.maxConcurrentTasks ?? 1;
        // Count currently active tasks
        const workload = await this.getAgentWorkload(agentId);
        const activeTasks = workload.byStatus.in_progress || 0;
        return activeTasks < maxConcurrent;
    }
    // ----------------------------------------
    // Task Status Queries
    // ----------------------------------------
    async getUnassignedTasks(filter) {
        const assignments = await this.listAssignments({
            ...filter,
            assignmentStatus: 'unassigned',
        });
        return assignments.map((a) => a.task);
    }
    async getTasksByAssignmentStatus(status, filter) {
        return this.listAssignments({
            ...filter,
            assignmentStatus: status,
        });
    }
    async listAssignments(filter) {
        // Build task filter from assignment filter
        const taskFilter = {
            type: 'task',
        };
        if (filter?.taskStatus) {
            taskFilter.status = filter.taskStatus;
        }
        // Push assignee filter to API level when available (M-2)
        if (filter?.agentId !== undefined) {
            taskFilter.assignee = filter.agentId;
        }
        // Get tasks (pre-filtered by assignee at API level when possible)
        const tasks = await this.api.list(taskFilter);
        // Convert to assignments and apply filters
        let assignments = tasks.map((task) => ({
            taskId: task.id,
            task,
            orchestratorMeta: getOrchestratorTaskMeta(task.metadata),
        }));
        // Apply agent filter
        if (filter?.agentId !== undefined) {
            assignments = assignments.filter((a) => {
                // Check both task.assignee and orchestrator metadata
                return (a.task.assignee === filter.agentId ||
                    a.orchestratorMeta?.assignedAgent === filter.agentId);
            });
        }
        // Apply assignment status filter
        if (filter?.assignmentStatus !== undefined) {
            const statuses = Array.isArray(filter.assignmentStatus)
                ? filter.assignmentStatus
                : [filter.assignmentStatus];
            assignments = assignments.filter((a) => {
                const assignmentStatus = this.determineAssignmentStatus(a);
                return statuses.includes(assignmentStatus);
            });
        }
        // Apply merge status filter
        if (filter?.mergeStatus !== undefined) {
            const mergeStatuses = Array.isArray(filter.mergeStatus)
                ? filter.mergeStatus
                : [filter.mergeStatus];
            assignments = assignments.filter((a) => {
                const mergeStatus = a.orchestratorMeta?.mergeStatus;
                return mergeStatus !== undefined && mergeStatuses.includes(mergeStatus);
            });
        }
        return assignments;
    }
    async getTasksAwaitingMerge() {
        return this.listAssignments({
            taskStatus: TaskStatus.REVIEW,
            mergeStatus: ['pending', 'testing'],
        });
    }
    // ----------------------------------------
    // Private Helpers
    // ----------------------------------------
    /**
     * Determines the assignment status of a task
     */
    determineAssignmentStatus(assignment) {
        const { task, orchestratorMeta } = assignment;
        // Check status-based assignment states first (regardless of assignee)
        // REVIEW status = task completed, awaiting merge review
        if (task.status === TaskStatus.REVIEW) {
            return 'completed';
        }
        // CLOSED status = either merged or completed
        if (task.status === TaskStatus.CLOSED) {
            const mergeStatus = orchestratorMeta?.mergeStatus;
            if (mergeStatus === 'merged') {
                return 'merged';
            }
            // CLOSED without 'merged' status is still 'completed'
            return 'completed';
        }
        // Check if unassigned (only for tasks not in terminal states)
        const hasAssignment = task.assignee || orchestratorMeta?.assignedAgent;
        if (!hasAssignment) {
            return 'unassigned';
        }
        // Check if actively being worked on
        if (task.status === TaskStatus.IN_PROGRESS && orchestratorMeta?.startedAt) {
            return 'in_progress';
        }
        // Assigned but not started
        return 'assigned';
    }
}
// ============================================================================
// Factory Function
// ============================================================================
/**
 * Creates a TaskAssignmentService instance
 *
 * @param api - The QuarryAPI instance
 * @param mergeRequestProvider - Optional provider for creating merge requests.
 *   When omitted, merge request creation is silently skipped.
 */
export function createTaskAssignmentService(api, mergeRequestProvider) {
    return new TaskAssignmentServiceImpl(api, mergeRequestProvider);
}
//# sourceMappingURL=task-assignment-service.js.map