/**
 * Multi-provider LLM API streaming client.
 *
 * Supports:
 *   - Anthropic Messages API  (native format with event: SSE)
 *   - OpenAI Chat Completions (data: SSE, also used by xAI and others)
 *   - Google Gemini           (OpenAI-compatible via generativelanguage endpoint)
 *
 * Uses native fetch() + SSE parsing — no SDK dependency.
 * All providers are normalized to yield a common event format for the agentic loop.
 */

// ── Provider defaults ────────────────────────────────────────

export const PROVIDERS = {
  anthropic: {
    name: 'Anthropic',
    baseUrl: 'https://api.anthropic.com',
    authHeader: 'x-api-key',
    envKey: 'ANTHROPIC_API_KEY',
    format: 'anthropic',
    defaultModel: 'claude-sonnet-4-6',
    models: {
      fast: 'claude-haiku-4-5-20251001',
      balanced: 'claude-sonnet-4-6',
      powerful: 'claude-opus-4-6'
    }
  },
  openai: {
    name: 'OpenAI',
    baseUrl: 'https://api.openai.com',
    authHeader: 'Authorization',
    authPrefix: 'Bearer ',
    envKey: 'OPENAI_API_KEY',
    format: 'openai',
    defaultModel: 'gpt-5.2-codex',
    models: {
      fast: 'gpt-5-codex-mini',
      balanced: 'gpt-5.2-codex',
      powerful: 'gpt-5.2-codex'
    }
  },
  xai: {
    name: 'xAI',
    baseUrl: 'https://api.x.ai',
    authHeader: 'Authorization',
    authPrefix: 'Bearer ',
    envKey: 'XAI_API_KEY',
    format: 'openai',
    defaultModel: 'grok-4-1-fast-non-reasoning',
    models: {
      fast: 'grok-4-1-fast-non-reasoning',
      balanced: 'grok-4-1-fast-non-reasoning',
      powerful: 'grok-4-1-fast-reasoning'
    }
  },
  gemini: {
    name: 'Google Gemini',
    baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
    authHeader: 'Authorization',
    authPrefix: 'Bearer ',
    envKey: 'GEMINI_API_KEY',
    format: 'openai',
    defaultModel: 'gemini-2.5-flash',
    models: {
      fast: 'gemini-2.5-flash',
      balanced: 'gemini-2.5-pro',
      powerful: 'gemini-2.5-pro'
    }
  }
};

/**
 * Resolve a provider config by name, with optional overrides.
 *
 * @param {string} providerName - One of: anthropic, openai, xai, gemini
 * @param {Object} [overrides] - Override baseUrl, apiKey, models, etc.
 * @returns {Object} Merged provider config
 */
export function resolveProvider(providerName, overrides = {}) {
  const base = PROVIDERS[providerName];
  if (!base) throw new Error(`Unknown provider: "${providerName}". Supported: ${Object.keys(PROVIDERS).join(', ')}`);
  return { ...base, ...overrides };
}

// ── Anthropic streaming ──────────────────────────────────────

/**
 * Stream from the Anthropic Messages API.
 *
 * @param {Object} request - Messages API request body
 * @param {Object} opts - { apiKey, baseUrl, signal }
 * @yields {Object} Anthropic SSE events (type + payload)
 */
export async function* streamAnthropic(request, { apiKey, baseUrl, signal, extraHeaders } = {}) {
  const url = `${baseUrl || PROVIDERS.anthropic.baseUrl}/v1/messages`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json',
      ...(extraHeaders || {})
    },
    body: JSON.stringify({ ...request, stream: true }),
    signal
  });

  if (!response.ok) {
    let body = '';
    try { body = await response.text(); } catch {}
    const err = new Error(`Anthropic API error ${response.status}: ${body.slice(0, 500)}`);
    err.status = response.status;
    throw err;
  }

  yield* parseSSEStream(response.body, true);
}

// ── OpenAI-compatible streaming ──────────────────────────────

/**
 * Convert Anthropic Messages API format to OpenAI Chat Completions format.
 *
 * @param {Object} request - Anthropic-style request { model, messages, system, tools, max_tokens }
 * @returns {Object} OpenAI-style request { model, messages, tools, max_completion_tokens, stream }
 */
