import type { GenerateChunk, GenerateOpts, LLMProvider } from '../types';

/**
 * Google Gemini: streamed JSON over POST
 *   /v1beta/models/{model}:streamGenerateContent?alt=sse&key=KEY
 *
 * The `alt=sse` flag makes Gemini emit Server-Sent Events with each
 * `data:` line a partial GenerateContentResponse chunk:
 *   { candidates: [ { content: { parts: [ { text: "..." } ] } } ],
 *     usageMetadata: { promptTokenCount, candidatesTokenCount } }
 */
export const gemini: LLMProvider = {
  id: 'gemini',
  label: 'Google Gemini',
  requiresApiKey: true,
  requiresBaseUrl: false,

  async *generate(opts: GenerateOpts): AsyncGenerator<GenerateChunk, void, void> {
    if (!opts.apiKey) {
      yield { type: 'error', message: 'Gemini API key is required.' };
      return;
    }
    const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(
      opts.model,
    )}:streamGenerateContent?alt=sse&key=${encodeURIComponent(opts.apiKey)}`;
    let res: Response;
    try {
      res = await fetch(url, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          systemInstruction: { parts: [{ text: opts.systemPrompt }] },
          contents: [
            { role: 'user', parts: [{ text: opts.userPrompt }] },
          ],
          generationConfig: {
            temperature: 0.7,
            maxOutputTokens: opts.maxOutputTokens ?? 8000,
          },
        }),
        signal: opts.signal,
      });
    } catch (e) {
      yield {
        type: 'error',
        message: `Gemini unreachable: ${(e as Error).message}`,
      };
      return;
    }
    if (!res.ok) {
      yield {
        type: 'error',
        message: `Gemini HTTP ${res.status}: ${(await res.text().catch(() => '')).slice(0, 500)}`,
      };
      return;
    }
    let tokensIn: number | undefined;
    let tokensOut: number | undefined;
    let textEmitted = false;
    let lastFinishReason: string | null = null;
    try {
      // Gemini SSE: same line-delimited "data: ..." frames.
      const { sseLines } = await import('../sse');
      for await (const data of sseLines(res)) {
        let evt: any;
        try {
          evt = JSON.parse(data);
        } catch {
          continue;
        }
        const cand = evt.candidates?.[0];
        const parts = cand?.content?.parts;
        if (Array.isArray(parts)) {
          for (const p of parts) {
            if (p.text) {
              yield { type: 'text', delta: p.text };
              textEmitted = true;
            }
          }
        }
        if (cand?.finishReason) {
          lastFinishReason = cand.finishReason;
        }
        if (evt.usageMetadata) {
          tokensIn = evt.usageMetadata.promptTokenCount;
          tokensOut = evt.usageMetadata.candidatesTokenCount;
        }
      }
      // Surface a useful error when Gemini returned 200 OK but emitted
      // no text — most often a safety/recitation block, or a thinking
      // model that exhausted maxOutputTokens on internal reasoning. The
      // test endpoint relies on this to give the user a real message
      // instead of a generic "empty response".
      if (
        !textEmitted &&
        lastFinishReason &&
        lastFinishReason !== 'STOP'
      ) {
        const friendly = describeFinishReason(lastFinishReason);
        yield { type: 'error', message: `Gemini blocked the response: ${friendly}` };
      }
      yield { type: 'usage', tokensIn, tokensOut };
      yield { type: 'done' };
    } catch (e) {
      yield {
        type: 'error',
        message: `Gemini stream error: ${(e as Error).message}`,
      };
    }
  },
};

function describeFinishReason(reason: string): string {
  switch (reason) {
    case 'SAFETY':
      return 'safety filter (try a flagship model or rephrase the prompt)';
    case 'RECITATION':
      return 'recitation filter';
    case 'MAX_TOKENS':
      return 'hit the output token limit before finishing — raise maxOutputTokens or use a non-thinking model';
    case 'BLOCKLIST':
      return 'blocklist match';
    case 'PROHIBITED_CONTENT':
      return 'prohibited-content filter';
    case 'SPII':
      return 'sensitive-PII filter';
    default:
      return reason;
  }
}
