import OpenAI from 'openai';
import type { ChatRequest, ChatResponse, ModelClient, MessageContentPart, TokenUsage } from './types.js';
import { getMessageTextWithTools } from './media.js';
import { ensureValidOpenAIAuth } from '../auth/openai.js';

export interface OpenAIClientConfig {
  apiKey?: string;
  model: string;
  maxTokens?: number;
  baseURL?: string;
  timeoutMs?: number;
  /** If true, use ChatGPT subscription OAuth via the Codex backend endpoint. */
  useOAuth?: boolean;
}

/**
 * Convert Flynn message content to OpenAI format.
 * OpenAI uses { type: 'text', text } and { type: 'image_url', image_url: { url } } parts.
 */
function toOpenAIContent(content: string | MessageContentPart[]): string | OpenAI.ChatCompletionContentPart[] {
  if (typeof content === 'string') {
    return content;
  }

  return content.map((part): OpenAI.ChatCompletionContentPart => {
    if (part.type === 'text') {
      return { type: 'text', text: part.text };
    }
    if (part.type === 'image') {
      // OpenAI accepts data URIs or regular URLs
      const url = part.source.type === 'base64'
        ? `data:${part.source.media_type};base64,${part.source.data!}`
        : part.source.url!;
      return { type: 'image_url', image_url: { url } };
    }
    if (part.type === 'audio') {
      // OpenAI native audio input via input_audio content part
      // Determine format from MIME type (OpenAI supports: wav, mp3, flac, opus, ogg, webm)
      const formatMap: Record<string, string> = {
        'audio/wav': 'wav',
        'audio/mpeg': 'mp3',
        'audio/mp3': 'mp3',
        'audio/ogg': 'ogg',
        'audio/webm': 'webm',
        'audio/mp4': 'mp4',
        'audio/x-m4a': 'mp4',
      };
      const format = formatMap[part.source.media_type] ?? 'wav';
      return {
        type: 'input_audio',
        input_audio: { data: part.source.data, format },
      } as unknown as OpenAI.ChatCompletionContentPart;
    }
    // Fallback — shouldn't happen
    return { type: 'text', text: JSON.stringify(part) };
  });
}

export class OpenAIClient implements ModelClient {
  private client?: OpenAI;
  private model: string;
  private defaultMaxTokens: number;
  private useOAuth: boolean;

  constructor(config: OpenAIClientConfig) {
    const timeoutMs = config.timeoutMs ?? 20_000;
    this.useOAuth = Boolean(config.useOAuth);

    // OAuth mode uses a different backend (ChatGPT Codex) and a different API shape.
    // Only initialize the OpenAI SDK for API-key providers.
    if (!this.useOAuth) {
      this.client = new OpenAI({
        apiKey: config.apiKey,
        baseURL: config.baseURL,
        timeout: timeoutMs,
        maxRetries: 0,
      });
    }
    this.model = config.model;
    this.defaultMaxTokens = config.maxTokens ?? 4096;
  }

