/**
 * Letta API Client
 *
 * Uses the official @letta-ai/letta-client SDK for all API interactions.
 */

import { Letta } from '@letta-ai/letta-client';

import { createLogger } from '../logger.js';

const log = createLogger('Letta-api');
const LETTA_BASE_URL = process.env.LETTA_BASE_URL || 'https://api.letta.com';

function getClient(): Letta {
  const apiKey = process.env.LETTA_API_KEY;
  // Local servers may not require an API key
  return new Letta({ 
    apiKey: apiKey || '', 
    baseURL: LETTA_BASE_URL,
    defaultHeaders: { "X-Letta-Source": "lettabot" },
  });
}

async function listAgentApprovalRunIds(agentId: string, limit = 10): Promise<string[]> {
  try {
    const client = getClient();
    const runsPage = await client.runs.list({
      agent_id: agentId,
      stop_reason: 'requires_approval',
      limit,
    });

    const runIds: string[] = [];
    for await (const run of runsPage) {
      if (run.stop_reason !== 'requires_approval') continue;
      const id = (run as { id?: unknown }).id;
      if (typeof id === 'string' && id.length > 0) {
        runIds.push(id);
      }
      if (runIds.length >= limit) break;
    }
    return runIds;
  } catch (e) {
    log.warn('Failed to list approval-blocked runs:', e instanceof Error ? e.message : e);
    return [];
  }
}

/**
 * Test connection to Letta server (silent, no error logging)
 */
export async function testConnection(): Promise<boolean> {
  try {
    const client = getClient();
    // Use a simple endpoint that doesn't have pagination issues
    await client.agents.list({ limit: 1 });
    return true;
  } catch {
    return false;
  }
}

/**
 * Recover stuck approvals at the agent level without requiring a concrete
 * conversation ID. This is the fallback for default/alias conversations.
 */
export async function recoverPendingApprovalsForAgent(
  agentId: string,
  reason = 'Session was interrupted - retrying request'
): Promise<{ recovered: boolean; details: string }> {
  try {
    const pending = await getPendingApprovals(agentId);
    if (pending.length === 0) {
      // Some servers report approval conflicts while omitting pending_approval
      // details/tool_call IDs. In that case, cancel approval-blocked runs directly.
      const approvalRunIds = await listAgentApprovalRunIds(agentId);
      if (approvalRunIds.length === 0) {
        return { recovered: false, details: 'No pending approvals found on agent' };
      }
      const cancelled = await cancelRuns(agentId, approvalRunIds);
      if (!cancelled) {
        return {
          recovered: false,
          details: `Found ${approvalRunIds.length} approval-blocked run(s) but failed to cancel`,
        };
      }
      return {
        recovered: true,
        details: `Cancelled ${approvalRunIds.length} approval-blocked run(s) without tool-call details`,
      };
    }

    let rejectedCount = 0;
    for (const approval of pending) {
      const ok = await rejectApproval(agentId, {
        toolCallId: approval.toolCallId,
        reason,
      });
      if (ok) rejectedCount += 1;
    }

    const runIds = [...new Set(
      pending
        .map(a => a.runId)
        .filter((id): id is string => !!id && id !== 'unknown')
    )];
    if (runIds.length > 0) {
      await cancelRuns(agentId, runIds);
    }

    if (rejectedCount === 0) {
      return { recovered: false, details: 'Failed to reject pending approvals' };
    }

    return {
      recovered: true,
      details: `Rejected ${rejectedCount} pending approval(s)${runIds.length > 0 ? ` and cancelled ${runIds.length} run(s)` : ''}`,
    };
  } catch (e) {
    return {
      recovered: false,
      details: `Agent-level approval recovery failed: ${e instanceof Error ? e.message : String(e)}`,
    };
  }
}

/**
 * Returns true when a conversation id refers to a concrete conversation record
 * that can be queried for messages/runs.
 */
