/**
 * Chat Controller
 *
 * HTTP request handlers for chat functionality. Provides endpoints for
 * sending messages, managing conversations, and retrieving chat history.
 *
 * @module controllers/chat/chat.controller
 */
import { getChatService, MessageValidationError, ConversationNotFoundError, } from '../../services/chat/chat.service.js';
// Phase 6c — chat.controller.ts still imports the legacy ChatService
// façade. Behaviour-neutral: the façade is a thin shim over
// ChatV2Service (single storage, single write primitive). The 14
// call sites below will be replaced one endpoint at a time in a
// follow-up cleanup PR; doing so does not change runtime
// architecture, only the import surface.
import { sanitizeMessages, sanitizeMessage } from '../../services/chat/chat-sanitizer.service.js';
import { getChatHighlightsService } from '../../services/chat/chat-highlights.service.js';
import { ORCHESTRATOR_SESSION_NAME } from '../../constants.js';
import { getSessionBackendSync } from '../../services/session/session-backend.factory.js';
import { LoggerService } from '../../services/core/logger.service.js';
import { isValidChannelType } from '../../types/chat.types.js';
// Module-level message queue service instance
let messageQueueService = null;
// Module-level thread status queue service instance
let threadStatusQueueService = null;
// Logger instance for chat controller
const logger = LoggerService.getInstance().createComponentLogger('ChatController');
/**
 * Set the message queue service for enqueuing messages to the orchestrator.
 * Called during server initialization.
 *
 * @param service - The MessageQueueService instance
 */
export function setMessageQueueService(service) {
    messageQueueService = service;
}
/**
 * Set the thread status queue service for thread status API endpoints.
 * Called during server initialization.
 *
 * @param service - The ThreadStatusQueueService instance
 */
export function setThreadStatusQueueService(service) {
    threadStatusQueueService = service;
}
// =============================================================================
// Message Endpoints
// =============================================================================
/**
 * POST /api/chat/send
 *
 * Send a message to the orchestrator. Creates a new conversation if
 * conversationId is not provided.
 *
 * @param req - Request with body: { content: string, conversationId?: string, metadata?: object }
 * @param res - Response with sent message and conversation
 */