  private async chatViaOAuthCodex(request: ChatRequest): Promise<ChatResponse> {
    const CODEX_API_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';

    const auth = await ensureValidOpenAIAuth();

    // Codex endpoint requires:
    // - instructions (non-empty)
    // - input must be a list
    // - store must be false
    // - stream must be true (SSE)
    const instructions = (request.system ?? '').trim() || 'You are helpful.';

    const input = request.messages
      .map((m) => {
        const text = getMessageTextWithTools(m);
        if (!text) {return null;}
        const contentType = m.role === 'assistant' ? 'output_text' : 'input_text';
        return {
          role: m.role,
          content: [{ type: contentType, text }],
        };
      })
      .filter((x): x is NonNullable<typeof x> => Boolean(x));

    const body = {
      model: this.model,
      instructions,
      store: false,
      stream: true,
      input,
      // Intentionally omit max_output_tokens: Codex endpoint rejects it.
      // Also omit tools/tool_choice for now.
    };

    const headers: Record<string, string> = {
      'content-type': 'application/json',
      'authorization': `Bearer ${auth.access_token}`,
      'originator': 'flynn',
      'user-agent': 'flynn/0.1',
      'session_id': `flynn-${Date.now()}`,
    };
    if (auth.account_id) {
      headers['ChatGPT-Account-Id'] = auth.account_id;
    }

    const res = await fetch(CODEX_API_ENDPOINT, {
      method: 'POST',
      headers,
      body: JSON.stringify(body),
    });

    if (!res.ok) {
      const text = await res.text();
      throw new Error(`${res.status} ${res.statusText}${text ? `: ${text}` : ''}`);
    }

    if (!res.body) {
      throw new Error('OpenAI OAuth request failed: missing response body');
    }

    let buffer = '';
    let outputText = '';
    let usage: TokenUsage | undefined;

    const reader = res.body.getReader();

    const processBlock = (block: string): void => {
      const lines = block.split('\n');
      let data = '';
      for (const line of lines) {
        if (line.startsWith('data:')) {
          data += line.slice('data:'.length).trim();
        }
      }
      if (!data) {return;}
      let obj: any;
      try {
        obj = JSON.parse(data);
      } catch {
        return;
      }

      if (obj.type === 'response.output_text.delta' && typeof obj.delta === 'string') {
        outputText += obj.delta;
      }

      if (obj.type === 'response.completed') {
        const u = obj.response?.usage;
        if (u) {
          usage = {
            inputTokens: u.input_tokens ?? 0,
            outputTokens: u.output_tokens ?? 0,
          };
        }
      }

      if (obj.type === 'response.failed') {
        const detail = obj.response?.error?.message ?? 'OpenAI OAuth response failed';
        throw new Error(detail);
      }
    };

    while (true) {
      const { value, done } = await reader.read();
      if (done) {break;}
      buffer += Buffer.from(value).toString('utf8');

      while (true) {
        const idx = buffer.indexOf('\n\n');
        if (idx === -1) {break;}
        const block = buffer.slice(0, idx);
        buffer = buffer.slice(idx + 2);
        processBlock(block);
      }
    }

    return {
      content: outputText,
      stopReason: 'end_turn',
      usage: usage ?? { inputTokens: 0, outputTokens: 0 },
    };
  }

  async chat(request: ChatRequest): Promise<ChatResponse> {
    if (this.useOAuth) {
      return this.chatViaOAuthCodex(request);
    }

    if (!this.client) {
      throw new Error('OpenAI client not initialized');
    }

    const messages: OpenAI.ChatCompletionMessageParam[] = [];

    if (request.system) {
      messages.push({ role: 'system', content: request.system });
    }

    for (const msg of request.messages) {
      messages.push({
        role: msg.role,
        content: toOpenAIContent(msg.content),
      } as OpenAI.ChatCompletionMessageParam);
    }

    // Build params, conditionally including tools
    const params: OpenAI.ChatCompletionCreateParamsNonStreaming = {
      model: this.model,
      max_tokens: request.maxTokens ?? this.defaultMaxTokens,
      messages,
    };

    if (request.tools && request.tools.length > 0) {
      params.tools = request.tools.map(t => ({
        type: 'function' as const,
        function: {
          name: t.name,
          description: t.description,
          parameters: t.input_schema as OpenAI.FunctionParameters,
        },
      }));
    }

    // Extended thinking/reasoning mode for o1/o3 models
    if (request.thinking) {
      (params as any).reasoning_effort = 'medium';
    }

    const response = await this.client.chat.completions.create(params);

    const choice = response.choices[0];
    const content = choice?.message?.content ?? '';

    // Parse tool_calls from the response if present
    const toolCalls = choice?.message?.tool_calls?.map((tc: OpenAI.ChatCompletionMessageToolCall) => ({
      id: tc.id,
      name: tc.function.name,
      args: JSON.parse(tc.function.arguments),
    })) ?? [];

    // Map OpenAI finish reasons to Flynn's stop reasons
    let stopReason: string;
    if (toolCalls.length > 0) {
      stopReason = 'tool_use';
    } else {
      const reason = choice?.finish_reason;
      if (reason === 'stop') {
        stopReason = 'end_turn';
      } else if (reason === 'length') {
        stopReason = 'max_tokens';
      } else if (reason === 'tool_calls') {
        // Edge case: finish_reason says tool_calls but none were parsed
        stopReason = 'end_turn';
      } else {
        stopReason = reason ?? 'end_turn';
      }
    }

    return {
      content,
      stopReason,
      usage: {
        inputTokens: response.usage?.prompt_tokens ?? 0,
        outputTokens: response.usage?.completion_tokens ?? 0,
      },
      ...(toolCalls.length > 0 ? { toolCalls } : {}),
    };
  }
}
