/**
 * OpenAI — API key from platform_settings first, then OPENAI_API_KEY env.
 */
import { getPlatformSettings } from './platformSettingsService.js';

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

let credsCache: { key: string; base: string; at: number } | null = null;
const CACHE_TTL_MS = 60_000;

/** Call after updating platform_settings so the next request picks up new credentials. */
export function invalidateOpenAiCredentialsCache(): void {
  credsCache = null;
}

/** Returns bearer key + base URL, or null if no key is available. */
export async function resolveOpenAiCredentials(): Promise<{ key: string; base: string } | null> {
  const now = Date.now();
  if (credsCache && now - credsCache.at < CACHE_TTL_MS) {
    return credsCache.key ? { key: credsCache.key, base: credsCache.base } : null;
  }

  const row = await getPlatformSettings();
  const dbKey = row?.openai_api_key?.trim() ?? '';
  const envKey = process.env.OPENAI_API_KEY?.trim() ?? '';
  const key = dbKey || envKey;
  const baseRaw = row?.openai_base_url?.trim() || process.env.OPENAI_BASE_URL?.trim() || DEFAULT_BASE;
  const base = baseRaw.replace(/\/$/, '');
  credsCache = { key, base, at: now };
  return key ? { key, base } : null;
}

export async function isOpenAiConfigured(): Promise<boolean> {
  const c = await resolveOpenAiCredentials();
  return Boolean(c?.key);
}

/** Model ids suitable for chat completions (filters obvious non-chat endpoints). */
export async function listOpenAiModels(): Promise<string[]> {
  const c = await resolveOpenAiCredentials();
  if (!c?.key) throw new Error('No OpenAI API key configured (Settings or OPENAI_API_KEY)');
  return listOpenAiModelsWithCredentials(c.base, c.key);
}

/** List chat models using explicit base URL and API key (agent / preview). */
export async function listOpenAiModelsWithCredentials(baseRaw: string, apiKey: string): Promise<string[]> {
  const base = baseRaw.replace(/\/$/, '');
  const res = await fetch(`${base}/models`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  const raw = await res.text();
  if (!res.ok) throw new Error(`OpenAI models error ${res.status}: ${raw.slice(0, 400)}`);

  const data = JSON.parse(raw) as { data?: Array<{ id: string }> };
  const ids = (data.data ?? []).map(m => m.id).filter(isLikelyChatModel);
  return [...new Set(ids)].sort();
}

export function isLikelyChatModel(id: string): boolean {
  const lower = id.toLowerCase();
  if (
    lower.includes('embedding') ||
    lower.includes('moderation') ||
    lower.includes('whisper') ||
    lower.includes('tts') ||
    lower.includes('dall-e') ||
    lower.includes('realtime') ||
    lower.includes('audio') ||
    lower.includes('transcribe') ||
    lower.includes('search')
  ) {
    return false;
  }
  return (
    id.startsWith('gpt-') ||
    /^o[0-9]/.test(id) ||
    id.startsWith('chatgpt-') ||
    id.startsWith('ft:') // fine-tuned chat models
  );
}

/** OpenAI chat content — string or multimodal parts (vision). */
export type OpenAiChatContentPart =
  | { type: 'text'; text: string }
  | { type: 'image_url'; image_url: { url: string } };

export type OpenAiChatMessage = {
  role: 'user' | 'assistant';
  content: string | OpenAiChatContentPart[];
};

export async function chatCompletionText(opts: {
  model: string;
  system: string;
  messages: OpenAiChatMessage[];
  maxTokens?: number;
  responseFormat?: 'json_object';
}): Promise<string> {
  const c = await resolveOpenAiCredentials();
  if (!c?.key) throw new Error('OPENAI_API_KEY is not set');
  return chatCompletionTextWithCredentials({
    base: c.base,
    apiKey: c.key,
    model: opts.model,
    system: opts.system,
    messages: opts.messages,
    maxTokens: opts.maxTokens,
    responseFormat: opts.responseFormat,
  });
}

/** OpenAI-compatible chat completions (official OpenAI or proxies). */
export async function chatCompletionTextWithCredentials(opts: {
  base: string;
  apiKey: string;
  model: string;
  system: string;
  messages: OpenAiChatMessage[];
  maxTokens?: number;
  responseFormat?: 'json_object';
  /** Extra request headers (e.g. OpenRouter HTTP-Referer / X-Title). */
  extraHeaders?: Record<string, string>;
  /** Label used in error messages (defaults to OpenAI). */
  providerLabel?: string;
}): Promise<string> {
  const base = opts.base.replace(/\/$/, '');
  const label = opts.providerLabel ?? 'OpenAI';
  const body: Record<string, unknown> = {
    model: opts.model,
    messages: [{ role: 'system', content: opts.system }, ...opts.messages],
    max_tokens: opts.maxTokens ?? 1024,
  };
  if (opts.responseFormat === 'json_object') {
    body.response_format = { type: 'json_object' };
  }
  const res = await fetch(`${base}/chat/completions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${opts.apiKey}`,
      'Content-Type': 'application/json',
      ...opts.extraHeaders,
    },
    body: JSON.stringify(body),
  });

  const raw = await res.text();
  if (!res.ok) throw new Error(`${label} error ${res.status}: ${raw.slice(0, 500)}`);

  const data = JSON.parse(raw) as {
    choices?: Array<{ message?: { content?: string | null } }>;
  };
  const text = data.choices?.[0]?.message?.content?.trim();
  if (!text) throw new Error(`${label} returned empty content`);
  return text;
}

