/**
 * OpenAI-passthrough adapter.
 *
 * Covers any provider that already speaks OpenAI chat/completions on the
 * wire: `openai`, `vllm`, `deepseek`, `ollama` (with their openai-compatible
 * endpoint enabled). The adapter forwards the request body verbatim and
 * streams the response straight through — no wire translation.
 *
 * Defaults when `url` is empty:
 *   - openai → https://api.openai.com
 *   - deepseek → https://api.deepseek.com
 *   - vllm/ollama → must be configured; these have no canonical public URL.
 */
import type { LlmAdapter, InferContext, NonStreamingResult, StreamingChunk, AdapterDeps, VerifyAuthResult } from '../types.js';

const DEFAULT_URLS: Record<string, string> = {
  openai: 'https://api.openai.com',
  deepseek: 'https://api.deepseek.com',
};

export class OpenAiPassthroughAdapter implements LlmAdapter {
  readonly kind: string;
  private readonly fetchImpl: typeof globalThis.fetch;

  constructor(kind: 'openai' | 'vllm' | 'deepseek' | 'ollama', deps: AdapterDeps = {}) {
    this.kind = kind;
    this.fetchImpl = deps.fetch ?? globalThis.fetch;
  }

  async infer(ctx: InferContext): Promise<NonStreamingResult> {
    const url = this.endpointUrl(ctx.url);
    const body = this.prepareBody(ctx, false);
    const res = await this.fetchImpl(`${url}/v1/chat/completions`, {
      method: 'POST',
      headers: this.headers(ctx),
      body: JSON.stringify(body),
    });
    const json = await res.json() as unknown;
    return { status: res.status, body: json };
  }

  async *stream(ctx: InferContext): AsyncGenerator<StreamingChunk> {
    const url = this.endpointUrl(ctx.url);
    const body = this.prepareBody(ctx, true);
    const res = await this.fetchImpl(`${url}/v1/chat/completions`, {
      method: 'POST',
      headers: this.headers(ctx),
      body: JSON.stringify(body),
    });
    if (!res.ok || res.body === null) {
      const text = await res.text().catch(() => '');
      throw new Error(`${this.kind} stream: HTTP ${String(res.status)} ${text}`);
    }

    // Re-frame the provider's SSE stream into our `StreamingChunk` shape.
    // OpenAI-compat providers already emit `data: {...}` + `data: [DONE]` —
    // we just unwrap the `data: ` prefix, forward payloads, and emit a
    // single terminal `done` chunk so the consumer always gets one.
    const decoder = new TextDecoder();
    let buf = '';
    const reader = res.body.getReader();
    try {
      // eslint-disable-next-line no-constant-condition
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        buf += decoder.decode(value, { stream: true });
        let idx: number;
        while ((idx = buf.indexOf('\n\n')) !== -1) {
          const event = buf.slice(0, idx);
          buf = buf.slice(idx + 2);
          for (const line of event.split('\n')) {
            if (!line.startsWith('data:')) continue;
            const payload = line.slice(5).trim();
            if (payload === '') continue;
            if (payload === '[DONE]') {
              yield { data: '[DONE]', done: true };
              return;
            }
            yield { data: payload };
          }
        }
      }
    } finally {
      reader.releaseLock();
    }
    // Provider closed without emitting [DONE] — give the consumer a clean end.
    yield { data: '[DONE]', done: true };
  }

  /**
   * Probe `GET <url>/v1/models` with the configured auth header. OpenAI,
   * vLLM, LiteLLM, DeepSeek, Ollama (in openai-compat mode) all expose this
   * endpoint and it's gated by the same auth as chat/completions. Cheap (no
   * generation), idempotent, and the response shape is a stable
   * `{ data: [...] }` array.
   */
  async verifyAuth(ctx: InferContext): Promise<VerifyAuthResult> {
    let url: string;
    try {
      url = this.endpointUrl(ctx.url);
    } catch (err) {
      return { ok: false, reason: 'unexpected', status: 0, body: (err as Error).message };
    }
    let res: Response;
    try {
      res = await this.fetchImpl(`${url}/v1/models`, {
        method: 'GET',
        headers: this.headers(ctx),
      });
    } catch (err) {
      return { ok: false, reason: 'unreachable', error: (err as Error).message };
    }
    if (res.ok) return { ok: true };
    const body = await res.text().catch(() => '');
    if (res.status === 401 || res.status === 403) {
      return { ok: false, reason: 'auth', status: res.status, body };
    }
    // Some providers don't expose /v1/models (e.g. a stripped LiteLLM proxy).
    // 404 + non-OAI providers shouldn't hard-block registration — caller
    // treats `unexpected` as a warning, not a failure.
    return { ok: false, reason: 'unexpected', status: res.status, body };
  }

  private endpointUrl(url: string): string {
    // Accept both conventional forms users actually paste — base host
    // (`https://api.openai.com`) and base + version (`https://api.openai.com/v1`).
    // Every OpenAI-compat provider documents their endpoint with the `/v1`
    // suffix, so users naturally include it; the adapter then re-appends
    // `/v1/chat/completions`, producing a doubled-`/v1` 404 against LiteLLM
    // and others. Strip a trailing `/v1` (with or without slash) so both
    // shapes resolve to the same canonical base. A more specific suffix
    // like `/v1beta` is preserved.
    if (url !== '') return url.replace(/\/+$/, '').replace(/\/v1$/, '');
    const def = DEFAULT_URLS[this.kind];
    if (def === undefined) {
      throw new Error(`${this.kind}: url is required (no default endpoint for this provider)`);
    }
    return def;
  }

  private headers(ctx: InferContext): Record<string, string> {
    const headers: Record<string, string> = { 'Content-Type': 'application/json' };
    if (ctx.apiKey !== '') headers['Authorization'] = `Bearer ${ctx.apiKey}`;
    return headers;
  }

  private prepareBody(ctx: InferContext, stream: boolean): Record<string, unknown> {
    const out: Record<string, unknown> = { ...ctx.body };
    if (out.model === undefined || out.model === '') out.model = ctx.modelOverride;
    out.stream = stream;
    return out;
  }
}