export async function sendMessage(req, res, next) {
    try {
        const { content, conversationId, metadata, forwardToOrchestrator: shouldForward = true } = req.body;
        if (!content || (typeof content === 'string' && content.trim().length === 0)) {
            res.status(400).json({
                success: false,
                error: 'Message content is required',
            });
            return;
        }
        const input = {
            content,
            conversationId,
            metadata,
        };
        const chatService = getChatService();
        const result = await chatService.sendMessage(input);
        // V3 Request creation — fire-and-forget, never blocks the response
        setImmediate(async () => {
            try {
                const { RequestService } = await import('../../services/v3/request.service.js');
                const requestSvc = RequestService.getInstance();
                // Deduplicate: one Request per conversation message
                const existing = await requestSvc.findBySourceConversationItemId(result.message.id);
                if (!existing) {
                    const { generateRequestTitle, classifyIntent } = await import('../../services/v3/v3-data.service.js');
                    const { intentLevel, intentCategory } = classifyIntent(content);
                    const request = await requestSvc.create({
                        title: generateRequestTitle(content, intentCategory),
                        description: content,
                        sourceConversationItemId: result.message.id,
                        priority: 'normal',
                        tags: ['chat-ui'],
                        intentLevel,
                        intentCategory,
                    });
                    // P2-2: RequestTracker.setActiveRequest write removed. The
                    // companion read in v3-data.service.ts no longer falls back to
                    // time-window correlation, so writing here would be dead code.
                    logger.debug('V3 Request created from chat message', { messageId: result.message.id, requestId: request.id });
                }
            }
            catch (err) {
                logger.warn('V3 Request creation failed (non-critical)', {
                    error: err instanceof Error ? err.message : String(err),
                });
            }
        });
        // Enqueue message for orchestrator processing if enabled (default: true)
        let orchestratorStatus = { forwarded: false };
        if (shouldForward) {
            const backend = getSessionBackendSync();
            const sessionExists = backend?.sessionExists(ORCHESTRATOR_SESSION_NAME) ?? false;
            if (!sessionExists && !messageQueueService) {
                // #247: Only fail if both orchestrator is down AND queue is unavailable.
                // If the queue is available, enqueue the message for replay when
                // the orchestrator comes back online.
                orchestratorStatus = {
                    forwarded: false,
                    error: 'Orchestrator is not running. Please start the orchestrator first.',
                };
            }
            else if (!sessionExists && messageQueueService) {
                // #247: Orchestrator is offline but queue is available — queue for later delivery.
                // The queue processor defers delivery until the orchestrator registers as active.
                try {
                    const queued = messageQueueService.enqueue({
                        content,
                        conversationId: result.conversation.id,
                        source: 'web_chat',
                    });
                    orchestratorStatus = {
                        forwarded: true,
                        queued: true,
                        queueId: queued.id,
                        error: 'Orchestrator is currently offline. Message queued for delivery when it comes back online.',
                    };
                    logger.info('Message queued for offline orchestrator (#247)', {
                        conversationId: result.conversation.id,
                        queueId: queued.id,
                    });
                }
                catch (enqueueErr) {
                    orchestratorStatus = {
                        forwarded: false,
                        error: `Orchestrator offline and queue failed: ${enqueueErr instanceof Error ? enqueueErr.message : 'Unknown error'}`,
                    };
                }
            }
            else if (!messageQueueService) {
                orchestratorStatus = {
                    forwarded: false,
                    error: 'Message queue service not initialized',
                };
            }
            else {
                try {
                    const queued = messageQueueService.enqueue({
                        content,
                        conversationId: result.conversation.id,
                        source: 'web_chat',
                    });
                    orchestratorStatus = { forwarded: true, queued: true, queueId: queued.id };
                }
                catch (enqueueErr) {
                    logger.warn('Failed to enqueue message', {
                        error: enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr),
                        conversationId: result.conversation.id,
                    });
                    orchestratorStatus = {
                        forwarded: false,
                        error: enqueueErr instanceof Error ? enqueueErr.message : 'Failed to enqueue message',
                    };
                }
            }
        }
        res.status(201).json({
            success: true,
            data: {
                ...result,
                orchestrator: orchestratorStatus,
            },
        });
    }
    catch (error) {
        if (error instanceof MessageValidationError) {
            res.status(400).json({
                success: false,
                error: error.message,
            });
            return;
        }
        next(error);
    }
}
/**
 * GET /api/chat/messages
 *
 * Get messages for a conversation with optional filtering.
 *
 * @param req - Request with query params for filtering
 * @param res - Response with array of messages
 */