export function isRecoverableConversationId(conversationId?: string | null): conversationId is string {
  if (typeof conversationId !== 'string') return false;
  const value = conversationId.trim();
  if (!value) return false;
  // SDK/API aliases are not materialized conversation IDs.
  if (value === 'default' || value === 'shared') return false;
  return true;
}

// Re-export types that callers use
export type LettaTool = Awaited<ReturnType<Letta['tools']['upsert']>>;

/**
 * Upsert a tool to the Letta API
 */
export async function upsertTool(params: {
  source_code: string;
  description?: string;
  tags?: string[];
}): Promise<LettaTool> {
  const client = getClient();
  return client.tools.upsert({
    source_code: params.source_code,
    description: params.description,
    tags: params.tags,
  });
}

/**
 * List all tools
 */
export async function listTools(): Promise<LettaTool[]> {
  const client = getClient();
  const page = await client.tools.list();
  const tools: LettaTool[] = [];
  for await (const tool of page) {
    tools.push(tool);
  }
  return tools;
}

/**
 * Get a tool by name
 */
export async function getToolByName(name: string): Promise<LettaTool | null> {
  try {
    const client = getClient();
    const page = await client.tools.list({ name });
    for await (const tool of page) {
      if (tool.name === name) return tool;
    }
    return null;
  } catch {
    return null;
  }
}

/**
 * Add a tool to an agent
 */
export async function addToolToAgent(agentId: string, toolId: string): Promise<void> {
  const client = getClient();
  await client.agents.tools.attach(toolId, { agent_id: agentId });
}

/**
 * Check if an agent exists
 */
export async function agentExists(agentId: string): Promise<boolean> {
  try {
    const client = getClient();
    await client.agents.retrieve(agentId);
    return true;
  } catch {
    return false;
  }
}

/**
 * Get an agent's current model handle
 */
export async function getAgentModel(agentId: string): Promise<string | null> {
  try {
    const client = getClient();
    const agent = await client.agents.retrieve(agentId);
    return agent.model ?? null;
  } catch (e) {
    log.error('Failed to get agent model:', e);
    return null;
  }
}

/**
 * Update an agent's model
 */
export async function updateAgentModel(agentId: string, model: string): Promise<boolean> {
  try {
    const client = getClient();
    await client.agents.update(agentId, { model });
    return true;
  } catch (e) {
    log.error('Failed to update agent model:', e);
    return false;
  }
}

/**
 * Update an agent's name
 */
export async function updateAgentName(agentId: string, name: string): Promise<boolean> {
  try {
    const client = getClient();
    await client.agents.update(agentId, { name });
    return true;
  } catch (e) {
    log.error('Failed to update agent name:', e);
    return false;
  }
}

/**
 * List available models
 */
export async function listModels(options?: { providerName?: string; providerCategory?: 'base' | 'byok' }): Promise<Array<{ handle: string; name: string; display_name?: string; tier?: string }>> {
  try {
    const client = getClient();
    const params: Record<string, unknown> = {};
    if (options?.providerName) params.provider_name = options.providerName;
    if (options?.providerCategory) params.provider_category = [options.providerCategory];
    const page = await client.models.list(Object.keys(params).length > 0 ? params : undefined);
    const models: Array<{ handle: string; name: string; display_name?: string; tier?: string }> = [];
    for await (const model of page) {
      if (model.handle && model.name) {
        models.push({ 
          handle: model.handle, 
          name: model.name,
          display_name: model.display_name ?? undefined,
          tier: (model as { tier?: string }).tier ?? undefined,
        });
      }
    }
    return models;
  } catch (e) {
    log.error('Failed to list models:', e);
    return [];
  }
}

/**
 * Get the most recent run time for an agent
 */
export async function getLastRunTime(agentId: string): Promise<Date | null> {
  try {
    const client = getClient();
    const page = await client.runs.list({ agent_id: agentId, limit: 1 });
    for await (const run of page) {
      if (run.created_at) {
        return new Date(run.created_at);
      }
    }
    return null;
  } catch (e) {
    log.error('Failed to get last run time:', e);
    return null;
  }
}

