// src/lib/services/ai.service.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
import { prisma } from "@/lib/db/prisma";
import {
  AnalyzeOptions,
  ContractPrecheckResult,
  NormalizedAnalysis,
} from "@/lib/services/ai/analysis.types";
import type { Prisma } from "@prisma/client";
import {
  buildAnalysisPrompt,
  buildPrevalidationPrompt,
} from "@/lib/services/ai/analysis.prompt";
import { parseJsonResponse as parseAiJsonResponse } from "@/lib/services/ai/analysis.parser";
import { normalizeAnalysis as normalizeAiAnalysis } from "@/lib/services/ai/analysis.normalizer";
import { RAGService } from "@/lib/services/rag.service";

import { keyManager } from "@/lib/services/ai/key-manager";

const PRIMARY_ANALYSIS_MODEL =
  process.env.AI_MODEL_PRIMARY || "gemini-3.1-flash-lite-preview";
const GEMINI_SECONDARY_ANALYSIS_MODEL =
  process.env.AI_MODEL_SECONDARY_GEMINI || process.env.AI_MODEL_SECONDARY || "";
const FALLBACK_ANALYSIS_MODEL =
  process.env.AI_MODEL_FALLBACK || "mistral-large-latest";
const FALLBACK_REPAIR_MODEL =
  process.env.AI_MODEL_FALLBACK_REPAIR || "mistral-large-latest";
const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY?.trim() || "";
const MISTRAL_API_URL = "https://api.mistral.ai/v1/chat/completions";
const MISTRAL_OCR_API_URL = "https://api.mistral.ai/v1/ocr";
const MISTRAL_VISION_MODEL =
  process.env.AI_MODEL_MISTRAL_VISION || "pixtral-large-latest";
const MISTRAL_OCR_MODEL =
  process.env.AI_MODEL_MISTRAL_OCR || "mistral-ocr-latest";

const GEMINI_ANALYSIS_MODELS = Array.from(
  new Set(
    [PRIMARY_ANALYSIS_MODEL, GEMINI_SECONDARY_ANALYSIS_MODEL].filter(Boolean),
  ),
);

const ANALYSIS_MODELS = Array.from(
  new Set([...GEMINI_ANALYSIS_MODELS, `mistral:${FALLBACK_ANALYSIS_MODEL}`]),
);

const FORCE_FALLBACK_TEST =
  process.env.AI_FORCE_FALLBACK_TEST === "1" ||
  String(process.env.AI_FORCE_FALLBACK_TEST).toLowerCase() === "true";

type ValidationEnvelope = {
  contractValidation?: {
    isValidContract?: boolean;
    confidence?: number;
    reason?: string | null;
  };
};

type PrevalidationResponse = {
  isValidContract?: boolean;
  confidence?: number;
  reason?: string | null;
};

type AdaptiveExplainability = {
  field?: string;
  sourceHints?: {
    confidence?: number;
  };
};

type AdaptiveAiMeta = {
  language?: string | null;
  keyPeople?: Array<{ role?: string | null }>;
};

type AdaptiveKeyPoints = {
  explainability?: AdaptiveExplainability[];
  aiMeta?: AdaptiveAiMeta;
};

type AdaptiveContractExample = {
  type?: string | null;
  provider?: string | null;
  policyNumber?: string | null;
  summary?: string | null;
  keyPoints?: Prisma.JsonValue | null;
};

const isAdaptiveKeyPoints = (
  value: Prisma.JsonValue | null | undefined,
): value is AdaptiveKeyPoints => {
  return typeof value === "object" && value !== null && !Array.isArray(value);
};

export class AIService {
  private static isTransientAIError(message: string): boolean {
    const normalized = message.toLowerCase();
    return (
      normalized.includes("503") ||
      normalized.includes("service unavailable") ||
      normalized.includes("high demand") ||
      normalized.includes("temporarily unavailable") ||
      normalized.includes("backend error") ||
      normalized.includes("internal server error") ||
      normalized.includes("bad gateway") ||
      normalized.includes("gateway timeout") ||
      normalized.includes("deadline exceeded")
    );
  }

  /**
   * Domain-specific guidance for contract Q&A.
   * This keeps responses focused on what matters most for each contract family.
   */
  private static getContractTypeGuidance(type?: string | null): string {
    switch (type) {
      case "INSURANCE_AUTO":
        return "Focus on coverage scope, exclusions, deductible/franchise impact, claims workflow, and driver/vehicle obligations.";
      case "INSURANCE_HOME":
        return "Focus on covered perils, property limits, occupancy obligations, exclusions, and claims evidence requirements.";
      case "INSURANCE_HEALTH":
        return "Focus on reimbursement rules, waiting periods, provider network constraints, exclusions, and pre-authorization requirements.";
      case "INSURANCE_LIFE":
        return "Focus on beneficiary clauses, premium continuity, surrender/termination conditions, exclusions, and payout trigger conditions.";
      case "LOAN":
        return "Focus on repayment schedule, interest mechanics, default triggers, penalties, early repayment clauses, and covenant obligations.";
      case "CREDIT_CARD":
        return "Focus on APR/fees, billing cycle deadlines, late-payment penalties, credit limit terms, and dispute/chargeback conditions.";
      case "INVESTMENT":
        return "Focus on risk profile, fee structure, lock-in/liquidity constraints, reporting duties, and suitability/compliance implications.";
      default:
        return "Focus on obligations, financial exposure, compliance risks, termination conditions, and operational next steps.";
    }
  }

