/**
 * Groq LLM provider for Patter's pipeline mode.
 *
 * Groq exposes an OpenAI-compatible Chat Completions API. We reuse the
 * streaming code path by implementing the same SSE parser as
 * ``OpenAILLMProvider`` but pointed at ``api.groq.com``. Defaults to
 * ``llama-3.3-70b-versatile``.
 */

import type { LLMChunk, LLMProvider, LLMStreamOptions } from "../llm-loop";
import {
  mergeAbortSignals,
  createStreamIdleWatchdog,
  LLM_STREAM_IDLE_TIMEOUT_MS,
} from "../llm-loop";
import { getLogger } from '../logger';
import { VERSION } from '../version';
import { PatterConnectionError } from '../errors';

const GROQ_BASE_URL = 'https://api.groq.com/openai/v1';

/** Known Groq Chat Completions models. Availability depends on account tier. */
export const GroqModel = {
  LLAMA_3_3_70B_VERSATILE: 'llama-3.3-70b-versatile',
  LLAMA_3_1_8B_INSTANT: 'llama-3.1-8b-instant',
  LLAMA_3_3_70B_SPECDEC: 'llama-3.3-70b-specdec',
  LLAMA_3_70B: 'llama3-70b-8192',
  LLAMA_3_8B: 'llama3-8b-8192',
  MIXTRAL_8X7B: 'mixtral-8x7b-32768',
  GEMMA2_9B: 'gemma2-9b-it',
} as const;
/** Union of {@link GroqModel} string values. */
export type GroqModel = (typeof GroqModel)[keyof typeof GroqModel];

const DEFAULT_MODEL: string = GroqModel.LLAMA_3_3_70B_VERSATILE;

/** Constructor options for {@link GroqLLMProvider}. */
export interface GroqLLMOptions {
  apiKey: string;
  model?: string;
  baseUrl?: string;
  /** Sampling temperature [0, 2]. */
  temperature?: number;
  /** Max tokens in the assistant response (sent as ``max_completion_tokens``). */
  maxTokens?: number;
  /** OpenAI-style ``response_format`` for JSON mode / structured outputs. */
  responseFormat?: Record<string, unknown>;
  /** Whether to allow parallel tool calls. */
  parallelToolCalls?: boolean;
  /** ``"auto" | "none" | "required"`` or a specific tool object. */
  toolChoice?: string | Record<string, unknown>;
  /** Sampling seed. */
  seed?: number;
  /** Nucleus sampling cutoff in [0, 1]. */
  topP?: number;
  /** Penalty in [-2, 2] applied to repeated tokens. */
  frequencyPenalty?: number;
  /** Penalty in [-2, 2] applied to seen tokens. */
  presencePenalty?: number;
  /** Stop sequence(s). */
  stop?: string | string[];
}

/** LLM provider backed by Groq's OpenAI-compatible Chat Completions API. */
export class GroqLLMProvider implements LLMProvider {
  /** Stable pricing/dashboard key — read by stream-handler/metrics. */
  static readonly providerKey = 'groq';
  private readonly apiKey: string;
  readonly model: string;
  private readonly baseUrl: string;
  private readonly temperature?: number;
  private readonly maxTokens?: number;
  private readonly responseFormat?: Record<string, unknown>;
  private readonly parallelToolCalls?: boolean;
  private readonly toolChoice?: string | Record<string, unknown>;
  private readonly seed?: number;
  private readonly topP?: number;
  private readonly frequencyPenalty?: number;
  private readonly presencePenalty?: number;
  private readonly stop?: string | string[];

  constructor(options: GroqLLMOptions) {
    if (!options.apiKey) {
      throw new Error(
        'Groq API key is required. Pass it via { apiKey } or read GROQ_API_KEY from the environment.',
      );
    }
    this.apiKey = options.apiKey;
    this.model = options.model ?? DEFAULT_MODEL;
    this.baseUrl = options.baseUrl ?? GROQ_BASE_URL;
    this.temperature = options.temperature;
    this.maxTokens = options.maxTokens;
    this.responseFormat = options.responseFormat;
    this.parallelToolCalls = options.parallelToolCalls;
    this.toolChoice = options.toolChoice;
    this.seed = options.seed;
    this.topP = options.topP;
    this.frequencyPenalty = options.frequencyPenalty;
    this.presencePenalty = options.presencePenalty;
    this.stop = options.stop;
  }

  /**
   * Pre-call DNS / TLS warmup for the Groq inference endpoint.
   * Best-effort: 5 s timeout, all exceptions swallowed at debug level.
   */
  async warmup(): Promise<void> {
    try {
      await fetch(`${this.baseUrl}/models`, {
        method: 'GET',
        headers: { Authorization: `Bearer ${this.apiKey}` },
        signal: AbortSignal.timeout(5_000),
      });
    } catch (err) {
      getLogger().debug(`Groq LLM warmup failed (best-effort): ${String(err)}`);
    }
  }

