'use strict';

const { GoogleGenAI } = require('@google/genai');
const { toGemini, responseFromGemini } = require('./tool-adapter');
const { filterToSupportedModels, supportedModelsForProvider } = require('./supported-models');

// ── Gemini CLI OAuth token refresh ──────────────────────────────────
const GEMINI_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
const GEMINI_CLIENT_SECRET = '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';
let _cachedToken = null;
let _tokenExpiresAt = 0;

function providerAbortError() {
  const err = new Error('provider request aborted');
  err.name = 'AbortError';
  return err;
}

function throwIfAborted(signal) {
  if (signal?.aborted) throw providerAbortError();
}

function delayWithAbort(ms, signal) {
  throwIfAborted(signal);
  return new Promise((resolve, reject) => {
    let timer = null;
    const cleanup = () => {
      if (timer) clearTimeout(timer);
      if (signal) signal.removeEventListener('abort', onAbort);
    };
    const onAbort = () => {
      cleanup();
      reject(providerAbortError());
    };
    timer = setTimeout(() => {
      cleanup();
      resolve();
    }, ms);
    if (signal) signal.addEventListener('abort', onAbort, { once: true });
  });
}

async function refreshOAuthToken(refreshToken, signal) {
  if (_cachedToken && Date.now() < _tokenExpiresAt - 60000) return _cachedToken;
  const resp = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: GEMINI_CLIENT_ID, client_secret: GEMINI_CLIENT_SECRET,
      refresh_token: refreshToken, grant_type: 'refresh_token',
    }),
    ...(signal ? { signal } : {}),
  });
  const data = await resp.json();
  if (!data.access_token) throw new Error('Token refresh failed: ' + (data.error_description || data.error));
  _cachedToken = data.access_token;
  _tokenExpiresAt = Date.now() + (data.expires_in || 3600) * 1000;
  return _cachedToken;
}

const GOOGLE_FALLBACK_MODELS = supportedModelsForProvider('google');

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

/**
 * Convert Anthropic-style messages to Gemini contents format.
 * Gemini uses 'model' instead of 'assistant', and structured parts.
 */
function messagesToGemini(messages) {
  const contents = [];
  for (const msg of messages) {
    const role = msg.role === 'assistant' ? 'model' : 'user';

    // Simple string content
    if (typeof msg.content === 'string') {
      contents.push({ role, parts: [{ text: msg.content }] });
      continue;
    }

    // Array content blocks
    if (Array.isArray(msg.content)) {
      const parts = [];
      for (const block of msg.content) {
        if (block.type === 'text') {
          parts.push({ text: block.text });
        } else if (block.type === 'tool_use') {
          parts.push({
            functionCall: { name: block.name, args: block.input },
          });
        } else if (block.type === 'tool_result') {
          // Look up the actual function name from prior tool_use blocks
          let toolName = block.name || block.tool_use_id; // fallback
          for (const prevMsg of messages) {
            if (Array.isArray(prevMsg.content)) {
              for (const b of prevMsg.content) {
                if (b.type === 'tool_use' && b.id === block.tool_use_id) {
                  toolName = b.name;
                  break;
                }
              }
            }
          }
          parts.push({
            functionResponse: {
              name: toolName,
              response: {
                content:
                  typeof block.content === 'string'
                    ? block.content
                    : JSON.stringify(block.content),
              },
            },
          });
        }
      }
      contents.push({ role, parts });
      continue;
    }

    // Fallback
    contents.push({ role, parts: [{ text: String(msg.content) }] });
  }
  return contents;
}

/**
 * Create a Google Gemini LLM provider.
 * @param {{ apiKey: string }} config — a Gemini API key from aistudio.google.com/apikey.
 *   NOTE: Gemini CLI OAuth tokens (ya29.*) use a private Code Assist API
 *   (cloudcode-pa.googleapis.com) with cloud-platform scope — they do NOT work
 *   with the public Gemini API (generativelanguage.googleapis.com) which requires
 *   a different scope (generative-language) or an API key.
 */
