import https from 'node:https';
import http from 'node:http';
import type { LlmProvider, CompletionOptions, CompletionResult, ChatMessage, ToolCall } from './types.js';

export interface OpenAiConfig {
  apiKey: string;
  baseUrl?: string;
  defaultModel?: string;
}

interface OpenAiMessage {
  role: string;
  content: string | null;
  tool_calls?: Array<{
    id: string;
    type: 'function';
    function: { name: string; arguments: string };
  }>;
  tool_call_id?: string;
  name?: string;
}

/**
 * OpenAI-compatible provider. Works with OpenAI API, Azure OpenAI,
 * and any service with an OpenAI-compatible chat completions endpoint.
 */
export class OpenAiProvider implements LlmProvider {
  readonly name = 'openai';
  private apiKey: string;
  private baseUrl: string;
  private defaultModel: string;

  constructor(config: OpenAiConfig) {
    this.apiKey = config.apiKey;
    this.baseUrl = (config.baseUrl ?? 'https://api.openai.com').replace(/\/$/, '');
    this.defaultModel = config.defaultModel ?? 'gpt-4o';
  }

  async complete(options: CompletionOptions): Promise<CompletionResult> {
    const model = options.model ?? this.defaultModel;
    const body: Record<string, unknown> = {
      model,
      messages: options.messages.map(toOpenAiMessage),
    };
    if (options.temperature !== undefined) body.temperature = options.temperature;
    if (options.maxTokens !== undefined) body.max_tokens = options.maxTokens;

    if (options.tools && options.tools.length > 0) {
      body.tools = options.tools.map((t) => ({
        type: 'function',
        function: {
          name: t.name,
          description: t.description,
          parameters: t.inputSchema,
        },
      }));
    }

    const response = await this.request('/v1/chat/completions', body, 'POST', options.signal);
    return parseResponse(response);
  }

  async listModels(): Promise<string[]> {
    const response = await this.request('/v1/models', undefined, 'GET');
    const data = response as { data?: Array<{ id: string }> };
    return (data.data ?? []).map((m) => m.id);
  }

  async isAvailable(): Promise<boolean> {
    try {
      await this.listModels();
      return true;
    } catch {
      return false;
    }
  }

  private request(path: string, body: unknown, method = 'POST', signal?: AbortSignal): Promise<unknown> {
    return new Promise((resolve, reject) => {
      if (signal?.aborted === true) {
        reject(new Error('request aborted'));
        return;
      }
      const url = new URL(path, this.baseUrl);
      const isHttps = url.protocol === 'https:';
      const transport = isHttps ? https : http;

      const payload = body !== undefined ? JSON.stringify(body) : undefined;
      const opts = {
        hostname: url.hostname,
        port: url.port || (isHttps ? 443 : 80),
        path: url.pathname,
        method,
        timeout: 120000,
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'Content-Type': 'application/json',
          ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
        },
      };

      const req = transport.request(opts, (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunk: Buffer) => chunks.push(chunk));
        res.on('end', () => {
          const raw = Buffer.concat(chunks).toString('utf-8');
          // Surface transport failures as errors instead of parsing the error
          // body as a completion. A LiteLLM/vLLM HTTP 500 ("Connection error")
          // is valid JSON, so without this check it slips through as an empty
          // completion and gets misreported downstream as "bad JSON" — masking
          // the true backend-down / rate-limit / auth reason.
          const status = res.statusCode ?? 0;
          if (status >= 400) {
            reject(new Error(`OpenAI HTTP ${status}: ${raw.slice(0, 300)}`));
            return;
          }
          try {
            resolve(JSON.parse(raw));
          } catch {
            reject(new Error(`Invalid JSON response from ${path}: ${raw.slice(0, 200)}`));
          }
        });
      });
      req.on('error', reject);
      req.on('timeout', () => {
        req.destroy();
        reject(new Error('Request timed out'));
      });
      signal?.addEventListener('abort', () => {
        req.destroy();
        reject(new Error('request aborted'));
      }, { once: true });
      if (payload) req.write(payload);
      req.end();
    });
  }
}

function toOpenAiMessage(msg: ChatMessage): OpenAiMessage {
  const result: OpenAiMessage = {
    role: msg.role,
    content: msg.content,
  };
  if (msg.toolCallId !== undefined) result.tool_call_id = msg.toolCallId;
  if (msg.name !== undefined) result.name = msg.name;
  return result;
}

function parseResponse(raw: unknown): CompletionResult {
  const data = raw as {
    choices?: Array<{
      message?: {
        content?: string | null;
        // Reasoning ("thinking") models (e.g. qwen3-thinking) emit their answer
        // under reasoning_content with content null when the token budget is
        // tight. Capture it so gate/pagination selection can still find their
        // JSON instead of seeing an empty completion.
        reasoning_content?: string | null;
        provider_specific_fields?: { reasoning_content?: string | null };
        tool_calls?: Array<{
          id: string;
          function: { name: string; arguments: string };
        }>;
      };
      finish_reason?: string;
    }>;
    usage?: {
      prompt_tokens?: number;
      completion_tokens?: number;
      total_tokens?: number;
    };
  };

  const choice = data.choices?.[0];
  const toolCalls: ToolCall[] = (choice?.message?.tool_calls ?? []).map((tc) => ({
    id: tc.id,
    name: tc.function.name,
    arguments: safeParse(tc.function.arguments),
  }));

  const finishReason = choice?.finish_reason === 'tool_calls' ? 'tool_calls' as const
    : choice?.finish_reason === 'length' ? 'length' as const
    : 'stop' as const;

  const message = choice?.message;
  const primaryContent = message?.content ?? '';
  const reasoningContent =
    message?.reasoning_content ?? message?.provider_specific_fields?.reasoning_content ?? '';

  return {
    // Fall back to reasoning_content only when the model produced no content —
    // preserves normal behaviour but rescues thinking-only responses.
    content: primaryContent !== '' ? primaryContent : reasoningContent,
    toolCalls,
    usage: {
      promptTokens: data.usage?.prompt_tokens ?? 0,
      completionTokens: data.usage?.completion_tokens ?? 0,
      totalTokens: data.usage?.total_tokens ?? 0,
    },
    finishReason,
  };
}

function safeParse(json: string): Record<string, unknown> {
  try {
    return JSON.parse(json) as Record<string, unknown>;
  } catch {
    return {};
  }
}