export function toOpenAIRequest(request) {
  const messages = [];

  // Convert system prompts to system message
  if (request.system) {
    const systemTexts = Array.isArray(request.system)
      ? request.system.map(s => typeof s === 'string' ? s : s.text).join('\n\n')
      : String(request.system);
    messages.push({ role: 'system', content: systemTexts });
  }

  // Convert messages
  for (const msg of (request.messages || [])) {
    const role = msg.role;
    const content = msg.content;

    if (typeof content === 'string') {
      messages.push({ role, content });
      continue;
    }

    if (!Array.isArray(content)) {
      messages.push({ role, content: JSON.stringify(content) });
      continue;
    }

    // Handle Anthropic-style content blocks
    if (role === 'assistant') {
      // Extract text and tool_use blocks
      const textParts = content.filter(b => b.type === 'text').map(b => b.text);
      const toolCalls = content.filter(b => b.type === 'tool_use').map((b, i) => ({
        id: b.id,
        type: 'function',
        function: { name: b.name, arguments: JSON.stringify(b.input || {}) }
      }));

      const msg = { role: 'assistant' };
      if (textParts.length > 0) msg.content = textParts.join('');
      if (toolCalls.length > 0) msg.tool_calls = toolCalls;
      if (!msg.content && !msg.tool_calls) msg.content = '';
      messages.push(msg);
    } else if (role === 'user') {
      // Check for tool_result blocks
      const toolResults = content.filter(b => b.type === 'tool_result');
      if (toolResults.length > 0) {
        for (const tr of toolResults) {
          messages.push({
            role: 'tool',
            tool_call_id: tr.tool_use_id,
            content: typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content)
          });
        }
      } else {
        // Regular user content blocks
        const textParts = content.filter(b => b.type === 'text').map(b => b.text);
        messages.push({ role: 'user', content: textParts.join('') || JSON.stringify(content) });
      }
    } else {
      messages.push({ role, content: JSON.stringify(content) });
    }
  }

  const result = {
    model: request.model,
    messages,
    stream: true
  };

  if (request.max_tokens) result.max_completion_tokens = request.max_tokens;

  // Convert Anthropic tool format to OpenAI function-calling format
  if (request.tools && request.tools.length > 0) {
    result.tools = request.tools.map(t => ({
      type: 'function',
      function: {
        name: t.name,
        description: t.description || '',
        parameters: t.input_schema || { type: 'object', properties: {} }
      }
    }));
  }

  return result;
}

/**
 * Stream from an OpenAI-compatible Chat Completions endpoint.
 * Works with OpenAI, xAI, Google Gemini (OpenAI compat mode).
 *
 * Yields Anthropic-normalized events so the agentic loop can use one format.
 *
 * @param {Object} request - Anthropic-style request (will be converted)
 * @param {Object} opts - { apiKey, baseUrl, authHeader, authPrefix, signal }
 * @yields {Object} Anthropic-normalized SSE events
 */
export async function* streamOpenAI(request, { apiKey, baseUrl, authHeader, authPrefix, signal, extraHeaders } = {}) {
  const openaiReq = toOpenAIRequest(request);
  const url = `${baseUrl || PROVIDERS.openai.baseUrl}/v1/chat/completions`;

  const headers = { 'content-type': 'application/json', ...(extraHeaders || {}) };
  if (authHeader === 'Authorization' || !authHeader) {
    headers['Authorization'] = `${authPrefix || 'Bearer '}${apiKey}`;
  } else {
    headers[authHeader] = apiKey;
  }

  const response = await fetch(url, {
    method: 'POST',
    headers,
    body: JSON.stringify(openaiReq),
    signal
  });

  if (!response.ok) {
    let body = '';
    try { body = await response.text(); } catch {}
    const err = new Error(`API error ${response.status}: ${body.slice(0, 500)}`);
    err.status = response.status;
    throw err;
  }

  // Parse OpenAI SSE and normalize to Anthropic event format
  yield* normalizeOpenAIStream(response.body);
}

/**
 * Parse an OpenAI SSE stream and normalize chunks to Anthropic event format.
 * This allows the agentic loop to use a single event format regardless of provider.
 *
 * @param {ReadableStream} body - Response body stream
 * @yields {Object} Anthropic-normalized events
 */
async function* normalizeOpenAIStream(body) {
  let messageStarted = false;
  // Track active tool calls by index
  const activeToolCalls = new Map();
  let contentBlockIndex = 0;

  for await (const chunk of parseOpenAISSE(body)) {
    if (chunk === '[DONE]') {
      yield { type: 'message_stop' };
      return;
    }

    const choice = chunk.choices?.[0];
    if (!choice) continue;

    // Emit message_start on first chunk
    if (!messageStarted) {
      messageStarted = true;
      yield {
        type: 'message_start',
        message: {
          id: chunk.id || 'msg_openai',
          role: 'assistant',
          content: [],
          usage: chunk.usage || {}
        }
      };
    }

    const delta = choice.delta || {};

    // Text content
    if (delta.content) {
      if (contentBlockIndex === 0 && !activeToolCalls.size) {
        // First text block — emit content_block_start once
        if (contentBlockIndex === 0) {
          yield { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } };
          contentBlockIndex = 1;
        }
      }
      yield { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: delta.content } };
    }

    // Tool calls
    if (delta.tool_calls) {
      for (const tc of delta.tool_calls) {
        const idx = tc.index ?? 0;
        if (!activeToolCalls.has(idx)) {
          // New tool call — emit content_block_start
          // Close text block first if needed
          if (contentBlockIndex === 1) {
            yield { type: 'content_block_stop', index: 0 };
          }
          const blockIdx = contentBlockIndex++;
          activeToolCalls.set(idx, { blockIdx, id: tc.id || `call_${idx}`, name: tc.function?.name || '', args: '' });
          yield {
            type: 'content_block_start',
            index: blockIdx,
            content_block: {
              type: 'tool_use',
              id: tc.id || `call_${idx}`,
              name: tc.function?.name || '',
              input: {}
            }
          };
        }

        const active = activeToolCalls.get(idx);
        if (tc.id) active.id = tc.id;
        if (tc.function?.name) active.name = tc.function.name;
        if (tc.function?.arguments) {
          active.args += tc.function.arguments;
          yield {
            type: 'content_block_delta',
            index: active.blockIdx,
            delta: { type: 'input_json_delta', partial_json: tc.function.arguments }
          };
        }
      }
    }

    // Finish reason
    if (choice.finish_reason) {
      // Close all open blocks
      for (const [, active] of activeToolCalls) {
        yield { type: 'content_block_stop', index: active.blockIdx };
      }
      yield {
        type: 'message_delta',
        delta: { stop_reason: choice.finish_reason === 'tool_calls' ? 'tool_use' : choice.finish_reason },
        usage: chunk.usage || {}
      };
    }
  }
}