function createGoogleProvider(config = {}) {
  const isOAuth = config.authMode === 'oauth';
  const GenAI = config._GoogleGenAI || GoogleGenAI;
  if (!isOAuth && config.apiKey && config.apiKey.startsWith('ya29.')) {
    console.warn('[google] Stored key is a Gemini CLI OAuth token (ya29.*). ' +
      'These tokens use Google\'s private Code Assist API and cannot authenticate ' +
      'with the public Gemini API. Get a Gemini API key from https://aistudio.google.com/apikey');
  }

  function makeOAuthClient(accessToken) {
    const httpOptions = accessToken
      ? { headers: { 'Authorization': `Bearer ${accessToken}` } }
      : undefined;
    return new GenAI({ apiKey: 'unused', httpOptions });
  }

  // Build GenAI client — for OAuth we pass Bearer header instead of apiKey
  let genai;
  if (isOAuth) {
    genai = makeOAuthClient(config.apiKey);
  } else {
    genai = new GenAI({ apiKey: config.apiKey });
  }

  return {
    type: 'google',

    async chat({ model, messages, tools, system, maxTokens, temperature, signal } = {}) {
      const contents = messagesToGemini(messages || []);

      const requestConfig = {
        model: model || 'gemini-2.5-flash',
        contents,
      };

      // Generation config
      const generationConfig = {};
      if (maxTokens != null) generationConfig.maxOutputTokens = maxTokens;
      if (temperature != null) generationConfig.temperature = temperature;
      if (Object.keys(generationConfig).length > 0) {
        requestConfig.config = { ...generationConfig };
      }

      // System instruction
      if (system) {
        requestConfig.config = requestConfig.config || {};
        requestConfig.config.systemInstruction = system;
      }

      // Tools
      const geminiTools = toGemini(tools);
      if (geminiTools && geminiTools.length > 0) {
        requestConfig.config = requestConfig.config || {};
        requestConfig.config.tools = geminiTools;
      }

      // Abort signal
      if (signal) {
        requestConfig.config = requestConfig.config || {};
        requestConfig.config.abortSignal = signal;
      }

      // Auto-refresh OAuth token if needed (use local var to avoid closure race)
      let activeGenai = genai;
      if (isOAuth && config.refreshToken) {
        try {
          const freshToken = await refreshOAuthToken(config.refreshToken, signal);
          activeGenai = makeOAuthClient(freshToken);
        } catch (e) {
          console.warn('[google] OAuth token refresh failed:', e.message);
        }
      }
      if (isOAuth && !config.apiKey && !config.refreshToken) {
        throw new Error('Google OAuth mode requires an access token or refresh token');
      }

      const start = Date.now();
      // Retry on transient errors (Google SDK has no built-in retry unlike Anthropic/OpenAI)
      const { isRetryable, parseRetryDelay } = require('./retry');
      let raw;
      const MAX_RETRIES = 3;
      for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
        throwIfAborted(signal);
        try {
          raw = await activeGenai.models.generateContent(requestConfig);
          break;
        } catch (err) {
          throwIfAborted(signal);
          const status = err.status || err.code || 0;
          const message = err.message || '';
          if (attempt < MAX_RETRIES && isRetryable(status, message)) {
            const delay = parseRetryDelay(null, attempt);
            console.log(`[google] Retrying (${attempt + 1}/${MAX_RETRIES}) after ${delay}ms: ${message.slice(0, 100)}`);
            await delayWithAbort(delay, signal);
            continue;
          }
          throw err;
        }
      }
      const normalized = responseFromGemini(raw);

      let stopReason = 'end_turn';
      if (normalized.toolCalls && normalized.toolCalls.length > 0) stopReason = 'tool_use';
      else if (normalized.stopReason === 'STOP') stopReason = 'end_turn';

      return { ...normalized, stopReason, latencyMs: Date.now() - start, model: model || 'gemini-2.5-flash', provider: 'google', raw };
    },

    async listModels(options = {}) {
      const exact = Boolean(options.exact || options.liveOnly);
      if (!isOAuth && isPlaceholderApiKey(config.apiKey)) return exact ? [] : GOOGLE_FALLBACK_MODELS;
      if (isOAuth && !config.apiKey && !config.refreshToken) return exact ? [] : GOOGLE_FALLBACK_MODELS;
      try {
        // Auto-refresh OAuth token for listing too (use local var to avoid closure race)
        let activeGenai = genai;
        if (isOAuth && config.refreshToken) {
          try {
            const freshToken = await refreshOAuthToken(config.refreshToken);
            activeGenai = makeOAuthClient(freshToken);
          } catch {}
        }
        const response = await activeGenai.models.list();
        const liveModels = [];
        const seen = new Set();
        for (const m of response.models || response || []) {
          const fullName = m.name || '';
          const id = fullName.replace(/^models\//, '');
          if (seen.has(id)) continue;
          if (!id.startsWith('gemini')) continue;
          seen.add(id);
          const displayName = (m.displayName || id).replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
          liveModels.push({
            id,
            name: displayName,
            maxContextTokens: m.inputTokenLimit || null,
            maxOutputTokens: m.outputTokenLimit || null,
          });
        }
        const models = filterToSupportedModels('google', liveModels);
        return models.length > 0 ? models : GOOGLE_FALLBACK_MODELS;
      } catch (err) {
        // Auth errors should propagate — don't mask broken keys with fallback models
        const status = err.status || err.code || 0;
        const msg = err.message || '';
        if (status === 401 || status === 403 || /unauthorized|forbidden|invalid.*key|auth|scope/i.test(msg)) {
          throw err;
        }
        if (exact) throw err;
        console.warn('[google] Failed to list models from API, using fallback:', err.message);
        return GOOGLE_FALLBACK_MODELS;
      }
    },
  };
}

module.exports = { createGoogleProvider };
