'use strict';

const { toOpenAI, messagesToOpenAI, responseFromOpenAI } = require('./tool-adapter');
const { filterToSupportedModels, supportedModelsForProvider } = require('./supported-models');
const { isPortkeyProviderConfig } = require('./portkey');

// CTM only exposes the OpenAI models it actually routes/evaluates. The
// provider /v1/models endpoint includes legacy, audio, realtime, search, and
// product-internal aliases that are not valid Wall-E chat/coding choices.
const FALLBACK_MODELS = supportedModelsForProvider('openai');

function isPlaceholderApiKey(key) {
  return !key || /^(test|test-key|dummy|fake|example)(-|$)/i.test(key) || /^sk-test/i.test(key);
}

function isDefaultOpenAIBaseUrl(baseUrl) {
  if (!baseUrl) return true;
  return /^https:\/\/api\.openai\.com(?:\/v1)?\/?$/i.test(String(baseUrl).trim());
}

/**
 * Create an OpenAI-compatible LLM provider.
 * @param {{ apiKey: string, baseUrl?: string, customHeaders?: object }} config
 */
function createOpenAIProvider(config = {}) {
  const { apiKey, baseUrl, customHeaders, allowUnknownModels } = config;
  const allowRawModelList = Boolean(allowUnknownModels || (baseUrl && !isDefaultOpenAIBaseUrl(baseUrl)) || isPortkeyProviderConfig({ baseUrl, customHeaders }));

  let _client;
  function client() {
    if (!_client) {
      // Lazy-load to avoid import cost when provider isn't used
      const OpenAI = require('openai');
      _client = new OpenAI({
        apiKey,
        ...(baseUrl ? { baseURL: baseUrl } : {}),
        ...(customHeaders && typeof customHeaders === 'object' && Object.keys(customHeaders).length > 0
          ? { defaultHeaders: customHeaders }
          : {}),
      });
    }
    return _client;
  }

  return {
    type: 'openai',

    /**
     * Send a chat completion request.
     * @param {{ model: string, messages: Array, tools?: Array, system?: string|object|Array, maxTokens?: number, temperature?: number, signal?: AbortSignal }} opts
     */
    async chat({ model, messages, tools, system, maxTokens, temperature, signal } = {}) {
      const t0 = Date.now();

      // Build messages array — prepend system as first message
      const converted = messagesToOpenAI(messages || []);
      if (system) {
        const systemText =
          typeof system === 'string' ? system : JSON.stringify(system);
        converted.unshift({ role: 'system', content: systemText });
      }

      const params = {
        model,
        messages: converted,
      };

      if (tools && tools.length > 0) {
        params.tools = toOpenAI(tools);
      }
      if (maxTokens != null) {
        params.max_completion_tokens = maxTokens;
      }
      if (temperature != null) {
        params.temperature = temperature;
      }

      const raw = await client().chat.completions.create(params, {
        ...(signal ? { signal } : {}),
      });

      const latencyMs = Date.now() - t0;
      const normalized = responseFromOpenAI(raw);

      // Map OpenAI finish_reason to our standard stopReason
      let stopReason = 'end_turn';
      if (normalized.stopReason === 'tool_calls') {
        stopReason = 'tool_use';
      } else if (normalized.stopReason === 'stop') {
        stopReason = 'end_turn';
      } else if (normalized.stopReason) {
        stopReason = normalized.stopReason;
      }

      return {
        content: normalized.content,
        toolCalls: normalized.toolCalls || [],
        stopReason,
        usage: normalized.usage || { input: 0, output: 0 },
        latencyMs,
        model: raw.model || model,
        provider: 'openai',
        raw,
      };
    },

    /**
     * List supported OpenAI models. Direct OpenAI remains constrained to CTM's
     * supported catalog; Portkey/OpenAI-compatible gateways may surface models
     * that CTM does not know yet, so callers can import raw gateway ids.
     */
    async listModels(options = {}) {
      const exact = Boolean(options.exact || options.liveOnly);
      const includeUnknown = Boolean(
        options.includeUnknown
        || options.allowUnknown
        || allowRawModelList
      );
      // Skip the API call entirely when the key is a known dummy placeholder
      // (e.g. devbox 'sk-ant-api03-unused'). Avoids 401s and unnecessary
      // network round-trips on first boot before the user configures keys.
      if (isPlaceholderApiKey(apiKey)) return exact || includeUnknown ? [] : FALLBACK_MODELS;
      try {
        const response = await client().models.list();
        const raw = [];
        const seen = new Set();
        for await (const m of response) {
          const id = m.id;
          if (seen.has(id)) continue;
          seen.add(id);
          raw.push({ id });
        }
        if (includeUnknown) {
          const supportedById = new Map(supportedModelsForProvider('openai').map((model) => [model.id, model]));
          return raw.map((model) => supportedById.get(model.id) || {
            id: model.id,
            name: model.id,
            capabilities: ['chat'],
          });
        }
        const models = filterToSupportedModels('openai', raw);
        return models.length > 0 ? models : supportedModelsForProvider('openai');
      } catch (err) {
        if (exact) throw err;
        if (includeUnknown) throw err;
        console.warn('[openai] Failed to list models from API, using fallback:', err.message);
        return supportedModelsForProvider('openai');
      }
    },
  };
}

module.exports = { createOpenAIProvider };