/**
 * List agents, optionally filtered by name search
 */
export async function listAgents(query?: string): Promise<Array<{ id: string; name: string; description?: string | null; created_at?: string | null }>> {
  try {
    const client = getClient();
    const page = await client.agents.list({ query_text: query, limit: 50 });
    const agents: Array<{ id: string; name: string; description?: string | null; created_at?: string | null }> = [];
    for await (const agent of page) {
      agents.push({
        id: agent.id,
        name: agent.name,
        description: agent.description,
        created_at: agent.created_at,
      });
    }
    return agents;
  } catch (e) {
    log.error('Failed to list agents:', e);
    return [];
  }
}

/**
 * Find an agent by exact name match
 * Returns the most recently created agent if multiple match
 */
export async function findAgentByName(name: string): Promise<{ id: string; name: string } | null> {
  try {
    const client = getClient();
    const page = await client.agents.list({ query_text: name, limit: 50 });
    let bestMatch: { id: string; name: string; created_at?: string | null } | null = null;
    
    for await (const agent of page) {
      // Exact name match only
      if (agent.name === name) {
        // Keep the most recently created if multiple match
        if (!bestMatch || (agent.created_at && bestMatch.created_at && agent.created_at > bestMatch.created_at)) {
          bestMatch = { id: agent.id, name: agent.name, created_at: agent.created_at };
        }
      }
    }
    
    return bestMatch ? { id: bestMatch.id, name: bestMatch.name } : null;
  } catch (e) {
    log.error('Failed to find agent by name:', e);
    return null;
  }
}

// ============================================================================
// Tool Approval Management
// ============================================================================

export interface PendingApproval {
  runId: string;
  toolCallId: string;
  toolName: string;
  messageId: string;
}

/**
 * Check for pending approval requests on an agent's conversation.
 * Returns details of any tool calls waiting for approval.
 */
