/**
 * Anthropic Messages-API streaming provider.
 *
 * Different from the existing Claude harness: that one uses the Claude Agent
 * SDK + subscription OAuth, this one talks directly to `api.anthropic.com/v1/messages`
 * with a pay-per-token API key. Lets users bring their own Anthropic credentials
 * through the pi flow instead of (or alongside) the subscription path.
 *
 * Wire shape: SSE with typed events — `message_start`, `content_block_start`,
 * `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`,
 * `ping`, `error`. Each event's `data:` JSON carries a `type` field matching
 * the event name, so we ignore the SSE `event:` line and route off the JSON.
 */
import { log } from '../../../../shared/logger.js';
import type {
  PiStreamRequest,
  PiStreamEvent,
  PiMessage,
  PiContentBlock,
  PiStopReason,
} from './types.js';

/* ── SSE parser (shares the LF/CRLF-tolerant pattern from the other providers) ── */

async function* parseSse(res: Response): AsyncIterable<any> {
  if (!res.body) return;
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      let idx;
      while (
        (idx = (() => {
          const a = buffer.indexOf('\n\n');
          const b = buffer.indexOf('\r\n\r\n');
          if (a < 0) return b;
          if (b < 0) return a;
          return Math.min(a, b);
        })()) !== -1
      ) {
        const isCrlf = buffer.slice(idx, idx + 4) === '\r\n\r\n';
        const raw = buffer.slice(0, idx);
        buffer = buffer.slice(idx + (isCrlf ? 4 : 2));
        const parsed = parseEvent(raw);
        if (parsed !== undefined) yield parsed;
      }
    }
    buffer += decoder.decode();
    if (buffer.trim()) {
      const parsed = parseEvent(buffer);
      if (parsed !== undefined) yield parsed;
    }
  } finally {
    try { reader.releaseLock(); } catch {}
  }
}

function parseEvent(raw: string): any | undefined {
  const lines = raw.split(/\r?\n/);
  const dataLines = lines
    .filter((l) => l.startsWith('data:'))
    .map((l) => l.slice(5).trimStart());
  if (!dataLines.length) return undefined;
  const data = dataLines.join('\n');
  if (!data) return undefined;
  try { return JSON.parse(data); } catch { return undefined; }
}

/* ── Message conversion (pi → Anthropic) ── */

function toAnthropicContent(blocks: PiContentBlock[]): any[] {
  const out: any[] = [];
  for (const b of blocks) {
    if (b.type === 'text') {
      out.push({ type: 'text', text: b.text });
    } else if (b.type === 'image') {
      out.push({
        type: 'image',
        source: { type: 'base64', media_type: b.mediaType, data: b.data },
      });
    } else if (b.type === 'tool_use') {
      out.push({
        type: 'tool_use',
        id: b.id,
        name: b.name,
        input: b.input || {},
      });
    } else if (b.type === 'tool_result') {
      out.push({
        type: 'tool_result',
        tool_use_id: b.toolUseId,
        content: b.content,
        is_error: b.isError || false,
      });
    }
  }
  return out;
}

function toAnthropicMessages(pi: PiMessage[]): any[] {
  return pi
    .filter((m) => m.content.length > 0)
    .map((m) => ({
      role: m.role === 'assistant' ? 'assistant' : 'user',
      content: toAnthropicContent(m.content),
    }))
    .filter((m) => m.content.length > 0);
}

function toAnthropicTools(tools: { name: string; description: string; inputSchema: Record<string, any> }[]) {
  return tools.map((t) => ({
    name: t.name,
    description: t.description,
    input_schema: t.inputSchema,
  }));
}

function mapStopReason(reason?: string): PiStopReason {
  switch (reason) {
    case 'end_turn':      return 'end_turn';
    case 'stop_sequence': return 'end_turn';
    case 'max_tokens':    return 'max_tokens';
    case 'tool_use':      return 'tool_use';
    case 'pause_turn':    return 'end_turn';
    case 'refusal':       return 'error';
    default:              return 'end_turn';
  }
}

/* ── Streaming entry point ── */

interface PartialBlock {
  kind: 'text' | 'tool_use' | 'other';
  text?: string;
  toolUseId?: string;
  toolName?: string;
  toolArgsBuf?: string;
}

