/**
 * Agent Loop: Stream → Tool Execute → Loop
 * Ported from opencode-ai/opencode internal/llm/agent/agent.go
 *
 * For cloud mode (BYOK): when the AI returns tool_use calls, execute them
 * server-side (or via relay), append results, and loop until no more tool calls.
 *
 * This implements the core agentic loop pattern:
 *   1. Stream response from LLM
 *   2. Collect tool calls from the response
 *   3. Execute tools (file read, shell, etc.)
 *   4. Append tool results to message history
 *   5. Loop back to step 1 until AI finishes without tool calls
 */

import { streamChat, calculateCost } from './provider-factory.js';
import { checkNeedsCompact, buildSummarizationPrompt, compactMessages } from './auto-compact.js';

const MAX_TOOL_LOOPS = 25; // Safety limit to prevent infinite loops

/**
 * Available server-side tools (opencode pattern: tool registry per agent type)
 * These are tools the AI can call during the agent loop.
 * For relay mode, tool execution is forwarded to the CLI.
 * For cloud mode, tools execute on the server or sandbox.
 */
const TOOLS_SCHEMA = [
  {
    type: 'function',
    function: {
      name: 'read_file',
      description: 'Read the contents of a file',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File path to read' },
        },
        required: ['path'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'write_file',
      description: 'Write content to a file',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File path to write' },
          content: { type: 'string', description: 'File content' },
        },
        required: ['path', 'content'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'run_command',
      description: 'Execute a shell command',
      parameters: {
        type: 'object',
        properties: {
          command: { type: 'string', description: 'Shell command to run' },
          cwd: { type: 'string', description: 'Working directory (optional)' },
        },
        required: ['command'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'list_files',
      description: 'List files in a directory',
      parameters: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Directory path' },
          pattern: { type: 'string', description: 'Glob pattern (optional)' },
        },
        required: ['path'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'search_files',
      description: 'Search for text in files',
      parameters: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Search query (regex)' },
          path: { type: 'string', description: 'Directory to search in' },
        },
        required: ['query'],
      },
    },
  },
];

/**
 * Run the agent loop (opencode pattern: processGeneration).
 *
 * @param {string} model - LLM model name
 * @param {Array} messages - Message history
 * @param {object} options - { apiKey, systemPrompt, onEvent, executeTool, maxLoops }
 * @returns {Promise<{ messages, usage, cost }>}
 *
 * options.onEvent(event) - Called for each streaming event:
 *   { type: 'content_delta', content: '...' }
 *   { type: 'tool_call', name: '...', input: {...} }
 *   { type: 'tool_result', name: '...', output: '...' }
 *   { type: 'complete' }
 *   { type: 'error', error: '...' }
 *
 * options.executeTool(name, input) - Executes a tool, returns string result.
 *   If not provided, tool calls are skipped (text-only mode).
 */
export async function runAgentLoop(model, messages, options = {}) {
  const {
    apiKey,
    systemPrompt,
    onEvent = () => {},
    executeTool,
    maxLoops = MAX_TOOL_LOOPS,
  } = options;

  let totalUsage = { promptTokens: 0, completionTokens: 0 };
  let loopCount = 0;
  let currentMessages = [...messages];

  // Add system prompt if provided
  if (systemPrompt && (!currentMessages.length || currentMessages[0].role !== 'system')) {
    currentMessages.unshift({ role: 'system', content: systemPrompt });
  }

  while (loopCount < maxLoops) {
    loopCount++;

    // Check if we need auto-compact (opencode pattern: summarize at 85% context)
    const compactCheck = checkNeedsCompact(currentMessages, model);
    if (compactCheck.needsCompact) {
      onEvent({ type: 'system', message: 'Compacting conversation context...' });
      // In a real implementation, this would call the LLM to summarize
      // For now, just trim old messages
      if (currentMessages.length > 10) {
        const recent = currentMessages.slice(-6);
        currentMessages = [currentMessages[0], ...recent]; // keep system + recent
      }
    }

    // Stream response from LLM
    let assistantContent = '';
    let toolCalls = [];
    let currentToolCall = null;

    try {
      for await (const event of streamChat(model, currentMessages, {
        apiKey,
        tools: executeTool ? TOOLS_SCHEMA : undefined,
      })) {
        if (event.type === 'content_delta') {
          assistantContent += event.content;
          onEvent({ type: 'content_delta', content: event.content });
        } else if (event.type === 'tool_call_start') {
          currentToolCall = { name: event.name, id: event.id, input: '' };
        } else if (event.type === 'tool_call_delta') {
          if (currentToolCall) currentToolCall.input += event.content;
        } else if (event.type === 'tool_call_end') {
          if (currentToolCall) {
            try { currentToolCall.input = JSON.parse(currentToolCall.input); } catch {}
            toolCalls.push(currentToolCall);
            onEvent({ type: 'tool_call', name: currentToolCall.name, input: currentToolCall.input });
            currentToolCall = null;
          }
        } else if (event.type === 'usage') {
          totalUsage.promptTokens += event.promptTokens;
          totalUsage.completionTokens += event.completionTokens;
        } else if (event.type === 'complete') {
          break;
        }
      }
    } catch (err) {
      onEvent({ type: 'error', error: err.message });
      break;
    }

    // Append assistant message
    const assistantMsg = { role: 'assistant', content: assistantContent };
    if (toolCalls.length > 0) {
      assistantMsg.tool_calls = toolCalls.map(tc => ({
        id: tc.id || `call_${Date.now()}`,
        type: 'function',
        function: { name: tc.name, arguments: JSON.stringify(tc.input) },
      }));
    }
    currentMessages.push(assistantMsg);

    // If no tool calls, we're done (opencode pattern: finish reason != ToolUse → return)
    if (toolCalls.length === 0 || !executeTool) {
      onEvent({ type: 'complete' });
      break;
    }

    // Execute tools and append results (opencode pattern: tool execution loop)
    for (const tc of toolCalls) {
      let result;
      try {
        result = await executeTool(tc.name, tc.input);
        onEvent({ type: 'tool_result', name: tc.name, output: result });
      } catch (err) {
        result = `Error: ${err.message}`;
        onEvent({ type: 'tool_result', name: tc.name, output: result, isError: true });
      }

      currentMessages.push({
        role: 'tool',
        tool_call_id: tc.id || `call_${Date.now()}`,
        content: typeof result === 'string' ? result : JSON.stringify(result),
      });
    }

    // Loop back to get next response (step 5 → step 1)
  }

  if (loopCount >= maxLoops) {
    onEvent({ type: 'error', error: `Agent loop exceeded maximum iterations (${maxLoops})` });
  }

  const cost = calculateCost(model, totalUsage.promptTokens, totalUsage.completionTokens);

  return {
    messages: currentMessages,
    usage: totalUsage,
    costCents: cost,
  };
}

export { TOOLS_SCHEMA };