export async function getPendingApprovals(
  agentId: string,
  conversationId?: string
): Promise<PendingApproval[]> {
  try {
    const client = getClient();

    // Prefer agent-level pending approval to avoid scanning stale history.
    // IMPORTANT: Must include 'agent.pending_approval' or the field won't be returned.
    try {
      const agentState = await client.agents.retrieve(agentId, {
        include: ['agent.pending_approval'],
      });
      if ('pending_approval' in agentState) {
        const pending = agentState.pending_approval;
        if (!pending) {
          log.info('No pending approvals on agent; falling back to run scan');
        } else {
          log.info(`Found pending approval: ${pending.id}, run_id=${pending.run_id}`);

          // Extract tool calls - handle both Array<ToolCall> and ToolCallDelta formats
          const rawToolCalls = pending.tool_calls;
          const toolCallsList: Array<{ tool_call_id: string; name: string }> = [];

          if (Array.isArray(rawToolCalls)) {
            for (const tc of rawToolCalls) {
              if (tc && 'tool_call_id' in tc && tc.tool_call_id) {
                toolCallsList.push({ tool_call_id: tc.tool_call_id, name: tc.name || 'unknown' });
              }
            }
          } else if (rawToolCalls && typeof rawToolCalls === 'object' && 'tool_call_id' in rawToolCalls && rawToolCalls.tool_call_id) {
            // ToolCallDelta case
            toolCallsList.push({ tool_call_id: rawToolCalls.tool_call_id, name: rawToolCalls.name || 'unknown' });
          }

          // Fallback to deprecated singular tool_call field
          if (toolCallsList.length === 0 && pending.tool_call) {
            const tc = pending.tool_call;
            if ('tool_call_id' in tc && tc.tool_call_id) {
              toolCallsList.push({ tool_call_id: tc.tool_call_id, name: tc.name || 'unknown' });
            }
          }

          const seen = new Set<string>();
          const approvals: PendingApproval[] = [];
          for (const tc of toolCallsList) {
            if (seen.has(tc.tool_call_id)) continue;
            seen.add(tc.tool_call_id);
            approvals.push({
              runId: pending.run_id || 'unknown',
              toolCallId: tc.tool_call_id,
              toolName: tc.name || 'unknown',
              messageId: pending.id,
            });
          }
          if (approvals.length > 0) {
            log.info(`Extracted ${approvals.length} pending approval(s): ${approvals.map(a => a.toolName).join(', ')}`);
            return approvals;
          }
          log.warn('Agent pending_approval had no tool_call_ids; falling back to run scan');
        }
      }
    } catch (e) {
      log.warn('Failed to retrieve agent pending_approval, falling back to run scan:', e);
    }
    
    // First, check for runs with 'requires_approval' stop reason
    const runsPage = await client.runs.list({
      agent_id: agentId,
      conversation_id: conversationId,
      stop_reason: 'requires_approval',
      limit: 10,
    });
    
    const pendingApprovals: PendingApproval[] = [];
    
    for await (const run of runsPage) {
      if (run.status === 'running' || run.stop_reason === 'requires_approval') {
        // Get recent messages to find approval_request_message
        const messagesPage = await client.agents.messages.list(agentId, {
          conversation_id: conversationId,
          limit: 100,
        });
        
        const messages: Array<{ message_type?: string }> = [];
        for await (const msg of messagesPage) {
          messages.push(msg as { message_type?: string });
        }
        
        const resolvedToolCalls = new Set<string>();
        for (const msg of messages) {
          if ('message_type' in msg && msg.message_type === 'approval_response_message') {
            const approvalMsg = msg as {
              approvals?: Array<{ tool_call_id?: string | null }>;
            };
            const approvals = approvalMsg.approvals || [];
            for (const approval of approvals) {
              if (approval.tool_call_id) {
                resolvedToolCalls.add(approval.tool_call_id);
              }
            }
          }
        }
        
        const seenToolCalls = new Set<string>();
        for (const msg of messages) {
          // Check for approval_request_message type
          if ('message_type' in msg && msg.message_type === 'approval_request_message') {
            const approvalMsg = msg as {
              id: string;
              tool_calls?: Array<{ tool_call_id: string; name: string }>;
              tool_call?: { tool_call_id: string; name: string };
              run_id?: string;
            };
            
            // Extract tool call info
            const toolCalls = approvalMsg.tool_calls || (approvalMsg.tool_call ? [approvalMsg.tool_call] : []);
            for (const tc of toolCalls) {
              if (resolvedToolCalls.has(tc.tool_call_id)) {
                continue;
              }
              if (seenToolCalls.has(tc.tool_call_id)) {
                continue;
              }
              seenToolCalls.add(tc.tool_call_id);
              pendingApprovals.push({
                runId: approvalMsg.run_id || run.id,
                toolCallId: tc.tool_call_id,
                toolName: tc.name,
                messageId: approvalMsg.id,
              });
            }
          }
        }
      }
    }
    
    return pendingApprovals;
  } catch (e) {
    log.error('Failed to get pending approvals:', e);
    return [];
  }
}

/**
 * Reject a pending tool approval request.
 * Sends an approval response with approve: false.
 */
export async function rejectApproval(
  agentId: string,
  approval: {
    toolCallId: string;
    reason?: string;
  },
  conversationId?: string
): Promise<boolean> {
  try {
    const client = getClient();
    
    // Send approval response via messages.create
    await client.agents.messages.create(agentId, {
      messages: [{
        type: 'approval',
        approvals: [{
          approve: false,
          tool_call_id: approval.toolCallId,
          type: 'approval',
          reason: approval.reason || 'Session was interrupted - please retry your request',
        }],
      }],
      streaming: false,
    });
    
    log.info(`Rejected approval for tool call ${approval.toolCallId}`);
    return true;
  } catch (e) {
    const err = e as { status?: number; error?: { detail?: string } };
    const detail = err?.error?.detail || '';
    if (err?.status === 400 && detail.includes('No tool call is currently awaiting approval')) {
      log.warn(`Approval already resolved for tool call ${approval.toolCallId}`);
      return true;
    }
    // Re-throw rate limit errors so callers can bail out early instead of
    // hammering the API in a tight loop.
    if (err?.status === 429) {
      log.error('Failed to reject approval:', e);
      throw e;
    }
    log.error('Failed to reject approval:', e);
    return false;
  }
}