const DEFAULT_WHISPER_MODEL = process.env.WHISPER_MODEL?.trim() || 'whisper-1';
/** OpenAI Whisper upload limit (25 MB). */
export const WHISPER_MAX_BYTES = Number(process.env.WHISPER_MAX_BYTES ?? String(25 * 1024 * 1024));

function whisperFilename(mime: string): string {
  const m = mime.split(';')[0].trim().toLowerCase();
  if (m.includes('ogg')) return 'voice.ogg';
  if (m.includes('mpeg') || m.includes('mp3')) return 'voice.mp3';
  if (m.includes('mp4') || m.includes('m4a')) return 'voice.m4a';
  if (m.includes('webm')) return 'voice.webm';
  return 'voice.bin';
}

/** Speech-to-text via OpenAI Whisper (or compatible /audio/transcriptions endpoint). */
export async function transcribeAudioWithCredentials(opts: {
  base: string;
  apiKey: string;
  buffer: Buffer;
  mime: string;
  model?: string;
}): Promise<string> {
  const base = opts.base.replace(/\/$/, '');
  const mime = opts.mime.split(';')[0].trim() || 'application/octet-stream';
  const blob = new Blob([new Uint8Array(opts.buffer)], { type: mime });
  const form = new FormData();
  form.append('file', blob, whisperFilename(mime));
  form.append('model', opts.model ?? DEFAULT_WHISPER_MODEL);

  const res = await fetch(`${base}/audio/transcriptions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${opts.apiKey}` },
    body: form,
  });
  const raw = await res.text();
  if (!res.ok) throw new Error(`Whisper error ${res.status}: ${raw.slice(0, 500)}`);

  const data = JSON.parse(raw) as { text?: string };
  const text = data.text?.trim();
  if (!text) throw new Error('Whisper returned empty transcript');
  return text;
}

/** Transcribe using platform OpenAI credentials (Settings or OPENAI_API_KEY). */
export async function transcribeAudio(buffer: Buffer, mime: string): Promise<string | null> {
  const c = await resolveOpenAiCredentials();
  if (!c?.key) return null;
  if (buffer.length > WHISPER_MAX_BYTES) return null;
  try {
    return await transcribeAudioWithCredentials({
      base: c.base,
      apiKey: c.key,
      buffer,
      mime,
    });
  } catch {
    return null;
  }
}