export async function getMessages(req, res, next) {
    try {
        const { conversationId, senderType, contentType, after, before, limit, offset } = req.query;
        if (!conversationId) {
            res.status(400).json({
                success: false,
                error: 'conversationId is required',
            });
            return;
        }
        const filter = {
            conversationId: conversationId,
            senderType: senderType,
            contentType: contentType,
            after: after,
            before: before,
            limit: limit ? parseInt(limit, 10) : undefined,
            offset: offset ? parseInt(offset, 10) : undefined,
        };
        const chatService = getChatService();
        const [messages, totalCount] = await Promise.all([
            chatService.getMessages(filter),
            chatService.getMessageCount(filter),
        ]);
        // hasMore is true when there are older messages not included in this response.
        // For default loads (no offset), the returned messages are the newest tail,
        // so hasMore = totalCount > messages.length. For explicit offset loads,
        // hasMore = offset > 0 (there are messages before the offset window).
        const hasMore = offset
            ? parseInt(offset, 10) > 0
            : totalCount > messages.length;
        res.json({
            success: true,
            data: sanitizeMessages(messages),
            count: messages.length,
            totalCount,
            hasMore,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * GET /api/chat/messages/:conversationId/:messageId
 *
 * Get a single message by ID.
 *
 * @param req - Request with conversationId and messageId params
 * @param res - Response with the message
 */
export async function getMessage(req, res, next) {
    try {
        const { conversationId, messageId } = req.params;
        const chatService = getChatService();
        const message = await chatService.getMessage(conversationId, messageId);
        if (!message) {
            res.status(404).json({
                success: false,
                error: 'Message not found',
            });
            return;
        }
        res.json({
            success: true,
            data: sanitizeMessage(message),
        });
    }
    catch (error) {
        next(error);
    }
}
// =============================================================================
// Agent Response Endpoint
// =============================================================================
/**
 * POST /api/chat/agent-response
 *
 * Store an agent's response message in a chat conversation. Used by
 * orchestrator bash skills to post agent responses directly to the
 * chat without going through terminal output parsing.
 *
 * @param req - Request with body: { content, senderName, senderType?, conversationId? }
 * @param res - Response with { success, data: { messageId, conversationId } }
 * @param next - Express next function for error propagation
 *
 * @example
 * ```
 * POST /api/chat/agent-response
 * {
 *   "content": "Task completed successfully. The API endpoint is live.",
 *   "senderName": "Orchestrator",
 *   "senderType": "orchestrator",
 *   "conversationId": "conv-abc123"
 * }
 * ```
 */
export async function agentResponse(req, res, next) {
    try {
        const { content, senderName, senderType, conversationId } = req.body;
        if (!content || (typeof content === 'string' && content.trim().length === 0)) {
            res.status(400).json({
                success: false,
                error: 'Message content is required',
            });
            return;
        }
        if (!senderName) {
            res.status(400).json({
                success: false,
                error: 'senderName is required',
            });
            return;
        }
        const chatService = getChatService();
        // Resolve conversation: use provided ID or get/create the current one
        let resolvedConversationId = conversationId;
        if (!resolvedConversationId) {
            const current = await chatService.getCurrentConversation();
            if (current) {
                resolvedConversationId = current.id;
            }
            else {
                const newConversation = await chatService.createNewConversation('Agent Chat');
                resolvedConversationId = newConversation.id;
            }
        }
        const resolvedSenderType = senderType || 'agent';
        // Agent messages (status reports, [DONE], [WORKING], [IDLE], etc.) are internal
        // system communications that should be routed to the orchestrator only — NOT
        // saved to the user-facing chat conversation. Only orchestrator/system messages
        // appear in the user's chat.
        const isAgentSender = resolvedSenderType === 'agent';
        let savedMessageId;
        if (!isAgentSender) {
            // Save orchestrator/system messages to the chat conversation
            const savedMessage = await chatService.addDirectMessage(resolvedConversationId, content, {
                type: resolvedSenderType,
                name: senderName,
            });
            savedMessageId = savedMessage.id;
            logger.info('Agent response stored via REST', {
                senderName,
                senderType: resolvedSenderType,
                conversationId: resolvedConversationId,
                messageId: savedMessage.id,
            });
        }
        // Forward agent status reports to the orchestrator queue and Slack.
        // Agents call report-status with [DONE], [IDLE], [WORKING], or structured [STATUS REPORT].
        // All are forwarded so the orchestrator can take follow-up action
        // (assign next task, notify user, etc.) without waiting for ActivityMonitor polling.
        if (isAgentSender) {
            logger.info('Agent status routed to orchestrator (not saved to chat)', {
                senderName,
                conversationId: resolvedConversationId,
                preview: content.substring(0, 80),
            });
            // 2026-05-23 incident fix: agent-originating [DONE] / [COMPLETED]
            // / [DELIVERED] markers in a slack conversation are a delivery
            // signal — Steve is waiting for ORC to forward the result via
            // reply-slack. Record a pending delivery; if ORC doesn't reply
            // within the cadence, the enforcer will nudge it with
            // [DELIVER_REQUIRED]. The service no-ops on non-slack conversations
            // and on non-delivery markers (e.g. [IN_PROGRESS]).
            try {
                const { OrcDeliveryEnforcerService } = await import('../../services/orc/orc-delivery-enforcer.service.js');
                OrcDeliveryEnforcerService.getInstance()?.markPendingDelivery({
                    conversationId: resolvedConversationId,
                    agentSender: senderName,
                    text: content,
                });
            }
            catch (enforcerErr) {
                logger.warn('OrcDeliveryEnforcer.markPendingDelivery threw (swallowed)', {
                    error: enforcerErr instanceof Error ? enforcerErr.message : String(enforcerErr),
                });
            }
            try {
                // 1. Enqueue notification to orchestrator via MessageQueueService
                if (messageQueueService) {
                    messageQueueService.enqueue({
                        content: `Agent status: ${content}`,
                        conversationId: resolvedConversationId,
                        source: 'system_event',
                    });
                }
                // 2. Send Slack notification for task completions
                if (content.startsWith('[DONE]')) {
                    const { getSlackThreadStore } = await import('../../services/slack/slack-thread-store.service.js');
                    const { getSlackOrchestratorBridge } = await import('../../services/slack/slack-orchestrator-bridge.js');
                    const threadStore = getSlackThreadStore();
                    const threads = threadStore ? threadStore.findThreadsForAgent(senderName) : [];
                    if (threads.length > 0) {
                        const bridge = getSlackOrchestratorBridge();
                        if (bridge) {
                            const summaryText = content.replace(/^\[DONE\]\s*Agent\s+\S+:\s*/, '');
                            await bridge.sendNotification({
                                type: 'task_completed',
                                title: 'Agent Completed',
                                message: `Agent ${senderName} completed: ${summaryText}`,
                                urgency: 'normal',
                                timestamp: new Date().toISOString(),
                                channelId: threads[0].channelId,
                                threadTs: threads[0].threadTs,
                            });
                            // If a pending reaction was stored when the message arrived
                            // (orchestrator-routed path sets a 👀 on the trigger message),
                            // flip it to ✅ now that the agent reply has been delivered.
                            // No-op when there's no pending entry for this thread.
                            await bridge.addCompletionReaction(threads[0].channelId, threads[0].threadTs);
                        }
                    }
                }
            }
            catch (notifyErr) {
                logger.warn('Failed to send agent status notification', {
                    error: notifyErr instanceof Error ? notifyErr.message : String(notifyErr),
                    senderName,
                });
            }
        }
        res.status(201).json({
            success: true,
            data: {
                messageId: savedMessageId,
                conversationId: resolvedConversationId,
            },
        });
    }
    catch (error) {
        logger.error('Failed to store agent response', {
            error: error instanceof Error ? error.message : String(error),
        });
        next(error);
    }
}
// =============================================================================
// Conversation Endpoints
// =============================================================================
/**
 * GET /api/chat/conversations
 *
 * List all conversations with optional filtering.
 *
 * @param req - Request with query params for filtering
 * @param res - Response with array of conversations
 */
export async function getConversations(req, res, next) {
    try {
        const { includeArchived, search, limit, offset, channelType } = req.query;
        const filter = {
            includeArchived: includeArchived === 'true',
            search: search,
            channelType: (typeof channelType === 'string' && isValidChannelType(channelType))
                ? channelType
                : undefined,
            limit: limit ? parseInt(limit, 10) : undefined,
            offset: offset ? parseInt(offset, 10) : undefined,
        };
        const chatService = getChatService();
        const conversations = await chatService.getConversations(filter);
        res.json({
            success: true,
            data: conversations,
            count: conversations.length,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * GET /api/chat/conversations/current
 *
 * Get the current (most recent active) conversation.
 * Creates a new conversation if none exists.
 *
 * @param req - Request
 * @param res - Response with current conversation
 */
export async function getCurrentConversation(req, res, next) {
    try {
        const chatService = getChatService();
        const conversation = await chatService.getCurrentConversation();
        if (!conversation) {
            // Create a new conversation if none exists
            const newConversation = await chatService.createNewConversation('New Chat');
            res.json({
                success: true,
                data: newConversation,
                isNew: true,
            });
            return;
        }
        res.json({
            success: true,
            data: conversation,
            isNew: false,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * GET /api/chat/conversations/:id
 *
 * Get a single conversation by ID.
 *
 * @param req - Request with conversation ID param
 * @param res - Response with the conversation
 */
export async function getConversation(req, res, next) {
    try {
        const { id } = req.params;
        const chatService = getChatService();
        const conversation = await chatService.getConversation(id);
        if (!conversation) {
            res.status(404).json({
                success: false,
                error: 'Conversation not found',
            });
            return;
        }
        res.json({
            success: true,
            data: conversation,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * POST /api/chat/conversations
 *
 * Create a new conversation.
 *
 * @param req - Request with body: { title?: string }
 * @param res - Response with created conversation
 */
export async function createConversation(req, res, next) {
    try {
        const { title } = req.body;
        const chatService = getChatService();
        const conversation = await chatService.createNewConversation(title);
        res.status(201).json({
            success: true,
            data: conversation,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * PUT /api/chat/conversations/:id
 *
 * Update a conversation's title.
 *
 * @param req - Request with conversation ID param and body: { title: string }
 * @param res - Response with updated conversation
 */
export async function updateConversation(req, res, next) {
    try {
        const { id } = req.params;
        const { title } = req.body;
        if (!title || typeof title !== 'string') {
            res.status(400).json({
                success: false,
                error: 'Title is required',
            });
            return;
        }
        const chatService = getChatService();
        const conversation = await chatService.updateConversationTitle(id, title);
        res.json({
            success: true,
            data: conversation,
        });
    }
    catch (error) {
        if (error instanceof ConversationNotFoundError) {
            res.status(404).json({
                success: false,
                error: 'Conversation not found',
            });
            return;
        }
        next(error);
    }
}
/**
 * PUT /api/chat/conversations/:id/archive
 *
 * Archive a conversation.
 *
 * @param req - Request with conversation ID param
 * @param res - Response confirming archive
 */
export async function archiveConversation(req, res, next) {
    try {
        const { id } = req.params;
        const chatService = getChatService();
        await chatService.archiveConversation(id);
        res.json({
            success: true,
            message: 'Conversation archived',
        });
    }
    catch (error) {
        if (error instanceof ConversationNotFoundError) {
            res.status(404).json({
                success: false,
                error: 'Conversation not found',
            });
            return;
        }
        next(error);
    }
}
/**
 * PUT /api/chat/conversations/:id/unarchive
 *
 * Unarchive a conversation.
 *
 * @param req - Request with conversation ID param
 * @param res - Response confirming unarchive
 */
export async function unarchiveConversation(req, res, next) {
    try {
        const { id } = req.params;
        const chatService = getChatService();
        await chatService.unarchiveConversation(id);
        res.json({
            success: true,
            message: 'Conversation unarchived',
        });
    }
    catch (error) {
        if (error instanceof ConversationNotFoundError) {
            res.status(404).json({
                success: false,
                error: 'Conversation not found',
            });
            return;
        }
        next(error);
    }
}
/**
 * DELETE /api/chat/conversations/:id
 *
 * Delete a conversation and all its messages.
 *
 * @param req - Request with conversation ID param
 * @param res - Response confirming deletion
 */
export async function deleteConversation(req, res, next) {
    try {
        const { id } = req.params;
        const chatService = getChatService();
        await chatService.deleteConversation(id);
        res.json({
            success: true,
            message: 'Conversation deleted',
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * POST /api/chat/conversations/:id/clear
 *
 * Clear all messages in a conversation.
 *
 * @param req - Request with conversation ID param
 * @param res - Response confirming clear
 */
export async function clearConversation(req, res, next) {
    try {
        const { id } = req.params;
        const chatService = getChatService();
        await chatService.clearConversation(id);
        res.json({
            success: true,
            message: 'Conversation cleared',
        });
    }
    catch (error) {
        next(error);
    }
}
// =============================================================================
// Statistics Endpoint
// =============================================================================
/**
 * GET /api/chat/statistics
 *
 * Get chat statistics.
 *
 * @param req - Request
 * @param res - Response with statistics
 */
export async function getStatistics(req, res, next) {
    try {
        const chatService = getChatService();
        const statistics = await chatService.getStatistics();
        res.json({
            success: true,
            data: statistics,
        });
    }
    catch (error) {
        next(error);
    }
}
// =============================================================================
// Thread Status Endpoints
// =============================================================================
/**
 * GET /api/chat/thread-status
 *
 * Returns all thread status entries. Supports optional status filter.
 *
 * @param req - Request with optional query: { status?: ThreadStatus }
 * @param res - Response with array of ThreadStatusEntry
 */
export function getThreadStatusList(req, res, next) {
    try {
        if (!threadStatusQueueService) {
            res.status(503).json({
                success: false,
                error: 'Thread status queue service not initialized',
            });
            return;
        }
        const { status } = req.query;
        const entries = status
            ? threadStatusQueueService.getByStatus(status)
            : threadStatusQueueService.getAllEntries();
        res.json({
            success: true,
            data: entries,
            count: entries.length,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * GET /api/chat/thread-status/stats
 *
 * Returns thread status queue statistics.
 *
 * @param req - Request
 * @param res - Response with ThreadStatusStats
 */
export function getThreadStatusStats(req, res, next) {
    try {
        if (!threadStatusQueueService) {
            res.status(503).json({
                success: false,
                error: 'Thread status queue service not initialized',
            });
            return;
        }
        const stats = threadStatusQueueService.getStats();
        res.json({
            success: true,
            data: stats,
        });
    }
    catch (error) {
        next(error);
    }
}
/**
 * GET /api/chat/thread-status/:threadKey
 *
 * Returns a single thread status entry by threadKey.
 *
 * @param req - Request with threadKey param
 * @param res - Response with ThreadStatusEntry
 */
export function getThreadStatusByKey(req, res, next) {
    try {
        if (!threadStatusQueueService) {
            res.status(503).json({
                success: false,
                error: 'Thread status queue service not initialized',
            });
            return;
        }
        const { threadKey } = req.params;
        const entry = threadStatusQueueService.get(decodeURIComponent(threadKey));
        if (!entry) {
            res.status(404).json({
                success: false,
                error: `Thread not found: ${threadKey}`,
            });
            return;
        }
        res.json({
            success: true,
            data: entry,
        });
    }
    catch (error) {
        next(error);
    }
}
// =============================================================================
// Highlights Endpoint
// =============================================================================
/**
 * GET /api/chat/highlights
 *
 * Retrieve highlighted messages (decisions, questions, blockers, task completions)
 * from all conversations. Useful for dashboard activity feeds.
 *
 * @param req - Request with query params: { since?: string, limit?: number }
 * @param res - Response with array of chat highlights
 * @param next - Express next function for error propagation
 *
 * @example
 * ```
 * GET /api/chat/highlights?since=2026-03-25T00:00:00.000Z&limit=10
 * ```
 */
export async function handleGetHighlights(req, res, next) {
    try {
        const { since, limit } = req.query;
        const highlightsService = getChatHighlightsService();
        const highlights = await highlightsService.getHighlights({
            since: since,
            limit: limit ? parseInt(limit, 10) : undefined,
        });
        res.json({
            success: true,
            data: { highlights },
        });
    }
    catch (error) {
        next(error);
    }
}
//# sourceMappingURL=chat.controller.js.map