/**
 * Parse OpenAI SSE format (data: {...}\n\n or data: [DONE]\n\n).
 *
 * @param {ReadableStream} body
 * @yields {Object|string} Parsed JSON objects or '[DONE]'
 */
async function* parseOpenAISSE(body) {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const data = line.slice(6).trim();
      if (data === '[DONE]') {
        yield '[DONE]';
        continue;
      }
      try {
        yield JSON.parse(data);
      } catch {
        // Non-JSON data line — skip
      }
    }
  }
}

// ── Unified streaming entry point ────────────────────────────

/**
 * Stream messages from any supported provider.
 * Routes to the correct protocol based on provider format.
 *
 * @param {Object} request - Anthropic-style request body (canonical format)
 * @param {Object} opts
 * @param {string} opts.provider - Provider name (anthropic, openai, xai, gemini)
 * @param {string} opts.apiKey - API key
 * @param {string} [opts.baseUrl] - Override base URL
 * @param {AbortSignal} [opts.signal] - Abort signal
 * @yields {Object} Anthropic-format SSE events (normalized for all providers)
 */
export async function* streamMessages(request, opts = {}) {
  const providerName = opts.provider || 'anthropic';
  const providerConfig = resolveProvider(providerName);
  const apiKey = opts.apiKey;
  const baseUrl = opts.baseUrl || providerConfig.baseUrl;

  if (providerConfig.format === 'anthropic') {
    yield* streamAnthropic(request, { apiKey, baseUrl, signal: opts.signal, extraHeaders: opts.extraHeaders });
  } else {
    yield* streamOpenAI(request, {
      apiKey,
      baseUrl,
      authHeader: providerConfig.authHeader,
      authPrefix: providerConfig.authPrefix,
      signal: opts.signal,
      extraHeaders: opts.extraHeaders
    });
  }
}

// ── SSE parsing utilities ────────────────────────────────────

/**
 * Parse Anthropic-format SSE from a ReadableStream.
 *
 * @param {ReadableStream} body
 * @param {boolean} [hasEventPrefix=true] - Whether events have "event:" prefix (Anthropic) or just "data:" (OpenAI)
 * @yields {Object} Parsed SSE events
 */
async function* parseSSEStream(body, hasEventPrefix = true) {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();

    let eventType = null;
    for (const line of lines) {
      if (hasEventPrefix && line.startsWith('event: ')) {
        eventType = line.slice(7).trim();
      } else if (line.startsWith('data: ')) {
        const type = eventType || 'data';
        try {
          const data = JSON.parse(line.slice(6));
          yield { type, ...data };
        } catch {}
        eventType = null;
      } else if (line.trim() === '') {
        eventType = null;
      }
    }
  }
}

/**
 * Parse a raw SSE text blob into event objects.
 * Useful for testing without a live API connection.
 * Supports both Anthropic (event: + data:) and OpenAI (data: only) formats.
 *
 * @param {string} sseText - Raw SSE text
 * @param {Object} [opts]
 * @param {string} [opts.format='anthropic'] - 'anthropic' or 'openai'
 * @returns {Object[]} Array of parsed events
 */
export function parseSSE(sseText, opts = {}) {
  const format = opts.format || 'anthropic';
  const events = [];

  if (format === 'openai') {
    for (const line of sseText.split('\n')) {
      if (!line.startsWith('data: ')) continue;
      const data = line.slice(6).trim();
      if (data === '[DONE]') {
        events.push({ type: 'done' });
        continue;
      }
      try {
        events.push(JSON.parse(data));
      } catch {}
    }
    return events;
  }

  // Anthropic format: event: type\ndata: json
  let eventType = null;
  for (const line of sseText.split('\n')) {
    if (line.startsWith('event: ')) {
      eventType = line.slice(7).trim();
    } else if (line.startsWith('data: ') && eventType) {
      try {
        const data = JSON.parse(line.slice(6));
        events.push({ type: eventType, ...data });
      } catch {}
      eventType = null;
    } else if (line.trim() === '') {
      eventType = null;
    }
  }

  return events;
}
