/**
 * Google Gemini API Client
 *
 * Handles communication with Google Gemini API for translation
 * Supports dynamic config refresh when database settings change
 */

import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai';
import { getGeminiConfig, onConfigChange } from '../config';
import { logger } from '../utils/logger';
import { Tone, GeminiTranslationResponse, BulkStringItem, BulkTranslateResponse } from '../types';
import {
  canonicalizeStructuredFields,
  splitStructuredTranslation,
  StructuredFields,
} from '../utils/structuredFields';

// Model selection is code-driven (no env var).
// DEVELOPMENT_MODE=1 enables automatic fallback through GEMINI_FALLBACK_MODELS when rate-limited.
// TODO: Switch primary to gemini-3-pro once production API keys are in place.
const DEVELOPMENT_MODE: 0 | 1 = 1;
const GEMINI_PRIMARY_MODEL: string = 'gemini-3.1-flash-lite';
const GEMINI_FALLBACK_MODELS: string[] = [
  'gemini-2.5-flash',
];
const STRUCTURED_PLACEHOLDER_PATTERN = /__(?:TAG|EXCPT)_\d+__/g;
const STRUCTURED_PLACEHOLDER_PREFIX_PATTERN = /__(?:TAG|EXCPT)_/;

export interface GeminiTokenUsage {
  inputTokens: number;
  outputTokens: number;
  totalTokens: number;
}

export function getGeminiTokenUsage(usageMetadata: unknown): GeminiTokenUsage {
  const metadata = usageMetadata && typeof usageMetadata === 'object'
    ? usageMetadata as Record<string, unknown>
    : {};
  const inputTokens = typeof metadata.promptTokenCount === 'number' && metadata.promptTokenCount >= 0
    ? metadata.promptTokenCount
    : 0;
  const candidateTokens = typeof metadata.candidatesTokenCount === 'number' && metadata.candidatesTokenCount >= 0
    ? metadata.candidatesTokenCount
    : 0;
  const thinkingTokens = typeof metadata.thoughtsTokenCount === 'number' && metadata.thoughtsTokenCount >= 0
    ? metadata.thoughtsTokenCount
    : 0;
  const outputTokens = candidateTokens + thinkingTokens;

  return {
    inputTokens,
    outputTokens,
    totalTokens: inputTokens + outputTokens,
  };
}

class GeminiClient {
  private client: GoogleGenerativeAI | null = null;
  private model: GenerativeModel | null = null;
  private readonly timeout = 60000; // 60 seconds timeout
  private currentConfig: { apiKey: string } | null = null;

  constructor() {
    this.initializeClient();

    // Subscribe to config changes to refresh client when Gemini settings are updated
    onConfigChange('gemini', () => {
      logger.info('Gemini config changed, refreshing client...');
      this.refreshConfig();
    });

    onConfigChange('all', () => {
      logger.info('All config changed, refreshing Gemini client...');
      this.refreshConfig();
    });
  }

  /**
   * Initialize or reinitialize the Gemini client with current config
   */
  private initializeClient(): void {
    try {
      const geminiConfig = getGeminiConfig();
      this.currentConfig = geminiConfig;

      this.client = new GoogleGenerativeAI(geminiConfig.apiKey);
      this.model = this.client.getGenerativeModel({ model: GEMINI_PRIMARY_MODEL });

      logger.info('Gemini client initialized', {
        gemini_model: GEMINI_PRIMARY_MODEL,
        development_mode: DEVELOPMENT_MODE === 1,
        hasKey: !!geminiConfig.apiKey,
      });
    } catch (error) {
      logger.error('Failed to initialize Gemini client', { error });
      throw error;
    }
  }