  /** Stream Patter-format LLM chunks from the Groq chat completions API. */
  async *stream(
    messages: Array<Record<string, unknown>>,
    tools?: Array<Record<string, unknown>> | null,
    opts?: LLMStreamOptions,
  ): AsyncGenerator<LLMChunk, void, unknown> {
    const body: Record<string, unknown> = {
      model: this.model,
      messages,
      stream: true,
      stream_options: { include_usage: true },
    };
    if (this.temperature !== undefined) body.temperature = this.temperature;
    if (this.maxTokens !== undefined) {
      // Groq accepts both, but ``max_completion_tokens`` is the modern OpenAI
      // spec name and matches Cerebras/OpenAI parity.
      body.max_completion_tokens = this.maxTokens;
    }
    if (this.responseFormat !== undefined) body.response_format = this.responseFormat;
    if (this.parallelToolCalls !== undefined) body.parallel_tool_calls = this.parallelToolCalls;
    if (this.toolChoice !== undefined) body.tool_choice = this.toolChoice;
    if (this.seed !== undefined) body.seed = this.seed;
    if (this.topP !== undefined) body.top_p = this.topP;
    if (this.frequencyPenalty !== undefined) body.frequency_penalty = this.frequencyPenalty;
    if (this.presencePenalty !== undefined) body.presence_penalty = this.presencePenalty;
    if (this.stop !== undefined) body.stop = this.stop;
    if (tools) body.tools = tools;

    // Idle watchdog (re-armed per chunk) instead of a whole-stream ceiling —
    // see llm-loop.ts createStreamIdleWatchdog.
    const idle = createStreamIdleWatchdog();
    const response = await fetch(`${this.baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.apiKey}`,
        'User-Agent': `getpatter/${VERSION}`,
      },
      body: JSON.stringify(body),
      signal: mergeAbortSignals(opts?.signal, idle.signal),
    });

    if (!response.ok) {
      const errText = await response.text();
      // Cap the logged/thrown body — provider 401 bodies have been observed to
      // embed the rejected API-key prefix, which would otherwise land in logs.
      getLogger().error(`Groq API error: ${response.status} ${errText.slice(0, 200)}`);
      // Throw (don't return silently) so the LLM fallback chain fails over and
      // the spoken error fallback can fire — a silent return looks like success.
      throw new PatterConnectionError(
        `Groq API returned ${response.status}: ${errText.slice(0, 200)}`,
      );
    }

    try {
      yield* parseOpenAISseStream(response, idle.touch);
    } catch (err) {
      if (idle.fired && !opts?.signal?.aborted) {
        throw new PatterConnectionError(
          `Groq stream idle timeout — no data for ${LLM_STREAM_IDLE_TIMEOUT_MS / 1000}s`,
        );
      }
      throw err;
    } finally {
      idle.clear();
    }
  }
}

// ---------------------------------------------------------------------------
// Shared OpenAI-format SSE stream parser
// ---------------------------------------------------------------------------

/**
 * Parse a streaming OpenAI-format Chat Completions response and yield
 * Patter ``LLMChunk`` objects.
 *
 * Exported so ``cerebras-llm.ts`` can reuse the same parser.
 */
export async function* parseOpenAISseStream(
  response: Response,
  onRead?: () => void,
): AsyncGenerator<LLMChunk, void, unknown> {
  const reader = response.body?.getReader();
  if (!reader) return;

  const decoder = new TextDecoder();
  let buffer = '';

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

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

      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || !trimmed.startsWith('data: ')) continue;
        const data = trimmed.slice(6);
        if (data === '[DONE]') continue;

        let chunk: {
          choices?: Array<{
            delta?: {
              content?: string;
              tool_calls?: Array<{
                index: number;
                id?: string;
                function?: { name?: string; arguments?: string };
              }>;
            };
          }>;
          usage?: {
            prompt_tokens?: number;
            completion_tokens?: number;
            prompt_tokens_details?: { cached_tokens?: number };
          };
          // Some Groq deployments return ``x_groq.usage`` in the final chunk.
          x_groq?: {
            usage?: {
              prompt_tokens?: number;
              completion_tokens?: number;
            };
          };
        };
        try {
          chunk = JSON.parse(data);
        } catch {
          continue;
        }

        // Final chunk with usage (choices=[]). Forward for cost attribution.
        const usage = chunk.usage ?? chunk.x_groq?.usage;
        if (usage) {
          const cached = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
          // ``prompt_tokens`` includes the cached portion — subtract so cached
          // tokens aren't billed at both the input AND cache-read rate
          // (mirrors OpenAILLMProvider and the Python providers).
          yield {
            type: 'usage',
            inputTokens: Math.max(0, (usage.prompt_tokens ?? 0) - cached),
            outputTokens: usage.completion_tokens,
            cacheReadInputTokens: cached,
          };
        }

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

        if (delta.content) {
          yield { type: 'text', content: delta.content };
        }

        if (delta.tool_calls) {
          for (const tc of delta.tool_calls) {
            yield {
              type: 'tool_call',
              index: tc.index,
              id: tc.id,
              name: tc.function?.name,
              arguments: tc.function?.arguments,
            };
          }
        }
      }
    }
  } finally {
    reader.cancel().catch(() => {});
  }
}