/**
 * Cancel active runs for an agent.
 * Optionally specify specific run IDs to cancel.
 * Note: Requires Redis on the server for canceling active runs.
 */
export async function cancelRuns(
  agentId: string,
  runIds?: string[]
): Promise<boolean> {
  try {
    const client = getClient();
    await client.agents.messages.cancel(agentId, {
      run_ids: runIds,
    });
    log.info(`Cancelled runs for agent ${agentId}${runIds ? ` (${runIds.join(', ')})` : ''}`);
    return true;
  } catch (e) {
    log.error('Failed to cancel runs:', e);
    return false;
  }
}

/**
 * Cancel active runs for a specific conversation.
 * Scoped to a single conversation -- won't affect other channels/conversations.
 */
export async function cancelConversation(
  conversationId: string
): Promise<boolean> {
  try {
    const client = getClient();
    await client.conversations.cancel(conversationId);
    log.info(`Cancelled runs for conversation ${conversationId}`);
    return true;
  } catch (e) {
    // 409 "No active runs to cancel" is expected when cancel fires before run starts
    const err = e as { status?: number };
    if (err?.status === 409) {
      log.info(`No active runs to cancel for conversation ${conversationId} (409)`);
      return true;
    }
    log.error(`Failed to cancel conversation ${conversationId}:`, e);
    return false;
  }
}

/**
 * Fetch the error detail from the latest failed run on an agent.
 * Returns the actual error detail from run metadata (which is more
 * descriptive than the opaque `stop_reason=error` wire message).
 * Single API call -- fast enough to use on every error.
 */
export async function getLatestRunError(
  agentId: string,
  conversationId?: string
): Promise<{ message: string; stopReason: string; isApprovalError: boolean } | null> {
  try {
    const client = getClient();
    const runs = await client.runs.list({
      agent_id: agentId,
      conversation_id: conversationId,
      limit: 1,
    });
    const runsArray: Array<Record<string, unknown>> = [];
    for await (const run of runs) {
      runsArray.push(run as unknown as Record<string, unknown>);
      break; // Only need the first one
    }
    const run = runsArray[0];
    if (!run) return null;

    if (conversationId
      && typeof run.conversation_id === 'string'
      && run.conversation_id !== conversationId) {
      log.warn('Latest run lookup returned a different conversation, skipping enrichment');
      return null;
    }

    const meta = run.metadata as Record<string, unknown> | undefined;
    const err = meta?.error as Record<string, unknown> | undefined;
    const detail = typeof err?.detail === 'string' ? err.detail : '';
    const stopReason = typeof run.stop_reason === 'string' ? run.stop_reason : 'error';

    // Run has no metadata error but is stuck waiting for approval.
    // This happens when the 409 prevents a new run from starting --
    // the latest run is the one blocking, and it has no error, just a
    // stop_reason indicating it needs approval.
    const status = typeof run.status === 'string' ? run.status : '';
    if (!detail && stopReason === 'requires_approval') {
      const runId = typeof run.id === 'string' ? run.id : 'unknown';
      log.info(`Latest run stuck on approval: run=${runId} status=${status} stop_reason=${stopReason}`);
      return {
        message: `Run ${runId} stuck waiting for tool approval (status=${status})`,
        stopReason,
        isApprovalError: true,
      };
    }

    if (!detail) return null;

    const isApprovalError = detail.toLowerCase().includes('waiting for approval')
      || detail.toLowerCase().includes('approve or deny');

    log.info(`Latest run error: ${detail.slice(0, 150)}${isApprovalError ? ' [approval]' : ''}`);
    return { message: detail, stopReason, isApprovalError };
  } catch (e) {
    log.warn('Failed to fetch latest run error:', e instanceof Error ? e.message : e);
    return null;
  }
}