  /**
   * Analyze contract with Gemini 2.5 Flash.
   *
   * Pipeline overview:
   * 1) Download uploaded file
   * 2) Resolve MIME type safely
   * 3) Build adaptive prompt context from previous completed analyses
   * 4) Ask Gemini for strict JSON output
   * 5) Parse + normalize output
   * 6) Validate contract legitimacy and required fields
   * 7) Retry with correction hints if output is invalid
   * 8) Return canonical analysis object
   *
   * Supports both PDF and image files
   */
  static async analyzeContract(fileUrl: string, options?: AnalyzeOptions) {
    keyManager.resetKeys();
    try {
      const maxRetries = Math.min(3, Math.max(1, options?.maxRetries ?? 2));
      const forceFallbackModelTest =
        options?.forceFallbackModelTest ?? FORCE_FALLBACK_TEST;

      // Step 1: Download raw file bytes from storage URL.
      const response = await fetch(fileUrl);
      if (!response.ok) {
        throw new Error(`Failed to download file: ${response.statusText}`);
      }

      const buffer = await response.arrayBuffer();
      const base64 = Buffer.from(buffer).toString("base64");

      // Step 2: Resolve MIME type from response header and URL fallback.
      const mimeType = this.resolveMimeType(
        fileUrl,
        response.headers.get("content-type"),
      );

      // Quick pre-validation to short-circuit obvious non-contract files.
      const precheck = await this.preValidateContract({
        base64,
        mimeType,
        fileName: options?.fileName,
      });

      if (!precheck.isValidContract || precheck.confidence < 45) {
        throw new Error(
          `INVALID_CONTRACT:${precheck.reason || "Uploaded file is not recognized as a valid contract."}`,
        );
      }

      // Step 4: Build adaptive extraction context from previously analyzed contracts.
      const adaptiveContext = await this.buildAdaptiveContext(options?.userId);
      const basePrompt = buildAnalysisPrompt({
        adaptiveContext,
        fileName: options?.fileName,
      });

      let previousRawResponse = "";
      let lastValidationError = "";

      for (let attempt = 1; attempt <= maxRetries; attempt++) {
        const startTime = Date.now();

        const correctionHint =
          attempt === 1
            ? ""
            : `\n\nCORRECTION MODE:\nYour previous response was invalid.\nReason: ${lastValidationError || "Invalid structure"}.\nReturn JSON only and keep every required field.\nPrevious invalid response:\n${previousRawResponse.slice(0, 2000)}`;

        // Step 5: Ask model to extract strict JSON from the uploaded file.
        const text = await this.generateAnalysisWithFallback({
          prompt: `${basePrompt}${correctionHint}`,
          base64,
          mimeType,
          forceFallbackModelTest,
        });

        if (!text) {
          lastValidationError = "No content in AI response";
          continue;
        }

        previousRawResponse = text;

        try {
          // Step 6: Parse and normalize output into canonical structure.
          let parsed: unknown;

          try {
            parsed = this.parseJsonResponse(text);
          } catch (parseError) {
            console.warn(
              "Initial JSON parse failed. Attempting repair with fallback model...",
            );
            const repaired = await this.repairMalformedJson(
              text,
              parseError instanceof Error
                ? parseError.message
                : "Invalid JSON response",
            );

            if (!repaired) {
              // Emergency fallback: try to extract key fields from raw text
              console.warn(
                "Repair model failed. Attempting emergency field extraction...",
              );
              const emergency = this.emergencyExtractFields(text);
              if (emergency) {
                console.log("✅ Emergency extraction succeeded");
                parsed = this.parseJsonResponse(emergency);
              } else {
                throw parseError;
              }
            } else {
              parsed = this.parseJsonResponse(repaired);
            }
          }

          const normalized = this.normalizeAnalysis(parsed);

          // Step 7: Reject non-contract uploads with explicit error.
          this.assertValidContract(parsed, normalized);

          console.log(
            "📄 Extracted text length:",
            normalized.extractedText.length,
            "chars",
          );
          console.log(
            "✅ Analysis completed in",
            ((Date.now() - startTime) / 1000).toFixed(2),
            "seconds",
          );

          return normalized;
        } catch (validationError: unknown) {
          // If validation fails, keep reason and retry with correction guidance.
          lastValidationError =
            validationError instanceof Error
              ? validationError.message
              : "Failed to parse model output";
          if (attempt === maxRetries) {
            throw new Error(lastValidationError);
          }
        }
      }

      throw new Error("AI analysis failed after retries.");
    } catch (error: unknown) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      // Better error messages
      if (errorMessage.includes("API key")) {
        throw new Error(
          "Invalid or missing AI API key. Check AI_API_KEY1/2/3 for Gemini and MISTRAL_API_KEY for Mistral fallback.",
        );
      } else if (errorMessage.includes("INVALID_CONTRACT:")) {
        const reason = String(errorMessage)
          .replace("INVALID_CONTRACT:", "")
          .trim();
        throw new Error(
          reason || "Uploaded file is not recognized as a valid contract.",
        );
      } else if (this.isTransientAIError(errorMessage)) {
        throw new Error(
          `The AI providers (Gemini/Mistral) are temporarily overloaded for the configured analysis models (${ANALYSIS_MODELS.join(", ")}). The app retried automatically, but both providers are still busy. Please try again in a few minutes.`,
        );
      } else if (
        errorMessage.includes("not found") ||
        errorMessage.includes("404")
      ) {
        throw new Error(
          `Invalid Gemini model configuration. Current models: ${ANALYSIS_MODELS.join(", ")}. Check model availability in your Gemini account.`,
        );
      } else if (
        errorMessage.includes("fetch") &&
        !errorMessage.includes("generativelanguage")
      ) {
        throw new Error(
          "Download failed. Check if the file URL is correct and accessible.",
        );
      } else if (
        errorMessage.includes("JSON") ||
        errorMessage.includes("No complete JSON object") ||
        errorMessage.includes("parse failed")
      ) {
        console.error("❌ Raw response that failed to parse:", error);
        console.error("Full error message:", errorMessage);

        // Help user understand what went wrong
        if (errorMessage.includes("escaped quotes")) {
          throw new Error(
            "The contract contains special characters that corrupted the analysis. Try uploading a cleaner version.",
          );
        } else if (errorMessage.includes("incomplete")) {
          throw new Error(
            "AI analysis failed to complete properly. This might be a large or complex contract. Try a smaller contract first.",
          );
        } else if (errorMessage.includes("missing expected")) {
          throw new Error(
            "This doesn't appear to be a valid financial/insurance contract. Please upload a legitimate contract document.",
          );
        } else {
          throw new Error(
            "AI returned a malformed response format. Please retry analysis; if it fails again, the file may require OCR cleanup.",
          );
        }
      } else if (errorMessage.includes("quota")) {
        throw new Error(
          "Limit exceeded. Gemini or Mistral quota may be exhausted. Check your provider dashboards for usage and limits.",
        );
      } else {
        throw new Error(`Error analyzing contract: ${errorMessage}`);
      }
    }
  }

  /**
   * Prompt generation has been moved to lib/services/ai/analysis.prompt.ts.
   */
  private static buildPrompt(input?: {
    adaptiveContext?: string;
    fileName?: string;
  }): string {
    return buildAnalysisPrompt(input);
  }

  /**
   * Resolve MIME type from HTTP headers first, then URL extension fallback.
   */
  private static resolveMimeType(
    fileUrl: string,
    headerContentType: string | null,
  ): string {
    const normalizedHeader = headerContentType?.toLowerCase() || "";
    if (normalizedHeader.startsWith("application/pdf")) {
      return "application/pdf";
    }
    if (normalizedHeader.startsWith("image/png")) {
      return "image/png";
    }
    if (normalizedHeader.startsWith("image/jpeg")) {
      return "image/jpeg";
    }
    if (normalizedHeader.startsWith("image/webp")) {
      return "image/webp";
    }

    const lowerUrl = fileUrl.toLowerCase();
    if (lowerUrl.includes(".pdf")) return "application/pdf";
    if (lowerUrl.includes(".png")) return "image/png";
    if (lowerUrl.includes(".jpg") || lowerUrl.includes(".jpeg"))
      return "image/jpeg";
    if (lowerUrl.includes(".webp")) return "image/webp";
    return "application/pdf"; // Default
  }

  private static parseJsonResponse(text: string): unknown {
    return parseAiJsonResponse(text);
  }

  private static isMistralConfigured(): boolean {
    return MISTRAL_API_KEY.length > 0;
  }

  private static async generateWithMistral(input: {
    model?: string;
    prompt: string;
    systemPrompt?: string;
    responseAsJson: boolean;
    maxOutputTokens: number;
    temperature?: number;
    topP?: number;
  }): Promise<string> {
    if (!this.isMistralConfigured()) {
      throw new Error(
        "Mistral fallback is not configured. Set MISTRAL_API_KEY.",
      );
    }

    const modelName = input.model || FALLBACK_ANALYSIS_MODEL;

    // Build messages with system/user role separation for better instruction adherence
    const messages: Array<{ role: string; content: string }> = [];
    if (input.systemPrompt) {
      messages.push({ role: "system", content: input.systemPrompt });
    }
    messages.push({ role: "user", content: input.prompt });

    // Use json_object mode (compatible with all models)
    const responseFormat: Record<string, unknown> | undefined =
      input.responseAsJson ? { type: "json_object" as const } : undefined;

    const temperature = input.temperature ?? 0;
    const top_p = temperature === 0 ? 1 : (input.topP ?? 0.95);

    const body: Record<string, unknown> = {
      model: modelName,
      temperature,
      top_p,
      max_tokens: input.maxOutputTokens,
      response_format: responseFormat,
      messages,
    };

    const response = await fetch(MISTRAL_API_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${MISTRAL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const details = await response.text();
      throw new Error(
        `Mistral API error ${response.status}: ${details.slice(0, 300)}`,
      );
    }

    const json = (await response.json()) as {
      choices?: Array<{ message?: { content?: string | null } }>;
    };
    const text = json.choices?.[0]?.message?.content?.trim() || "";

    if (!text) {
      throw new Error("Empty response from Mistral fallback model.");
    }

    return text;
  }

  /**
   * Multimodal analysis using Mistral Pixtral vision model.
   * Sends base64-encoded images directly to Pixtral for analysis,
   * eliminating the need for a separate OCR bridge when Gemini is down.
   */
  private static async generateWithMistralVision(input: {
    prompt: string;
    base64: string;
    mimeType: string;
    systemPrompt?: string;
    responseAsJson?: boolean;
    maxOutputTokens?: number;
  }): Promise<string> {
    if (!this.isMistralConfigured()) {
      throw new Error(
        "Mistral fallback is not configured. Set MISTRAL_API_KEY.",
      );
    }

    const messages: Array<{ role: string; content: unknown }> = [];
    if (input.systemPrompt) {
      messages.push({ role: "system", content: input.systemPrompt });
    }

    // OpenAI-compatible multimodal content format for Pixtral vision
    messages.push({
      role: "user",
      content: [
        { type: "text", text: input.prompt },
        {
          type: "image_url",
          image_url: {
            url: `data:${input.mimeType};base64,${input.base64}`,
          },
        },
      ],
    });

    const responseFormat: Record<string, unknown> | undefined =
      input.responseAsJson ? { type: "json_object" as const } : undefined;

    const body: Record<string, unknown> = {
      model: MISTRAL_VISION_MODEL,
      temperature: 0,
      top_p: 1,
      max_tokens: input.maxOutputTokens ?? 16384,
      response_format: responseFormat,
      messages,
    };

    const response = await fetch(MISTRAL_API_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${MISTRAL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const details = await response.text();
      throw new Error(
        `Mistral Vision API error ${response.status}: ${details.slice(0, 300)}`,
      );
    }

    const json = (await response.json()) as {
      choices?: Array<{ message?: { content?: string | null } }>;
    };
    const text = json.choices?.[0]?.message?.content?.trim() || "";

    if (!text) {
      throw new Error("Empty response from Mistral Pixtral vision model.");
    }

    console.log(`✅ Mistral Pixtral vision analysis succeeded`);
    return text;
  }

  private static async generateWithMistralModelChain(input: {
    preferredModel?: string;
    prompt: string;
    systemPrompt?: string;
    responseAsJson: boolean;
    maxOutputTokens: number;
    temperature?: number;
    topP?: number;
  }): Promise<string> {
    const candidates = Array.from(
      new Set(
        [
          input.preferredModel,
          FALLBACK_ANALYSIS_MODEL,
          "mistral-large-latest",
          "mistral-small-latest",
          "open-mistral-nemo",
        ].filter(Boolean),
      ),
    ) as string[];

    let lastError: unknown = null;

    for (const modelName of candidates) {
      try {
        const text = await this.generateWithMistral({
          model: modelName,
          prompt: input.prompt,
          systemPrompt: input.systemPrompt,
          responseAsJson: input.responseAsJson,
          maxOutputTokens: input.maxOutputTokens,
          temperature: input.temperature,
          topP: input.topP,
        });
        if (modelName !== (input.preferredModel || FALLBACK_ANALYSIS_MODEL)) {
          console.warn(
            `Mistral switched to fallback model ${modelName} after primary fallback model failed.`,
          );
        }
        return text;
      } catch (error) {
        lastError = error;
        console.warn(
          `Mistral model ${modelName} failed. Trying next fallback model.`,
          error instanceof Error ? error.message : String(error),
        );
      }
    }

    throw lastError instanceof Error
      ? lastError
      : new Error("All Mistral fallback models failed.");
  }

  /**
   * Build a Mistral-optimized system prompt that mirrors the Gemini behavior.
   * This separates role & formatting rules from user content for better
   * instruction adherence on open-source models.
   *
   * Unlike the Gemini prompt which sends examples with the file inline,
   * this prompt is designed to prevent hallucination by using explicit
   * placeholder markers instead of realistic example values.
   */
  private static buildMistralSystemPrompt(): string {
    return `You are an expert contract analysis engine for the BFSI (Banking, Financial Services, and Insurance) sector.
You receive the full text content of a contract document and must extract structured information from it.

ABSOLUTE RULES — VIOLATION OF THESE IS A CRITICAL FAILURE:
1. Return ONLY valid, parseable JSON — no markdown, no backticks, no explanations, no commentary.
2. EVERY value you output MUST come directly from the document text provided to you.
3. If a piece of information does NOT exist in the document text, you MUST use null (for strings/numbers) or [] (for arrays). NEVER invent, assume, or guess data.
4. Do NOT copy example values from the schema description — they are placeholders, not real data.
5. The "extractedText" field MUST contain actual verbatim text from the document — not a summary, not examples.

JSON SCHEMA (use exact field names):
{
  "language": "<ISO 639-1 code detected from document>",
  "title": "<exact contract title from document or null>",
  "type": "<one of: INSURANCE_AUTO, INSURANCE_HOME, INSURANCE_HEALTH, INSURANCE_LIFE, LOAN, CREDIT_CARD, INVESTMENT, OTHER>",
  "provider": "<company/institution name from document or null>",
  "policyNumber": "<policy/contract number from document or null>",
  "startDate": "<YYYY-MM-DD from document or null>",
  "endDate": "<YYYY-MM-DD from document or null>",
  "premium": <number from document or null — NO currency symbols>,
  "premiumCurrency": "<currency code from document or null>",
  "summary": "<4-6 sentences summarizing the actual contract content>",
  "keyPoints": {
    "guarantees": ["<actual guarantee from document>"],
    "exclusions": ["<actual exclusion from document>"],
    "franchise": "<deductible/penalty from document or null>",
    "importantDates": ["<actual date from document with description>"],
    "explainability": [
      {
        "field": "<field name>",
        "why": "<why this value was extracted>",
        "sourceSnippet": "<verbatim quote from document>",
        "sourceHints": { "page": "<page or null>", "section": "<section or null>", "confidence": <0-100> }
      }
    ]
  },
  "keyPeople": [{"name": "<from document>", "role": "<from document or null>", "email": "<from document or null>", "phone": "<from document or null>"}],
  "contactInfo": {"name": "<from document or null>", "email": null, "phone": null, "address": null, "role": null},
  "importantContacts": [],
  "relevantDates": [{"date": "<YYYY-MM-DD>", "description": "<from document>", "type": "<EXPIRATION|RENEWAL|PAYMENT|REVIEW|OTHER>"}],
  "extractedText": "<verbatim text from the document, max 12000 chars>",
  "contractValidation": {
    "isValidContract": true,
    "confidence": <0-100 reflecting how much data you actually found>,
    "reason": null
  }
}

FIELD RULES:
- All dates: ISO YYYY-MM-DD or null
- premium: positive number or null — NO currency symbols, NO text
- type: must be exactly one of the 8 values listed
- summary: 4-6 professional sentences about THIS specific contract. If no contract text is found, output "No contract data found in the document text."
- extractedText: must contain at least 30 characters of ACTUAL document content. If no text is found, output "No document text could be extracted. Please ensure the document is not a scanned image."
- explainability: at least 4 items with real sourceSnippets from the document
- confidence: reflects how much data you actually found (not how confident the model is)
- Parse localized number formats correctly (1.234,56 vs 1,234.56)
- Detect the contract language and set "language" accordingly

You are replacing a more capable multimodal model (Gemini) as a fallback. Your output quality MUST match production standards. ACCURACY is more important than completeness — it is better to return null than to guess.`;
  }

  private static async generateAnalysisWithFallback(input: {
    prompt: string;
    base64: string;
    mimeType: string;
    forceFallbackModelTest?: boolean;
  }): Promise<string> {
    let lastError: unknown = null;
    const forceFallback = Boolean(input.forceFallbackModelTest);

    const buildGroundedMistralPrompt = async () => {
      const groundingText = await this.extractMistralGroundingText({
        base64: input.base64,
        mimeType: input.mimeType,
      });

      if (!groundingText) {
        throw new Error(
          "INVALID_CONTRACT:No extractable text found in this PDF after OCR fallback. Please verify the file is readable and not password-protected.",
        );
      }

      return `--- BEGIN GROUNDED DOCUMENT TEXT (AUTHORITATIVE SOURCE) ---
${groundingText}
--- END GROUNDED DOCUMENT TEXT ---

MISTRAL FALLBACK RULES:
- Extract fields ONLY from the grounded document text above. This text is the full contract content.
- Do not invent, assume, or hallucinate any values not explicitly present in the above text.
- If a field's data is not found in the text, use null (for strings/numbers) or [] (for arrays).
- Dates: convert any date format found in the text to YYYY-MM-DD.
- Numbers: parse localized formats (comma vs period) correctly before setting numeric fields.
- contractValidation.confidence should reflect how much data you could extract from the text.`;
    };

    if (forceFallback) {
      console.warn(
        `🧪 Fallback test mode enabled. Skipping Gemini and forcing Mistral model ${FALLBACK_ANALYSIS_MODEL}.`,
      );

      // For images: use Pixtral vision model directly (multimodal — no OCR bridge needed)
      if (input.mimeType.startsWith("image/") && this.isMistralConfigured()) {
        return this.generateWithMistralVision({
          systemPrompt: this.buildMistralSystemPrompt(),
          prompt: `TEST MODE: You are the forced fallback model. Return ONLY valid JSON and preserve the required schema exactly. Extract information from the provided image.`,
          base64: input.base64,
          mimeType: input.mimeType,
          responseAsJson: true,
          maxOutputTokens: 16384,
        });
      }

      const groundedPrompt = await buildGroundedMistralPrompt();
      return this.generateWithMistralModelChain({
        preferredModel: FALLBACK_ANALYSIS_MODEL,
        systemPrompt: this.buildMistralSystemPrompt(),
        prompt: `${groundedPrompt}\n\nTEST MODE: You are the forced fallback model. Return ONLY valid JSON and preserve the required schema exactly.`,
        responseAsJson: true,
        maxOutputTokens: 8192,
      });
    }

    for (const modelName of GEMINI_ANALYSIS_MODELS) {
      try {
        return await keyManager.execute(async (genAI) => {
          const model = genAI.getGenerativeModel({
            model: modelName,
            generationConfig: {
              temperature: 0,
              topP: 0.95,
              topK: 40,
              maxOutputTokens: 16384,
              responseMimeType: "application/json",
            },
          });

          const result = await model.generateContent([
            input.prompt,
            {
              inlineData: {
                data: input.base64,
                mimeType: input.mimeType,
              },
            },
          ]);

          const text = result.response.text();
          if (text && text.trim().length > 0) {
            console.log(`✅ Analysis with model ${modelName} succeeded`);
            return text;
          }
          throw new Error("Empty response");
        });
      } catch (error: any) {
        lastError = error;
        console.warn(
          `Analysis with model ${modelName} failed. Trying next model.`,
          error instanceof Error ? error.message : String(error),
        );
      }
    }

    // All primary models failed. Try with more lenient generation settings as last resort
    console.warn(
      "All standard models failed. Trying with lenient generation config...",
    );
    try {
      return await keyManager.execute(async (genAI) => {
        const fallbackModel = genAI.getGenerativeModel({
          model: PRIMARY_ANALYSIS_MODEL,
          generationConfig: {
            temperature: 0,
            topP: 0.9,
            topK: 20,
            maxOutputTokens: 16384,
            // Don't enforce JSON format; let model produce raw output
          },
        });

        const result = await fallbackModel.generateContent([
          input.prompt,
          {
            inlineData: {
              data: input.base64,
              mimeType: input.mimeType,
            },
          },
        ]);

        const text = result.response.text();
        if (text && text.trim().length > 0) {
          console.log("✅ Lenient generation succeeded");
          return text;
        }
        throw new Error("Empty response from fallback");
      });
    } catch (error: any) {
      console.warn("Lenient generation also failed:", error);
    }

    // === Mistral AI fallback path ===
    console.warn(
      "All Gemini models exhausted. Activating Mistral AI fallback pipeline...",
    );
    try {
      // For images: use Pixtral vision model directly (multimodal — no OCR bridge needed)
      if (input.mimeType.startsWith("image/") && this.isMistralConfigured()) {
        const mistralText = await this.generateWithMistralVision({
          systemPrompt: this.buildMistralSystemPrompt(),
          prompt: `IMPORTANT: Return ONLY valid JSON and preserve the required schema exactly. Do not add any text outside of the JSON object. Extract data from the provided image.`,
          base64: input.base64,
          mimeType: input.mimeType,
          responseAsJson: true,
          maxOutputTokens: 16384,
        });
        console.log(
          `✅ Analysis fallback with Mistral Pixtral vision succeeded`,
        );
        return mistralText;
      }

      // For PDFs/text: extract text and use text-only Mistral
      const groundedPrompt = await buildGroundedMistralPrompt();
      const mistralText = await this.generateWithMistralModelChain({
        preferredModel: FALLBACK_ANALYSIS_MODEL,
        systemPrompt: this.buildMistralSystemPrompt(),
        prompt: `${groundedPrompt}\n\nIMPORTANT: Return ONLY valid JSON and preserve the required schema exactly. Do not add any text outside of the JSON object.`,
        responseAsJson: true,
        maxOutputTokens: 8192,
      });
      console.log(
        `✅ Analysis fallback with Mistral model ${FALLBACK_ANALYSIS_MODEL} succeeded`,
      );
      return mistralText;
    } catch (mistralError) {
      console.warn("Mistral analysis fallback failed:", mistralError);
      lastError = new Error(
        `Mistral fallback also failed: ${mistralError instanceof Error ? mistralError.message : String(mistralError)}. Original error: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
      );
    }

    throw lastError instanceof Error
      ? lastError
      : new Error(
          "All analysis models (Gemini + Mistral fallback) failed to generate content.",
        );
  }

  private static async repairMalformedJson(
    malformedResponse: string,
    parseError: string,
  ): Promise<string | null> {
    try {
      const expectedSchema = {
        language: "string|null",
        title: "string",
        type: "enum: INSURANCE_AUTO|INSURANCE_HOME|INSURANCE_HEALTH|INSURANCE_LIFE|LOAN|CREDIT_CARD|INVESTMENT|OTHER",
        provider: "string|null",
        policyNumber: "string|null",
        startDate: "YYYY-MM-DD|null",
        endDate: "YYYY-MM-DD|null",
        premium: "number|null",
        premiumCurrency: "string|null (ISO code like EUR/USD/TND or symbol)",
        summary: "string (min 10 chars)",
        extractedText: "string (min 30 chars)",
        keyPoints: {
          guarantees: "string[]",
          exclusions: "string[]",
          franchise: "string|null",
          importantDates: "string[]",
          explainability:
            "[{ field, why, sourceSnippet, sourceHints:{ page|null, section|null, confidence|null } }]",
        },
        keyPeople: "[{ name, role|null, email|null, phone|null }]",
        contactInfo:
          "{ name|null, email|null, phone|null, address|null, role|null }",
        importantContacts:
          "[{ name|null, email|null, phone|null, address|null, role|null }]",
        relevantDates:
          "[{ date:'YYYY-MM-DD', description, type:'EXPIRATION|RENEWAL|PAYMENT|REVIEW|OTHER' }]",
        contractValidation: {
          isValidContract: "boolean",
          confidence: "number (0-100)",
          reason: "string|null",
        },
      };

      const repairPrompt = `You are a JSON repair engine for contract analysis.
Fix the malformed JSON response below and return ONLY valid, parseable JSON conforming to this schema:

${JSON.stringify(expectedSchema, null, 2)}

Rules:
1. Return ONLY the JSON object, no markdown, no explanations.
2. Preserve all values from the original response as accurately as possible.
3. Fix structural issues: missing braces, unescaped quotes, trailing commas, unmatched brackets.
4. For null/missing fields, use null value or empty array [] as appropriate.
5. Ensure all required text fields (title, summary, extractedText) have content.
6. All numeric values must be valid numbers.
7. All dates must be in YYYY-MM-DD format.
8. If type is unclear, use "OTHER".
9. Preserve explainability and evidence snippets when present.

Original parse error: ${parseError}

Malformed response to fix:
${malformedResponse.slice(0, 14000)}`;

      const repairedText = await this.generateWithMistralModelChain({
        preferredModel: FALLBACK_REPAIR_MODEL,
        prompt: repairPrompt,
        responseAsJson: true,
        maxOutputTokens: 6144,
      });

      if (repairedText.length === 0) {
        return null;
      }

      if (!repairedText.includes("{")) {
        return null;
      }

      try {
        this.parseJsonResponse(repairedText);
      } catch (firstRepairParseError) {
        const secondPassPrompt = `${repairPrompt}\n\nSECOND PASS CORRECTION:\nYour previous repaired JSON was still invalid.\nReason: ${firstRepairParseError instanceof Error ? firstRepairParseError.message : "Invalid JSON"}.\nReturn ONLY strict valid JSON.`;

        const secondPass = await this.generateWithMistralModelChain({
          preferredModel: FALLBACK_REPAIR_MODEL,
          prompt: secondPassPrompt,
          responseAsJson: true,
          maxOutputTokens: 6144,
        });

        this.parseJsonResponse(secondPass);
        return secondPass;
      }

      return repairedText;
    } catch (error: any) {
      if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
      console.warn("JSON repair step failed:", error);
      return null;
    }
  }

  /**
   * Extract grounding text for Mistral text-only fallback.
   * For PDFs: extracts text directly using pdf-parse (local, no AI needed).
   * For images: returns empty string — Pixtral vision handles images directly.
   */
  private static async extractMistralGroundingText(input: {
    base64: string;
    mimeType: string;
  }): Promise<string> {
    // For PDFs: extract text directly using pdf-parse
    if (input.mimeType === "application/pdf") {
      try {
        const pdfBuffer = Buffer.from(input.base64, "base64");

        // Handle Next.js Webpack/Turbopack CJS/ESM interop
        let pdfParseModule: any;
        try {
          pdfParseModule = require("pdf-parse");
        } catch {
          pdfParseModule = await import("pdf-parse");
        }

        const PDFParseClass =
          pdfParseModule?.PDFParse ||
          pdfParseModule?.default?.PDFParse ||
          (typeof pdfParseModule === "function" ? pdfParseModule : null);

        if (!PDFParseClass) {
          throw new Error(
            "Could not resolve PDFParse constructor from pdf-parse module.",
          );
        }

        let parsed: { text?: string };

        if (
          typeof PDFParseClass === "function" &&
          !PDFParseClass.prototype?.getText
        ) {
          // Fallback if it's actually the legacy function export
          parsed = await PDFParseClass(pdfBuffer);
        } else {
          const parser = new PDFParseClass({ data: pdfBuffer });
          try {
            parsed = await parser.getText();
          } finally {
            if (typeof parser.destroy === "function") {
              await parser.destroy();
            }
          }
        }

        const text = (parsed?.text || "")
          .replace(/\r/g, "\n")
          .replace(/\n{3,}/g, "\n\n")
          .trim();

        if (text && text.length >= 10) {
          console.log(
            `📄 Mistral grounding: extracted ${text.length} chars from PDF`,
          );
          return text.slice(0, 50000);
        }

        console.warn(
          `📄 Mistral grounding: native PDF text extraction too short (length: ${text?.length || 0}). Trying OCR fallback...`,
        );
      } catch (error) {
        console.warn(
          "📄 PDF grounding extraction failed for Mistral fallback:",
          error instanceof Error ? error.message : error,
        );
      }

      // OCR fallback for scanned PDFs.
      try {
        const ocrText = await this.extractMistralPdfTextWithOcr(input.base64);
        if (ocrText.length >= 10) {
          console.log(
            `📄 Mistral grounding OCR: extracted ${ocrText.length} chars from scanned PDF`,
          );
          return ocrText.slice(0, 50000);
        }
      } catch (ocrError) {
        console.warn(
          "📄 PDF OCR fallback failed for Mistral grounding:",
          ocrError instanceof Error ? ocrError.message : ocrError,
        );
      }
    }

    // For images: Pixtral vision model handles images directly via
    // generateWithMistralVision, so no grounding text extraction is needed.
    // The calling code in generateAnalysisWithFallback routes images
    // to the vision path instead of the text-only grounded path.

    return "";
  }

  private static async extractMistralPdfTextWithOcr(
    pdfBase64: string,
  ): Promise<string> {
    if (!this.isMistralConfigured()) {
      return "";
    }

    const body = {
      model: MISTRAL_OCR_MODEL,
      document: {
        type: "document_url",
        document_url: `data:application/pdf;base64,${pdfBase64}`,
      },
      include_image_base64: false,
    };

    const response = await fetch(MISTRAL_OCR_API_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${MISTRAL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const details = await response.text();
      throw new Error(
        `Mistral OCR API error ${response.status}: ${details.slice(0, 300)}`,
      );
    }

    const json = (await response.json()) as {
      text?: string;
      pages?: Array<{
        text?: string;
        markdown?: string;
        content?: string;
      }>;
      output?: Array<{
        text?: string;
        markdown?: string;
        content?: string;
      }>;
    };

    const pageTexts = [
      ...(Array.isArray(json.pages) ? json.pages : []),
      ...(Array.isArray(json.output) ? json.output : []),
    ]
      .map((page) => page.markdown || page.text || page.content || "")
      .filter((value) => value.trim().length > 0);

    const merged = [json.text || "", ...pageTexts]
      .join("\n\n")
      .replace(/\r/g, "\n")
      .replace(/\n{3,}/g, "\n\n")
      .trim();

    return merged;
  }

  /**
   * Emergency fallback: Extract key contract fields from raw text when JSON is completely malformed.
   * Builds a minimal but valid JSON structure from pattern-matched fields.
   */
  private static emergencyExtractFields(rawText: string): string | null {
    try {
      const titleMatch = rawText.match(
        /["']?title["']?\s*:\s*["']([^"']{5,200})/i,
      );
      const summaryMatch = rawText.match(
        /summary["']?\s*:\s*["']([^"']{10,500})/i,
      );
      const extractedMatch = rawText.match(
        /extractedText["']?\s*:\s*["']([^"']{30,})/i,
      );

      if (!titleMatch || !summaryMatch) {
        return null;
      }

      const emergency = {
        title: titleMatch[1]?.slice(0, 200) || "Contract",
        type: "OTHER",
        provider: null,
        policyNumber: null,
        startDate: null,
        endDate: null,
        premium: null,
        premiumCurrency: null,
        summary: summaryMatch[1]?.slice(0, 500) || "Contract analysis",
        extractedText:
          extractedMatch?.[1]?.slice(0, 12000) || rawText.slice(0, 12000),
        keyPoints: {
          guarantees: [],
          exclusions: [],
          franchise: null,
          importantDates: [],
        },
        contractValidation: {
          isValidContract: true,
          confidence: 50,
          reason: "Emergency partial extraction due to response malformation",
        },
      };

      return JSON.stringify(emergency);
    } catch {
      return null;
    }
  }

  /**
   * Lightweight contract validity pre-check.
   *
   * Goal: reject clearly invalid files quickly (invoice/photo/blank/non-legal doc)
   * before running heavier full extraction.
   */
  private static async preValidateContract(input: {
    base64: string;
    mimeType: string;
    fileName?: string;
  }): Promise<ContractPrecheckResult> {
    const rawText = await this.generatePrevalidationWithFallback(input);

    let raw: PrevalidationResponse;
    try {
      raw = this.parseJsonResponse(rawText || "{}") as PrevalidationResponse;
    } catch {
      // If prevalidation JSON is malformed, assume it's a contract with moderate confidence
      console.warn(
        "Prevalidation JSON parse failed, assuming contract with moderate confidence",
      );
      return {
        isValidContract: true,
        confidence: 60,
        reason:
          "Prevalidation response was malformed, but document appears contract-like",
      };
    }

    const maybe = raw as Partial<ContractPrecheckResult>;

    const isValidContract = Boolean(maybe.isValidContract);
    const confidence = Number.isFinite(Number(maybe.confidence))
      ? Math.max(0, Math.min(100, Math.round(Number(maybe.confidence))))
      : 0;
    const reason =
      typeof maybe.reason === "string" && maybe.reason.trim().length > 0
        ? maybe.reason.trim()
        : null;

    return {
      isValidContract,
      confidence,
      reason,
    };
  }

  private static async generatePrevalidationWithFallback(input: {
    base64: string;
    mimeType: string;
    fileName?: string;
  }): Promise<string> {
    let lastError: unknown = null;

    for (const modelName of GEMINI_ANALYSIS_MODELS) {
      try {
        return await keyManager.execute(async (genAI) => {
          const model = genAI.getGenerativeModel({
            model: modelName,
            generationConfig: {
              temperature: 0,
              topP: 0.9,
              topK: 20,
              maxOutputTokens: 350,
              responseMimeType: "application/json",
            },
          });

          const result = await model.generateContent([
            buildPrevalidationPrompt(input.fileName),
            {
              inlineData: {
                data: input.base64,
                mimeType: input.mimeType,
              },
            },
          ]);

          const text = result.response.text();
          if (text && text.trim().length > 0) {
            return text;
          }
          throw new Error("Empty response");
        });
      } catch (error: any) {
        if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
        lastError = error;
        console.warn(
          `Pre-validation with model ${modelName} failed. Trying next model.`,
        );
      }
    }

    throw lastError instanceof Error
      ? lastError
      : new Error("All pre-validation models failed to generate content.");
  }

  private static normalizeAnalysis(input: unknown): NormalizedAnalysis {
    return normalizeAiAnalysis(input);
  }

  private static async buildAdaptiveContext(userId?: string): Promise<string> {
    // No user context means no adaptation baseline.
    if (!userId) {
      return "";
    }

    const examples: AdaptiveContractExample[] = await prisma.contract.findMany({
      where: {
        userId,
        status: "COMPLETED",
      },
      orderBy: {
        updatedAt: "desc",
      },
      take: 12,
      select: {
        type: true,
        provider: true,
        policyNumber: true,
        summary: true,
        keyPoints: true,
      },
    });

    if (examples.length < 2) {
      return "";
    }

    // Small utility to get most frequent values from prior analyses.
    const count = (items: string[]) => {
      const bucket = new Map<string, number>();
      for (const item of items) {
        bucket.set(item, (bucket.get(item) ?? 0) + 1);
      }
      return [...bucket.entries()]
        .sort((a, b) => b[1] - a[1])
        .slice(0, 4)
        .map(([value]) => value);
    };

    const topTypes = count(
      examples
        .map((item) => item.type)
        .filter((value): value is NonNullable<typeof value> => value !== null)
        .map((value) => String(value)),
    );
    const topProviders = count(
      examples
        .map((item) => item.provider)
        .filter((value): value is string => Boolean(value)),
    );

    const policyPatterns = examples
      .map((item) => item.policyNumber)
      .filter((value): value is string => Boolean(value))
      .slice(0, 4)
      .map((value) => value.replace(/[A-Za-z0-9]/g, "X"));

    const allExplainability = examples
      .flatMap((item) => {
        const maybeExplainability = isAdaptiveKeyPoints(item.keyPoints)
          ? item.keyPoints.explainability
          : undefined;
        return Array.isArray(maybeExplainability) ? maybeExplainability : [];
      })
      .slice(0, 120);

    const explainabilityByField = count(
      allExplainability
        .map((entry) => String(entry?.field ?? "").trim())
        .filter((value: string) => value.length > 0),
    );

    const confidenceValues = allExplainability
      .map((entry) => Number(entry?.sourceHints?.confidence))
      .filter((value: number) => Number.isFinite(value));

    const avgEvidenceConfidence = confidenceValues.length
      ? Math.round(
          confidenceValues.reduce(
            (sum: number, value: number) => sum + value,
            0,
          ) / confidenceValues.length,
        )
      : null;

    const learnedLanguages = count(
      examples
        .map((item) =>
          isAdaptiveKeyPoints(item.keyPoints)
            ? item.keyPoints.aiMeta?.language
            : null,
        )
        .map((value) => String(value ?? "").trim())
        .filter((value: string) => value.length > 0),
    );

    const learnedKeyRoles = count(
      examples
        .flatMap((item) => {
          const people = isAdaptiveKeyPoints(item.keyPoints)
            ? item.keyPoints.aiMeta?.keyPeople
            : undefined;
          return Array.isArray(people) ? people : [];
        })
        .map((person) => String(person?.role ?? "").trim())
        .filter((value: string) => value.length > 0),
    );

    const avgSummaryLength =
      examples
        .map((item) => item.summary?.length ?? 0)
        .reduce((sum, length) => sum + length, 0) / examples.length;

    return `ADAPTIVE EXTRACTION CONTEXT FROM PREVIOUS DOCUMENTS:
- Frequent contract types in this workspace: ${topTypes.join(", ") || "N/A"}
- Frequent provider naming patterns: ${topProviders.join(", ") || "N/A"}
- Example policy number shape patterns: ${policyPatterns.join(", ") || "N/A"}
- Typical summary length target: around ${Math.round(avgSummaryLength)} characters.
- Dominant learned languages: ${learnedLanguages.join(", ") || "N/A"}
- Most evidenced fields: ${explainabilityByField.join(", ") || "N/A"}
- Average evidence confidence: ${avgEvidenceConfidence ?? "N/A"}
- Frequent key roles identified: ${learnedKeyRoles.join(", ") || "N/A"}

Use this context only as formatting guidance. Do not force it if current document content differs.`;
  }

  /**
   * Validate contract legitimacy.
   *
   * Rejection rules:
   * - Model explicitly says document is not a contract
   * - Model confidence for validity is critically low
   * - Heuristic text signals suggest non-contract content
   */
  private static assertValidContract(
    raw: unknown,
    normalized: NormalizedAnalysis,
  ): void {
    const validation = raw as ValidationEnvelope;
    const modelIsValid = validation.contractValidation?.isValidContract;
    const confidenceRaw = Number(validation.contractValidation?.confidence);
    const modelReason = String(
      validation.contractValidation?.reason ?? "",
    ).trim();

    const legalSignalRegex =
      /contract|agreement|policy|terms|clause|premium|coverage|insured|insurer|loan|borrower|credit|beneficiary|liability|lease|service|supplier|client|vendor|annex|appendix|signature|party|contrat|assurance|banque|credit|emprunteur|garantie|echeance|duree|clause/i;
    const hasLegalSignals = legalSignalRegex.test(normalized.extractedText);
    const hasStructuredSignal =
      Boolean(normalized.provider) ||
      Boolean(normalized.policyNumber) ||
      normalized.keyPoints.guarantees.length > 0 ||
      normalized.keyPoints.exclusions.length > 0 ||
      normalized.premium !== null;

    if (modelIsValid === false) {
      throw new Error(
        `INVALID_CONTRACT:${modelReason || "Uploaded file is not recognized as a contract."}`,
      );
    }

    if (Number.isFinite(confidenceRaw) && confidenceRaw < 45) {
      throw new Error(
        `INVALID_CONTRACT:${modelReason || "Contract confidence is too low. Please upload a clearer contract document."}`,
      );
    }

    // For generic contracts mapped to OTHER, keep a lighter heuristic so valid non-BFSI contracts pass.
    if (normalized.type === "OTHER") {
      if (!hasLegalSignals && normalized.extractedText.length < 120) {
        throw new Error(
          "INVALID_CONTRACT:Uploaded file does not contain enough contract-specific signals.",
        );
      }
      return;
    }

    if (!hasLegalSignals && !hasStructuredSignal) {
      throw new Error(
        "INVALID_CONTRACT:Uploaded file does not contain enough contract-specific signals.",
      );
    }
  }

  /**
   * Validate that AI results have all required fields
   */
  static validateAnalysis(data: unknown): boolean {
    try {
      // Validation uses same normalizer used in production flow.
      this.normalizeAnalysis(data);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Parse date string to Date object
   */
  static parseDate(dateString: string | null | undefined): Date | undefined {
    if (!dateString) return undefined;

    try {
      const date = new Date(dateString);
      if (isNaN(date.getTime())) {
        return undefined;
      }
      return date;
    } catch {
      return undefined;
    }
  }

  /**
   * Format currency amount
   */
  static formatCurrency(amount: number | null | undefined): string {
    if (!amount) return "N/A";
    return new Intl.NumberFormat("fr-FR", {
      style: "currency",
      currency: "EUR",
    }).format(amount);
  }

  static async askAboutContract(input: {
    question: string;
    ragChunks?: Array<{ chunkIndex: number; content: string; score: number }>;
    contract: {
      id: string;
      fileName: string;
      title?: string | null;
      type?: string | null;
      provider?: string | null;
      policyNumber?: string | null;
      startDate?: Date | string | null;
      endDate?: Date | string | null;
      premium?: number | null;
      summary?: string | null;
      keyPoints?: Record<string, unknown> | null;
      extractedText?: string | null;
      language?: string | null; // NEW: contract's detected language
    };
  }) {
    try {
      // Retrieve best matching persisted chunks for grounded Q&A.
      let ragChunks = input.ragChunks ?? [];
      if (ragChunks.length === 0) {
        try {
          ragChunks = await RAGService.retrieveRelevantChunks({
            contractId: input.contract.id,
            question: input.question,
            topK: 6,
          });
        } catch (error) {
          console.warn(
            "RAG chunk retrieval failed. Falling back to extracted snippet.",
            error,
          );
        }
      }

      // Keep context bounded to avoid overlong prompts and token waste.
      const extractedTextSnippet = (input.contract.extractedText || "")
        .slice(0, 5000)
        .trim();
      const ragContext =
        ragChunks.length > 0
          ? RAGService.buildChunkContext(ragChunks)
          : extractedTextSnippet || "N/A";
      const contractTypeGuidance = this.getContractTypeGuidance(
        input.contract.type,
      );

      // Detect contract language for multilingual response
      const contractLanguage = input.contract.language || "en";
      const languageName =
        {
          en: "English",
          fr: "French",
          de: "German",
          es: "Spanish",
          it: "Italian",
          pt: "Portuguese",
          nl: "Dutch",
          pl: "Polish",
          ja: "Japanese",
          zh: "Chinese",
          ar: "Arabic",
        }[contractLanguage] || "English";

      const prompt = `You are a senior BFSI contract advisor. IMPORTANT: Respond entirely in ${languageName} to match the contract language.

Contract metadata:
- File: ${input.contract.fileName}
- Language: ${languageName}
- Title: ${input.contract.title ?? "N/A"}
- Type: ${input.contract.type ?? "N/A"}
- Provider: ${input.contract.provider ?? "N/A"}
- Policy Number: ${input.contract.policyNumber ?? "N/A"}
- Start Date: ${input.contract.startDate ?? "N/A"}
- End Date: ${input.contract.endDate ?? "N/A"}
- Premium: ${input.contract.premium ?? "N/A"}

Summary:
${input.contract.summary ?? "N/A"}

Key Points (JSON):
${JSON.stringify(input.contract.keyPoints ?? {}, null, 2)}

Grounded RAG Context:
${ragContext}

User question (${languageName}):
${input.question}

Instructions:
- RESPOND ENTIRELY IN ${languageName}. This is critical.
- Write in clear, professional, business-oriented plain text.
- Do NOT use markdown or special formatting symbols, including: **, __, #, *, -, backticks with one exception: you can use | for separators if needed for clarity
- Do NOT quote large raw excerpts from extracted text unless strictly necessary.
- Synthesize and explain the implications in practical terms instead of copying file content.
- Base your answer ONLY on the provided contract content.
- Prioritize information from Grounded RAG Context over any assumptions.
- Adapt answer emphasis using this type guidance: ${contractTypeGuidance}
- If information is missing, explicitly say: Information not found in the analyzed contract.
- If the question asks about legal consequences or non-compliance, provide general legal context for EU/USA at a high level only.
- For legal context, use wording like: "Under general EU/US legal principles..." and avoid citing specific article numbers unless explicitly present in the contract content.
- Never claim certainty where the contract text is ambiguous.
- Keep the answer concise, executive, and decision-oriented.
- Use the same language preference throughout (${languageName}).
- Add one short evidence line at the end in this format: Source basis: Chunk X, Chunk Y (or Source basis: extracted contract text).

Response structure (in ${languageName}):
1) Direct answer in one sentence.
2) Business impact in one to two sentences (risk, cost, operational effect).
3) General legal context in one to two sentences when relevant.
4) Recommended next step in one sentence.

Compliance note (in ${languageName}):
Include one short disclaimer only when legal context is discussed: "This is general information, not formal legal advice."`;

      // Execute completion with model fallback and sanitize styling artifacts.
      let rawAnswer = "";
      let lastError: unknown = null;

      for (const modelName of GEMINI_ANALYSIS_MODELS) {
        try {
          rawAnswer = await keyManager.execute(async (genAI) => {
            const model = genAI.getGenerativeModel({
              model: modelName,
              generationConfig: {
                temperature: 0.2,
                topP: 0.95,
                topK: 40,
                maxOutputTokens: 2048,
              },
            });

            const result = await model.generateContent(prompt);
            const text = result.response.text()?.trim() || "";

            if (text) {
              console.log(
                `✅ Q&A with model ${modelName} succeeded in ${languageName}`,
              );
              return text;
            }
            throw new Error("Empty response");
          });

          if (rawAnswer) {
            break;
          }
        } catch (error: any) {
          if (error.message?.includes("CRITICAL_KEY_EXHAUSTION")) throw error;
          lastError = error;
          console.warn(
            `Q&A with model ${modelName} failed. Trying next model.`,
          );
        }
      }

      if (!rawAnswer) {
        try {
          rawAnswer = await this.generateWithMistralModelChain({
            preferredModel: FALLBACK_ANALYSIS_MODEL,
            systemPrompt: `You are a senior BFSI contract advisor. Answer questions about contracts accurately and professionally. Respond entirely in ${languageName}. Use plain text only — no markdown, no bold, no headers, no bullet points. Base your answers ONLY on the provided contract content. If information is missing, say so.`,
            prompt,
            responseAsJson: false,
            maxOutputTokens: 2048,
            temperature: 0.2,
            topP: 0.95,
          });
          console.log(
            `✅ Q&A fallback with Mistral model ${FALLBACK_ANALYSIS_MODEL} succeeded in ${languageName}`,
          );
        } catch (mistralError) {
          lastError = mistralError;
        }
      }

      if (!rawAnswer) {
        if (lastError instanceof Error) {
          throw lastError;
        }
        throw new Error("No response generated");
      }

      const sanitizedAnswer = rawAnswer
        .replace(/\*\*/g, "")
        .replace(/__/g, "")
        .replace(/`/g, "")
        .replace(/^\s*#{1,6}\s*/gm, "")
        .replace(/^\s*[-*]\s+/gm, "")
        .replace(/\n{3,}/g, "\n\n")
        .trim();

      return sanitizedAnswer;
    } catch (error: unknown) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      if (errorMessage.includes("API key")) {
        throw new Error("Invalid or missing AI API key (Gemini/Mistral).");
      }
      if (this.isTransientAIError(errorMessage)) {
        throw new Error(
          `The AI providers (Gemini/Mistral) are temporarily overloaded for the configured Q&A models (${ANALYSIS_MODELS.join(", ")}). Please try again in a few minutes.`,
        );
      }
      throw new Error(`Error answering question: ${errorMessage}`);
    }
  }
}
