/**
 * OpenAI-compatible LlmProvider — fetch-based, works with OpenAI, VibeFlare, Azure, etc.
 */

import type { AiCredentials } from '@/server/ai/credentials';
import { assertSafeFetchUrl, UnsafeFetchUrlError } from '@/server/security/safe-fetch-url';
import type { ImageRequest, LlmProvider, NormalizedTextResult, TextRequest } from './types';
import {
  ProviderAuthError,
  ProviderFatalError,
  ProviderRateLimitError,
  ProviderTransientError,
} from './types';

const DEFAULT_TIMEOUT_MS = 60_000;
const MAX_TOKENS = 2048;

interface ChatCompletionResponse {
  choices?: Array<{ message?: { content?: string | null } }>;
  usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number };
  error?: { message?: string };
}

function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
  return new Promise((resolve, reject) => {
    const t = setTimeout(() => reject(new ProviderTransientError(`timeout after ${ms}ms`)), ms);
    p.then(
      (v) => {
        clearTimeout(t);
        resolve(v);
      },
      (e) => {
        clearTimeout(t);
        reject(e);
      },
    );
  });
}

function normalize(_model: string, raw: ChatCompletionResponse): NormalizedTextResult {
  const promptTokens = raw.usage?.prompt_tokens ?? 0;
  const completionTokens = raw.usage?.completion_tokens ?? 0;
  const totalTokens = raw.usage?.total_tokens ?? promptTokens + completionTokens;

  return {
    text: raw.choices?.[0]?.message?.content?.trim() ?? '',
    model: _model,
    usage: { promptTokens, completionTokens, totalTokens },
    costUsd: null,
    rawResponse: raw,
  };
}

function mapHttpError(status: number, msg: string): never {
  if (status === 429) throw new ProviderRateLimitError(msg);
  if (status === 401 || status === 403) throw new ProviderAuthError(msg);
  if (status >= 500) throw new ProviderTransientError(msg);
  throw new ProviderFatalError(msg);
}

async function callChatCompletions(
  baseUrl: string,
  creds: AiCredentials,
  req: TextRequest,
): Promise<NormalizedTextResult> {
  try {
    try {
      assertSafeFetchUrl(baseUrl);
    } catch (err) {
      if (err instanceof UnsafeFetchUrlError) {
        throw new Error('invalid provider base URL', { cause: err });
      }
      throw err;
    }
    const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;

    const response = await withTimeout(
      fetch(url, {
        method: 'POST',
        redirect: 'manual',
        headers: {
          Authorization: `Bearer ${creds.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: req.model,
          messages: [{ role: 'user', content: req.prompt }],
          max_tokens: MAX_TOKENS,
        }),
      }),
      DEFAULT_TIMEOUT_MS,
    );

    if (response.status >= 300 && response.status < 400) {
      mapHttpError(response.status, 'provider returned a redirect');
    }

    const raw = (await response.json()) as ChatCompletionResponse;

    if (!response.ok) {
      const msg = raw.error?.message ?? `HTTP ${response.status}`;
      mapHttpError(response.status, msg);
    }

    return normalize(req.model, raw);
  } catch (err) {
    if (
      err instanceof ProviderRateLimitError ||
      err instanceof ProviderAuthError ||
      err instanceof ProviderFatalError ||
      err instanceof ProviderTransientError
    ) {
      throw err;
    }
    const msg = err instanceof Error ? err.message : String(err);
    throw new ProviderTransientError(msg);
  }
}

export function createOpenAICompatProvider(baseUrl: string): LlmProvider {
  return {
    id: 'openai',
    supportsImage: false,
    generateText: (creds, req) => callChatCompletions(baseUrl, creds, req),
    generateTextWithImage: async (_creds: AiCredentials, _req: ImageRequest) => {
      throw new ProviderFatalError('openai-compat provider does not support image input');
    },
  };
}

export const openaiProvider: LlmProvider = createOpenAICompatProvider('https://api.openai.com/v1');