export async function* streamAnthropic(req: PiStreamRequest): AsyncIterable<PiStreamEvent> {
  const url = `${req.baseUrl.replace(/\/+$/, '')}/messages`;

  const body: any = {
    model: req.modelId,
    messages: toAnthropicMessages(req.messages),
    max_tokens: req.maxOutputTokens ?? 8192,
    stream: true,
  };
  if (req.systemPrompt?.trim()) body.system = req.systemPrompt;
  if (req.tools && req.tools.length > 0) body.tools = toAnthropicTools(req.tools);

  let res: Response;
  try {
    res = await fetch(url, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'accept': 'text/event-stream',
        'x-api-key': req.apiKey,
        'anthropic-version': '2023-06-01',
      },
      body: JSON.stringify(body),
      signal: req.signal,
    });
  } catch (err: any) {
    yield { type: 'error', error: err?.message || String(err) };
    return;
  }

  if (!res.ok) {
    let detail = '';
    try { detail = await res.text(); } catch {}
    yield { type: 'error', error: `Anthropic ${res.status} ${res.statusText}${detail ? `: ${detail.slice(0, 400)}` : ''}` };
    return;
  }

  // Anthropic streams content blocks by index. Track partial state per index
  // so deltas land on the right block.
  const blocks = new Map<number, PartialBlock>();
  let accumulated = '';
  let lastStop: string | undefined;
  let usage: { inputTokens?: number; outputTokens?: number } | undefined;
  let chunkCount = 0;
  let firstChunkSummary = '';

  try {
    for await (const evt of parseSse(res)) {
      chunkCount++;
      if (chunkCount === 1) {
        try { firstChunkSummary = JSON.stringify(evt).slice(0, 600); } catch {}
      }
      const type = evt?.type;
      switch (type) {
        case 'message_start': {
          const u = evt?.message?.usage;
          if (u) usage = { inputTokens: u.input_tokens, outputTokens: u.output_tokens };
          break;
        }
        case 'content_block_start': {
          const idx = evt?.index ?? 0;
          const block = evt?.content_block || {};
          if (block.type === 'text') {
            blocks.set(idx, { kind: 'text', text: '' });
          } else if (block.type === 'tool_use') {
            blocks.set(idx, {
              kind: 'tool_use',
              toolUseId: block.id,
              toolName: block.name,
              toolArgsBuf: '',
            });
          } else {
            blocks.set(idx, { kind: 'other' });
          }
          break;
        }
        case 'content_block_delta': {
          const idx = evt?.index ?? 0;
          const delta = evt?.delta || {};
          const slot = blocks.get(idx);
          if (!slot) break;
          if (delta.type === 'text_delta' && typeof delta.text === 'string') {
            slot.text = (slot.text || '') + delta.text;
            accumulated += delta.text;
            yield { type: 'text_delta', delta: delta.text };
          } else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
            slot.toolArgsBuf = (slot.toolArgsBuf || '') + delta.partial_json;
          }
          // `thinking_delta` (extended thinking) is ignored for now — the
          // pi harness doesn't surface reasoning to the user yet.
          break;
        }
        case 'content_block_stop': {
          const idx = evt?.index ?? 0;
          const slot = blocks.get(idx);
          if (!slot) break;
          if (slot.kind === 'tool_use' && slot.toolUseId && slot.toolName) {
            let input: any = {};
            if (slot.toolArgsBuf) {
              try { input = JSON.parse(slot.toolArgsBuf); }
              catch { input = { _raw: slot.toolArgsBuf }; }
            }
            yield {
              type: 'tool_use',
              id: slot.toolUseId,
              name: slot.toolName,
              input,
            };
          }
          break;
        }
        case 'message_delta': {
          if (evt?.delta?.stop_reason) lastStop = evt.delta.stop_reason;
          const u = evt?.usage;
          if (u && (u.output_tokens !== undefined || u.input_tokens !== undefined)) {
            usage = {
              inputTokens: u.input_tokens ?? usage?.inputTokens,
              outputTokens: u.output_tokens ?? usage?.outputTokens,
            };
          }
          break;
        }
        case 'error': {
          const msg = evt?.error?.message || evt?.message || 'Unknown error';
          yield { type: 'error', error: `Anthropic stream error: ${msg}` };
          yield { type: 'done', stopReason: 'error', usage };
          return;
        }
        case 'message_stop':
        case 'ping':
        default:
          // ping is keep-alive; message_stop is bookkeeping.
          break;
      }
    }
  } catch (err: any) {
    if (err?.name === 'AbortError') {
      yield { type: 'done', stopReason: 'aborted' };
      return;
    }
    yield { type: 'error', error: err?.message || String(err) };
    return;
  }

  const hadToolUse = Array.from(blocks.values()).some((b) => b.kind === 'tool_use');

  log.info(
    `[pi/anthropic] stream done — chunks=${chunkCount} text=${accumulated.length} ` +
    `toolCalls=${Array.from(blocks.values()).filter((b) => b.kind === 'tool_use').length} ` +
    `stopReason=${lastStop || 'none'} ` +
    `promptTok=${usage?.inputTokens ?? '?'} outTok=${usage?.outputTokens ?? '?'}`,
  );
  if (chunkCount === 0) {
    log.warn(`[pi/anthropic] zero chunks parsed — content-type=${res.headers.get('content-type') || '?'}`);
  } else if (!accumulated && !hadToolUse) {
    log.info(`[pi/anthropic] first chunk (truncated): ${firstChunkSummary}`);
  }

  if (accumulated) yield { type: 'text_end', text: accumulated };

  if (!accumulated && !hadToolUse) {
    yield {
      type: 'error',
      error: `Anthropic returned no output (stopReason=${lastStop || 'unknown'}).`,
    };
    yield { type: 'done', stopReason: 'error', usage };
    return;
  }

  yield {
    type: 'done',
    stopReason: hadToolUse ? 'tool_use' : mapStopReason(lastStop),
    usage,
  };
}