  /**
   * Refresh the client configuration from database
   * Called automatically when config changes are detected
   */
  refreshConfig(): void {
    try {
      const newConfig = getGeminiConfig();

      // Only reinitialize if config has actually changed
      if (
        !this.currentConfig ||
        this.currentConfig.apiKey !== newConfig.apiKey
      ) {
        logger.info('Gemini config changed, reinitializing client...');
        this.initializeClient();
      } else {
        logger.debug('Gemini config unchanged, skipping reinitialize');
      }
    } catch (error) {
      logger.error('Failed to refresh Gemini client config', { error });
    }
  }

  /**
   * Get current configuration (for debugging/monitoring)
   */
  getCurrentConfig(): { hasKey: boolean } | null {
    return this.currentConfig
      ? { hasKey: !!this.currentConfig.apiKey }
      : null;
  }

  /**
   * Extract and preserve HTML tags from content
   * Returns content with placeholders and mapping of tags
   */
  private extractHtmlTags(content: string): { text: string; tags: Map<string, string> } {
    const tags = new Map<string, string>();
    let counter = 0;

    // Match HTML tags (opening, closing, and self-closing)
    const htmlTagRegex = /<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:="[^"]*"|='[^']*'|=[^\s>]*)?)*\s*\/?>/g;

    const text = content.replace(htmlTagRegex, (match) => {
      let placeholder: string;
      do {
        placeholder = `__TAG_${counter++}__`;
      } while (content.includes(placeholder) || tags.has(placeholder));
      tags.set(placeholder, match);
      return placeholder;
    });