async function listActiveConversationRunIds(
  agentId: string,
  conversationId: string,
  limit = 25
): Promise<string[]> {
  try {
    const client = getClient();
    const runs = await client.runs.list({
      agent_id: agentId,
      conversation_id: conversationId,
      active: true,
      limit,
    });

    const runIds: string[] = [];
    for await (const run of runs) {
      const id = (run as { id?: unknown }).id;
      if (typeof id === 'string' && id.length > 0) {
        runIds.push(id);
      }
      if (runIds.length >= limit) break;
    }
    return runIds;
  } catch (e) {
    log.warn('Failed to list active conversation runs:', e instanceof Error ? e.message : e);
    return [];
  }
}

/**
 * Disable tool approval requirement for a specific tool on an agent.
 * This sets requires_approval: false at the server level.
 */
export async function disableToolApproval(
  agentId: string,
  toolName: string
): Promise<boolean> {
  try {
    const client = getClient();
    // Note: API expects 'requires_approval' but client types say 'body_requires_approval'
    // This is a bug in @letta-ai/letta-client - filed issue, using workaround
    await client.agents.tools.updateApproval(toolName, {
      agent_id: agentId,
      requires_approval: false,
    } as unknown as Parameters<typeof client.agents.tools.updateApproval>[1]);
    log.info(`Disabled approval requirement for tool ${toolName} on agent ${agentId}`);
    return true;
  } catch (e) {
    log.error(`Failed to disable tool approval for ${toolName}:`, e);
    return false;
  }
}

/**
 * Get tools attached to an agent with their approval settings.
 */
export async function getAgentTools(agentId: string): Promise<Array<{
  name: string;
  id: string;
  requiresApproval?: boolean;
}>> {
  try {
    const client = getClient();
    const toolsPage = await client.agents.tools.list(agentId);
    const tools: Array<{ name: string; id: string; requiresApproval?: boolean }> = [];
    
    for await (const tool of toolsPage) {
      tools.push({
        name: tool.name ?? 'unknown',
        id: tool.id,
        // Note: The API might not return this field directly on list
        // We may need to check each tool individually
        requiresApproval: (tool as { requires_approval?: boolean }).requires_approval,
      });
    }
    
    return tools;
  } catch (e) {
    log.error('Failed to get agent tools:', e);
    return [];
  }
}

/**
 * Ensure no tools on the agent require approval.
 * Call on startup to proactively prevent stuck approval states.
 */
export async function ensureNoToolApprovals(agentId: string): Promise<void> {
  try {
    const tools = await getAgentTools(agentId);
    const approvalTools = tools.filter(t => t.requiresApproval);
    if (approvalTools.length > 0) {
      log.info(`Found ${approvalTools.length} tool(s) requiring approval: ${approvalTools.map(t => t.name).join(', ')}`);
      log.info('Disabling tool approvals for headless operation...');
      await disableAllToolApprovals(agentId);
    }
  } catch (e) {
    log.warn('Failed to check/disable tool approvals:', e);
  }
}

/**
 * Disable approval requirement for ALL tools on an agent.
 * Useful for ensuring a headless deployment doesn't get stuck.
 */
/**
 * Recover from orphaned approval_request_messages by directly inspecting the conversation.
 * 
 * Unlike getPendingApprovals() which relies on agent.pending_approval or run stop_reason,
 * this function looks at the actual conversation messages to find unresolved approval requests
 * from terminated (failed/cancelled) runs.
 * 
 * Returns { recovered: true } if orphaned approvals were found and resolved.
 */
export async function recoverOrphanedConversationApproval(
  agentId: string,
  conversationId: string,
  deepScan = false
): Promise<{ recovered: boolean; details: string }> {
  try {
    if (!isRecoverableConversationId(conversationId)) {
      return {
        recovered: false,
        details: `Conversation is not recoverable: ${conversationId || '(empty)'}`,
      };
    }

    const client = getClient();
    
    // List recent messages from the conversation to find orphaned approvals.
    // Default: 50 (fast path). Deep scan: 500 (for conversations with many approvals).
    const scanLimit = deepScan ? 500 : 50;
    log.info(`Scanning ${scanLimit} messages for orphaned approvals...`);
    const messagesPage = await client.conversations.messages.list(conversationId, { limit: scanLimit });
    const messages: Array<Record<string, unknown>> = [];
    for await (const msg of messagesPage) {
      messages.push(msg as unknown as Record<string, unknown>);
    }
    
    if (messages.length === 0) {
      return { recovered: false, details: 'No messages in conversation' };
    }
    
    // Build set of tool_call_ids that already have approval responses
    const resolvedToolCalls = new Set<string>();
    for (const msg of messages) {
      if (msg.message_type === 'approval_response_message') {
        const approvals = (msg.approvals as Array<{ tool_call_id?: string }>) || [];
        for (const a of approvals) {
          if (a.tool_call_id) resolvedToolCalls.add(a.tool_call_id);
        }
      }
    }
    
    // Find unresolved approval_request_messages
    interface UnresolvedApproval {
      toolCallId: string;
      toolName: string;
      runId: string;
    }
    const unresolvedByRun = new Map<string, UnresolvedApproval[]>();
    const seenToolCallIds = new Set<string>();
    
    for (const msg of messages) {
      if (msg.message_type !== 'approval_request_message') continue;
      
      const toolCalls = (msg.tool_calls as Array<{ tool_call_id: string; name: string }>) 
        || (msg.tool_call ? [msg.tool_call as { tool_call_id: string; name: string }] : []);
      const runId = msg.run_id as string | undefined;
      
      for (const tc of toolCalls) {
        if (!tc.tool_call_id || resolvedToolCalls.has(tc.tool_call_id)) continue;
        // Skip duplicate tool_call_ids across multiple approval_request_messages
        if (seenToolCallIds.has(tc.tool_call_id)) continue;
        seenToolCallIds.add(tc.tool_call_id);
        
        const key = runId || 'unknown';
        if (!unresolvedByRun.has(key)) unresolvedByRun.set(key, []);
        unresolvedByRun.get(key)!.push({
          toolCallId: tc.tool_call_id,
          toolName: tc.name || 'unknown',
          runId: key,
        });
      }
    }
    
    if (unresolvedByRun.size === 0) {
      return { recovered: false, details: 'No unresolved approval requests found' };
    }
    
    // Check each run's status - only resolve orphaned approvals from terminated runs
    let recoveredCount = 0;
    const details: string[] = [];
    
    for (const [runId, approvals] of unresolvedByRun) {
      if (runId === 'unknown') {
        // No run_id on the approval message - can't verify, skip
        details.push(`Skipped ${approvals.length} approval(s) with no run_id`);
        continue;
      }
      
      try {
        const run = await client.runs.retrieve(runId);
        const status = run.status;
        const stopReason = run.stop_reason;
        const isTerminated = status === 'failed' || status === 'cancelled';
        const isAbandonedApproval = status === 'completed' && stopReason === 'requires_approval';
        // Active runs stuck on approval block the entire conversation.
        // No client is going to approve them -- reject and cancel so
        // lettabot can proceed.
        const isStuckApproval = status === 'running' && stopReason === 'requires_approval';
        // Letta Cloud uses status "created" with no stop_reason for runs
        // that paused on requires_approval but haven't been resumed yet.
        // If we found unresolved approval_request_messages for this run,
        // it's stuck -- treat it the same as a running/requires_approval.
        const isCreatedWithApproval = status === 'created';
        
        if (isTerminated || isAbandonedApproval || isStuckApproval || isCreatedWithApproval) {
          log.info(`Found ${approvals.length} blocking approval(s) from ${status}/${stopReason} run ${runId}`);
          
          // Send denial for each unresolved tool call
          const approvalResponses = approvals.map(a => ({
            approve: false as const,
            tool_call_id: a.toolCallId,
            type: 'approval' as const,
            reason: `Auto-denied: originating run was ${status}/${stopReason}`,
          }));
          
          let deniedForRun = 0;
          for (let i = 0; i < approvalResponses.length; i++) {
            const approvalResponse = approvalResponses[i];
            try {
              // Letta surfaces one pending approval at a time for parallel tool calls,
              // so submit denials sequentially instead of as a single multi-ID batch.
              await client.conversations.messages.create(conversationId, {
                messages: [{
                  type: 'approval',
                  approvals: [approvalResponse],
                }],
                streaming: false,
              });
              deniedForRun += 1;
            } catch (approvalError) {
              const approvalErrMsg = approvalError instanceof Error ? approvalError.message : String(approvalError);
              log.warn(
                `Failed to submit approval denial for run ${runId} (tool_call_id=${approvalResponse.tool_call_id}):`,
                approvalError,
              );
              details.push(`Failed to deny approval ${approvalResponse.tool_call_id} from run ${runId}: ${approvalErrMsg}`);
              continue;
            }

            if (i < approvalResponses.length - 1) {
              await new Promise(resolve => setTimeout(resolve, 1500));
            }
          }

          if (deniedForRun === 0) {
            continue;
          }
          
          // The denial triggers a new agent run server-side. Wait for it to
          // settle before returning, otherwise the caller retries immediately
          // and hits a 409 because the denial's run is still processing.
          await new Promise(resolve => setTimeout(resolve, 3000));

          // Cancel only active runs for this conversation to avoid interrupting
          // unrelated in-flight requests on other conversations.
          const activeRunIds = await listActiveConversationRunIds(agentId, conversationId);
          let cancelled = false;
          if (activeRunIds.length > 0) {
            cancelled = await cancelRuns(agentId, activeRunIds);
            if (cancelled) {
              log.info(`Cancelled ${activeRunIds.length} active conversation run(s) after approval denial`);
            }
          } else {
            log.info(`No active runs to cancel for conversation ${conversationId}`);
          }
          
          recoveredCount += deniedForRun;
          const suffix = cancelled ? ' (runs cancelled)' : '';
          details.push(`Denied ${deniedForRun} approval(s) from ${status} run ${runId}${suffix}`);
        } else {
          details.push(`Run ${runId} is ${status}/${stopReason} - not orphaned`);
        }
      } catch (runError) {
        log.warn(`Failed to check run ${runId}:`, runError);
        details.push(`Failed to check run ${runId}`);
      }
    }
    
    const detailStr = details.join('; ');
    if (recoveredCount > 0) {
      log.info(`Recovered ${recoveredCount} orphaned approval(s): ${detailStr}`);
      return { recovered: true, details: detailStr };
    }
    
    return { recovered: false, details: detailStr };
  } catch (e) {
    log.error('Failed to recover orphaned conversation approval:', e);
    return { recovered: false, details: `Error: ${e instanceof Error ? e.message : String(e)}` };
  }
}

export async function disableAllToolApprovals(agentId: string): Promise<number> {
  try {
    const tools = await getAgentTools(agentId);
    let disabled = 0;
    
    for (const tool of tools) {
      const success = await disableToolApproval(agentId, tool.name);
      if (success) disabled++;
    }
    
    log.info(`Disabled approval for ${disabled}/${tools.length} tools on agent ${agentId}`);
    return disabled;
  } catch (e) {
    log.error('Failed to disable all tool approvals:', e);
    return 0;
  }
}