    return { text, tags };
  }

  /**
   * Restore HTML tags from placeholders
   */
  private restoreHtmlTags(text: string, tags: Map<string, string>): string {
    let result = text;

    tags.forEach((tag, placeholder) => {
      const occurrences = result.split(placeholder).length - 1;
      if (occurrences !== 1) {
        throw new Error(`HTML placeholder occurrence mismatch: ${placeholder}`);
      }
      result = result.replace(placeholder, tag);
    });

    return result;
  }

  private assertStructuredPlaceholdersPreserved(source: string, output: string, field: string): void {
    const expected = source.match(STRUCTURED_PLACEHOLDER_PATTERN) || [];
    const actual = output.match(STRUCTURED_PLACEHOLDER_PATTERN) || [];
    const expectedCounts = new Map(expected.map((placeholder) => [placeholder, expected.filter((item) => item === placeholder).length]));
    const actualCounts = new Map(actual.map((placeholder) => [placeholder, actual.filter((item) => item === placeholder).length]));

    const remaining = expected.reduce((text, placeholder) => text.split(placeholder).join(''), output);
    if (STRUCTURED_PLACEHOLDER_PREFIX_PATTERN.test(remaining) || expected.length !== actual.length ||
      [...expectedCounts.entries()].some(([placeholder, count]) => actualCounts.get(placeholder) !== count)) {
      throw new Error(`Structured translation placeholders were not preserved for field: ${field}`);
    }
  }


  private getToneInstruction(tone: Tone): string {
    const toneInstructions = {
      [Tone.FORMAL]: 'Use formal, professional language.',
      [Tone.CASUAL]: 'Use casual, conversational language.',
      [Tone.NEUTRAL]: 'Use neutral, standard language.',
    };

    return toneInstructions[tone] || toneInstructions[Tone.NEUTRAL];
  }

  /**
   * Get language name from ISO code for better prompting
   */
  private getLanguageName(code: string): string {
    const languageMap: Record<string, string> = {
      'en': 'English',
      'es': 'Spanish',
      'fr': 'French',
      'de': 'German',
      'it': 'Italian',
      'pt': 'Portuguese',
      'nl': 'Dutch',
      'pl': 'Polish',
      'ru': 'Russian',
      'ja': 'Japanese',
      'ko': 'Korean',
      'zh': 'Chinese',
      'ar': 'Arabic',
      'hi': 'Hindi',
      'tr': 'Turkish',
      'vi': 'Vietnamese',
      'th': 'Thai',
      'id': 'Indonesian',
      'ms': 'Malay',
      'sv': 'Swedish',
      'da': 'Danish',
      'no': 'Norwegian',
      'fi': 'Finnish',
      'cs': 'Czech',
      'el': 'Greek',
      'he': 'Hebrew',
      'ro': 'Romanian',
      'hu': 'Hungarian',
      'uk': 'Ukrainian',
      'bg': 'Bulgarian',
      'sr': 'Serbian',
      'hr': 'Croatian',
      'sk': 'Slovak',
      'sl': 'Slovenian',
      'et': 'Estonian',
      'lv': 'Latvian',
      'lt': 'Lithuanian',
    };

    return languageMap[code] || code.toUpperCase();
  }

  private withTimeout<T>(operation: Promise<T>, message: string, timeout = this.timeout): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error(message)), timeout);
      operation.then(resolve, reject).finally(() => clearTimeout(timer));
    });
  }

  /**
   * Translate content using Google Gemini API
   *
   * @param content Content to translate
   * @param sourceLang Source language (ISO 639-1 code)
   * @param targetLang Target language (ISO 639-1 code)
   * @param tone Translation tone/style
   * @returns Translation response with translated content, tokens used, and processing time
   * @throws Error if translation fails
   */
  private isRetryableError(e: unknown): boolean {
    if (!(e instanceof Error)) return false;
    const msg = (e.message || '').toLowerCase();
    return (
      msg.includes('rate limit') ||
      msg.includes('quota') ||
      msg.includes('resource_exhausted') ||
      msg.includes('too many requests') ||
      msg.includes('429') ||
      msg.includes('not found') ||
      msg.includes('model') ||
      msg.includes('503') ||
      msg.includes('unavailable') ||
      msg.includes('timed out')
    );
  }

  async translate(
    content: string,
    sourceLang: string,
    targetLang: string,
    tone: Tone = Tone.NEUTRAL
  ): Promise<GeminiTranslationResponse> {
    if (!this.client || !this.model) {
      throw new Error('Gemini client not initialized');
    }

    const startTime = Date.now();

    // Extract HTML tags to preserve them
    const { text: cleanText, tags } = this.extractHtmlTags(content);

    const sourceLanguage = this.getLanguageName(sourceLang);
    const targetLanguage = this.getLanguageName(targetLang);
    const toneInstruction = this.getToneInstruction(tone);

    // Build translation prompt
    const prompt = `Translate the following text from ${sourceLanguage} to ${targetLanguage}.

${toneInstruction}

IMPORTANT RULES:
1. Preserve all placeholders in the format __TAG_N__ exactly as they appear
2. Do not translate the placeholders themselves
3. Only translate the actual text content
4. Maintain the exact position and format of placeholders
5. Output ONLY the translated text, no explanations or additional text
6. Preserve all placeholders in the format __EXCPT_N__ exactly as they appear

Text to translate:
${cleanText}`;

    logger.info('Calling Gemini translation API', {
      sourceLang,
      targetLang,
      gemini_model: GEMINI_PRIMARY_MODEL,
      tone,
      contentLength: content.length,
      cleanTextLength: cleanText.length,
      tagCount: tags.size,
    });

    const runWithModel = async (
      activeModel: GenerativeModel,
      geminiModelName: string,
      fallbackUsed: boolean
    ): Promise<GeminiTranslationResponse> => {
      // Generate translation with timeout
      const result = await this.withTimeout(
        activeModel.generateContent(prompt),
        'Translation request timed out'
      );

      const response = result.response;
      const translatedText = response.text();

      if (!translatedText) {
        throw new Error('Empty translation response from Gemini API');
      }

      // Restore HTML tags
      const finalTranslation = this.restoreHtmlTags(translatedText, tags);

      // Extract token usage from response metadata (no extra API calls)
      const tokenUsage = getGeminiTokenUsage(response.usageMetadata);
      const inputTokens = tokenUsage.inputTokens;
      const outputTokens = tokenUsage.outputTokens;
      const totalTokens = tokenUsage.totalTokens;

      const processingTime = Date.now() - startTime;

      logger.info('Gemini translation successful', {
        gemini_model: geminiModelName,
        fallback_used: fallbackUsed,
        inputTokens,
        outputTokens,
        totalTokens,
        processingTimeMs: processingTime,
        tagsRestored: tags.size,
      });

      return {
        translation: finalTranslation,
        tokens_used: totalTokens,
        input_tokens: inputTokens,
        output_tokens: outputTokens,
        processing_time_ms: processingTime,
        model_used: geminiModelName,
      };
    };

    try {
      return await runWithModel(this.model, GEMINI_PRIMARY_MODEL, false);
    } catch (caughtError) {
      let error: unknown = caughtError;

      // Try each fallback model in order on retryable errors (rate limit, model unavailable, etc.)
      if (DEVELOPMENT_MODE === 1 && this.client && this.isRetryableError(error)) {
        for (const fallbackModelName of GEMINI_FALLBACK_MODELS) {
          logger.warn('Gemini primary model failed; attempting fallback', {
            failed_model: error instanceof Error ? (error as Error).message.substring(0, 80) : 'unknown',
            fallback_model: fallbackModelName,
          });

          try {
            const fallbackModel = this.client.getGenerativeModel({ model: fallbackModelName });
            return await runWithModel(fallbackModel, fallbackModelName, true);
          } catch (fallbackError) {
            logger.error('Gemini fallback model failed', {
              model: fallbackModelName,
              error: fallbackError instanceof Error ? fallbackError.message : 'Unknown error',
            });
            error = fallbackError;
            // If this fallback also hit a retryable error, continue to next; otherwise break
            if (!this.isRetryableError(fallbackError)) break;
          }
        }
      }

      const processingTime = Date.now() - startTime;

      // Handle specific error cases
      if (error instanceof Error) {
        const errorMessage = error.message;

        logger.error('Gemini translation failed', {
          error: errorMessage,
          processingTime,
        });

        // Map common errors to user-friendly messages
        if (errorMessage.includes('timed out')) {
          throw new Error('Translation request timed out. Please try again shortly.');
        }

        if (errorMessage.includes('API key')) {
          throw new Error('Gemini API authentication failed.');
        }

        if (errorMessage.includes('quota') || errorMessage.includes('rate limit')) {
          throw new Error('Gemini API rate limit exceeded. Please try again later.');
        }

        if (errorMessage.includes('model') || errorMessage.includes('not found')) {
          throw new Error('Translation model temporarily unavailable. Please try again later.');
        }

        // Generic error
        throw new Error(`Translation failed: ${errorMessage}`);
      }

      // Unknown error type
      logger.error('Unexpected error in Gemini client', {
        error: error instanceof Error ? error.message : 'Unknown error',
        processingTime,
      });

      throw new Error('An unexpected error occurred during translation. Please try again.');
    }
  }

  /**
   * Translate multiple strings in a single Gemini API call (token-efficient bulk mode).
   *
   * Extracts HTML tags from each string before building the JSON prompt,
   * then restores them in the response. On JSON parse failure the entire batch
   * is marked as failed so the caller can fall back gracefully.
   *
   * @param strings  Array of { id, content } pairs — id is echoed back unchanged
   * @param sourceLang Source language ISO 639-1 code
   * @param targetLang Target language ISO 639-1 code
   * @param tone Translation tone
   * @returns Array of results in the same order as the input
   */
  async translateBulk(
    strings: BulkStringItem[],
    sourceLang: string,
    targetLang: string,
    tone: Tone = Tone.NEUTRAL
  ): Promise<BulkTranslateResponse> {
    if (!this.client || !this.model) {
      throw new Error('Gemini client not initialized');
    }

    if (strings.length === 0) {
      return { results: [], model_used: GEMINI_PRIMARY_MODEL, tokens_used: 0, input_tokens: 0, output_tokens: 0 };
    }

    // Per-string HTML tag extraction
    const processedStrings = strings.map(s => {
      const { text, tags } = this.extractHtmlTags(s.content);
      return { id: s.id, text, tags };
    });

    const sourceLanguage = this.getLanguageName(sourceLang);
    const targetLanguage = this.getLanguageName(targetLang);
    const toneInstruction = this.getToneInstruction(tone);

    const inputArray = processedStrings.map(s => ({ id: s.id, text: s.text }));
    const inputJson = JSON.stringify(inputArray);

    const prompt = `Translate the following JSON array from ${sourceLanguage} to ${targetLanguage}.
${toneInstruction}

Rules:
1. Return ONLY a valid JSON array — no explanation, no markdown, no code fences
2. Keep each object's "id" value unchanged
3. Translate only the "text" field value
4. Preserve HTML placeholders __TAG_N__ exactly as they appear
5. Preserve exception placeholders __EXCPT_N__ exactly as they appear
6. If a string fails to translate, return {"id":"...","text":"","failed":true}

${inputJson}`;

    logger.info('Calling Gemini bulk translation API', {
      sourceLang,
      targetLang,
      tone,
      stringCount: strings.length,
    });

    const runBulkWithModel = async (
      activeModel: GenerativeModel,
      modelName: string,
      fallbackUsed: boolean
    ): Promise<BulkTranslateResponse> => {
      const result = await this.withTimeout(
        activeModel.generateContent(prompt),
        'Bulk translation request timed out'
      );

      const response = result.response;
      const responseText = response.text();
      if (!responseText) {
        throw new Error('Empty response from Gemini API');
      }

      // Extract token usage from response metadata
      const tokenUsage = getGeminiTokenUsage(response.usageMetadata);
      const inputTokens = tokenUsage.inputTokens;
      const outputTokens = tokenUsage.outputTokens;
      const totalTokens = tokenUsage.totalTokens;

      // Strip markdown code fences if the model wraps the JSON
      let cleanResponse = responseText.trim();
      if (cleanResponse.startsWith('```')) {
        cleanResponse = cleanResponse.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '').trim();
      }

      let parsed: Array<{ id: string; text: string; failed?: boolean }>;
      try {
        parsed = JSON.parse(cleanResponse);
      } catch (parseError) {
        logger.error('Failed to parse Gemini bulk response as JSON', {
          error: parseError instanceof Error ? parseError.message : 'Parse error',
          responsePreview: responseText.substring(0, 300),
          gemini_model: modelName,
        });
        return {
          results: strings.map(s => ({
            id: s.id,
            translation: '',
            success: false,
            error: 'Failed to parse bulk translation response as JSON',
          })),
          model_used: modelName,
          tokens_used: totalTokens,
          input_tokens: inputTokens,
          output_tokens: outputTokens,
        };
      }

      logger.info('Gemini bulk translation successful', {
        gemini_model: modelName,
        fallback_used: fallbackUsed,
        stringCount: strings.length,
        totalTokens,
      });

      const resultMap = new Map(parsed.map(item => [item.id, item]));

      return {
        results: processedStrings.map(s => {
          const item = resultMap.get(s.id);
          if (!item || item.failed || !item.text) {
            return {
              id: s.id,
              translation: '',
              success: false,
              error: item?.failed ? 'Translation failed for this string' : 'String missing from response',
            };
          }
          const restored = this.restoreHtmlTags(item.text, s.tags);
          return {
            id: s.id,
            translation: restored,
            success: true,
          };
        }),
        model_used: modelName,
        tokens_used: totalTokens,
        input_tokens: inputTokens,
        output_tokens: outputTokens,
      };
    };

    try {
      return await runBulkWithModel(this.model, GEMINI_PRIMARY_MODEL, false);
    } catch (caughtError) {
      let error: unknown = caughtError;

      // Try each fallback model in order on retryable errors (rate limit, model unavailable, etc.)
      if (DEVELOPMENT_MODE === 1 && this.client && this.isRetryableError(error)) {
        for (const fallbackModelName of GEMINI_FALLBACK_MODELS) {
          logger.warn('Gemini primary model failed on bulk; attempting fallback', {
            failed_model: error instanceof Error ? (error as Error).message.substring(0, 80) : 'unknown',
            fallback_model: fallbackModelName,
            stringCount: strings.length,
          });

          try {
            const fallbackModel = this.client.getGenerativeModel({ model: fallbackModelName });
            return await runBulkWithModel(fallbackModel, fallbackModelName, true);
          } catch (fallbackError) {
            logger.error('Gemini fallback model failed on bulk', {
              model: fallbackModelName,
              error: fallbackError instanceof Error ? fallbackError.message : 'Unknown error',
            });
            error = fallbackError;
            if (!this.isRetryableError(fallbackError)) break;
          }
        }
      }

      const errorMsg = error instanceof Error ? (error as Error).message : 'Unknown error';
      logger.error('Gemini bulk translation failed', { error: errorMsg, stringCount: strings.length });
      return {
        results: strings.map(s => ({
          id: s.id,
          translation: '',
          success: false,
          error: errorMsg,
        })),
        model_used: GEMINI_PRIMARY_MODEL,
        tokens_used: 0,
        input_tokens: 0,
        output_tokens: 0,
      };
    }
  }

  /**
   * Translate structured post fields (title, excerpt, content) in a single Gemini call.
   *
   * Fields are HTML-tag-extracted individually before building a JSON prompt,
   * then HTML tags are restored per-field from the JSON response.
   */
  async translateStructured(
    fields: StructuredFields,
    sourceLang: string,
    targetLang: string,
    tone: Tone = Tone.NEUTRAL
  ): Promise<{
    fields: StructuredFields;
    translatedFields: StructuredFields;
    translated_title?: string;
    translated_excerpt?: string;
    translated_content?: string;
    tokens_used: number;
    input_tokens: number;
    output_tokens: number;
    processing_time_ms: number;
    model_used: string;
  }> {
    if (!this.client || !this.model) {
      throw new Error('Gemini client not initialized');
    }

    const startTime = Date.now();
    const inputData = Object.fromEntries(
      Object.entries(canonicalizeStructuredFields(fields)).map(([key, value]) => [key, this.extractHtmlTags(value)])
    );
    const inputObj = Object.fromEntries(
      Object.entries(inputData).map(([key, value]) => [key, value.text])
    ) as StructuredFields;
    const sourceLanguage = this.getLanguageName(sourceLang);
    const targetLanguage = this.getLanguageName(targetLang);
    const toneInstruction = this.getToneInstruction(tone);
    const inputJson = JSON.stringify(inputObj);

    const prompt = `Translate the field values in the following JSON from ${sourceLanguage} to ${targetLanguage}.
${toneInstruction}

Rules:
1. Return ONLY a valid JSON object — no explanation, markdown, or code fences
2. Return exactly the same keys, with no missing or extra keys
3. Keep keys unchanged and translate only string values
4. Every output value must be a JSON string
5. Preserve HTML placeholders __TAG_N__ and exception placeholders __EXCPT_N__ exactly

${inputJson}`;

    const runWithModel = async (
      activeModel: GenerativeModel,
      modelName: string,
      fallbackUsed: boolean
    ) => {
      const result = await this.withTimeout(
        activeModel.generateContent(prompt),
        'Structured translation request timed out'
      );
      const response = result.response;
      const responseText = response.text();
      if (!responseText) {
        throw new Error('Empty translation response from Gemini API');
      }

      let cleanResponse = responseText.trim();
      if (cleanResponse.startsWith('```')) {
        cleanResponse = cleanResponse.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '').trim();
      }

      let parsed: unknown;
      try {
        parsed = JSON.parse(cleanResponse);
      } catch (parseError) {
        logger.error('Failed to parse structured translation response as JSON', {
          error: parseError instanceof Error ? parseError.message : 'Parse error',
          modelName,
        });
        throw new Error('Failed to parse structured translation response');
      }

      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error('Structured translation response must be a JSON object');
      }
      const output = parsed as Record<string, unknown>;
      const expectedKeys = Object.keys(inputObj).sort();
      const outputKeys = Object.keys(output).sort();
      if (expectedKeys.length !== outputKeys.length || expectedKeys.some((key, index) => key !== outputKeys[index])) {
        throw new Error('Structured translation response keys do not match input fields');
      }
      if (outputKeys.some((key) => typeof output[key] !== 'string')) {
        throw new Error('Structured translation response values must be strings');
      }

      const translated: StructuredFields = {};
      for (const key of expectedKeys) {
        const tags = inputData[key].tags;
        this.assertStructuredPlaceholdersPreserved(inputObj[key], output[key] as string, key);
        translated[key] = this.restoreHtmlTags(output[key] as string, tags);
      }

      // Extract token usage from response metadata (no extra API calls)
      const tokenUsage = getGeminiTokenUsage(response.usageMetadata);
      const inputTokens = tokenUsage.inputTokens;
      const outputTokens = tokenUsage.outputTokens;
      const totalTokens = tokenUsage.totalTokens;
      const processingTime = Date.now() - startTime;
      const aliases = splitStructuredTranslation(translated);

      logger.info('Gemini structured translation successful', {
        gemini_model: modelName,
        fallback_used: fallbackUsed,
        fieldCount: expectedKeys.length,
        totalTokens,
        processingTimeMs: processingTime,
      });

      return {
        fields: translated,
        translatedFields: translated,
        translated_title: aliases.translatedTitle,
        translated_excerpt: aliases.translatedExcerpt,
        translated_content: aliases.translatedContent,
        tokens_used: totalTokens,
        input_tokens: inputTokens,
        output_tokens: outputTokens,
        processing_time_ms: processingTime,
        model_used: modelName,
      };
    };

    try {
      return await runWithModel(this.model, GEMINI_PRIMARY_MODEL, false);
    } catch (caughtError) {
      let error: unknown = caughtError;

      if (DEVELOPMENT_MODE === 1 && this.client && this.isRetryableError(error)) {
        for (const fallbackModelName of GEMINI_FALLBACK_MODELS) {
          logger.warn('Gemini primary model failed on structured translation; attempting fallback', {
            fallback_model: fallbackModelName,
          });

          try {
            const fallbackModel = this.client.getGenerativeModel({ model: fallbackModelName });
            return await runWithModel(fallbackModel, fallbackModelName, true);
          } catch (fallbackError) {
            logger.error('Gemini fallback model failed on structured translation', {
              model: fallbackModelName,
            });
            error = fallbackError;
            if (!this.isRetryableError(fallbackError)) break;
          }
        }
      }

      throw error instanceof Error ? error : new Error('Structured translation failed');
    }
  }

  /**
   * Check health of Gemini service
   * Tests API key validity with a minimal request
   *
   * @returns true if service is healthy, false otherwise
   */
  async healthCheck(): Promise<boolean> {
    try {
      if (!this.client || !this.model) {
        logger.warn('Gemini client not initialized');
        return false;
      }

      // Test with a minimal translation
      const testPrompt = 'Translate "hello" to Spanish';
      const result = await this.withTimeout(
        this.model.generateContent(testPrompt),
        'Health check timeout',
        5000
      );

      const response = result.response;
      return !!response.text();
    } catch (error) {
      logger.warn('Gemini health check failed', {
        error: error instanceof Error ? error.message : 'Unknown error',
      });
      return false;
    }
  }
}

// Export singleton instance
export const geminiClient = new GeminiClient();
