import "server-only";

import { z } from "zod";
import { analyzeNormativeText, type NormativeAnalysisInput, type NormativeAnalysisResult } from "@/lib/normative-analysis/analyze";

const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
const DEFAULT_TIMEOUT_MS = 75_000;
const DEFAULT_MAX_TEXT_CHARS = 55_000;
const MAX_WARNING_ISSUES = 4;

const DOCUMENT_TYPES = ["BASES_LICITACION", "CONVOCATORIA", "ANEXOS", "REGLAMENTO", "LEY", "OTRO"] as const;
const VIABILITY_LEVELS = ["alta", "media", "baja"] as const;
const RISK_LEVELS = ["alto", "medio", "bajo"] as const;
const PRIORITY_LEVELS = ["alta", "media", "baja"] as const;
const CONFIDENCE_LEVELS = ["low", "medium", "high"] as const;

type OpenAiChatCompletionResponse = {
  choices?: Array<{
    message?: {
      content?: string | null;
    };
  }>;
  model?: string;
  usage?: {
    prompt_tokens?: number;
    completion_tokens?: number;
    total_tokens?: number;
  };
  error?: {
    message?: string;
  };
};

type NormativeAiUsage = {
  promptTokens: number | null;
  completionTokens: number | null;
  totalTokens: number | null;
};

export type NormativeAiEngine = "openai_json_schema" | "openai_json_object" | "heuristic_fallback";

export type NormativeAiAnalysisOutput = {
  result: NormativeAnalysisResult;
  engine: NormativeAiEngine;
  model: string | null;
  warnings: string[];
  usage: NormativeAiUsage | null;
};

const ChecklistItemSchema = z.object({
  title: z.string().min(3).max(180),
  description: z.string().min(10).max(1500),
});

const CriticalItemSchema = z.object({
  title: z.string().min(3).max(180),
  description: z.string().min(10).max(1500),
  severity: z.enum(["alto", "medio", "bajo"]),
});

const LockInItemSchema = z.object({
  title: z.string().min(3).max(220),
  impact: z.string().min(10).max(1800),
  recommendation: z.string().min(10).max(1800),
});

const PreparationRecommendationSchema = z.object({
  title: z.string().min(3).max(220),
  description: z.string().min(10).max(1600),
  priority: z.enum(["alta", "media", "baja"]),
});

const SourceTraceabilitySchema = z.object({
  documentType: z.enum(["BASES_LICITACION", "CONVOCATORIA", "ANEXOS", "REGLAMENTO", "LEY", "OTRO"]),
  sourceLabel: z.string().min(2).max(120),
  fields: z.array(z.string().min(2).max(120)).max(40),
});

const NormativeAiResultSchema: z.ZodType<NormativeAnalysisResult> = z.object({
  executiveSummary: z.string().min(40).max(6000),
  participationViability: z.object({
    score: z.number().int().min(0).max(100),
    level: z.enum(["alta", "media", "baja"]),
    rationale: z.string().min(10).max(1200),
  }),
  risk: z.object({
    level: z.enum(["alto", "medio", "bajo"]),
    description: z.string().min(10).max(1200),
  }),
  requiredChecklist: z.array(ChecklistItemSchema).min(3).max(20),
  criticalRequirements: z.array(CriticalItemSchema).min(1).max(10),
  lockIns: z.array(LockInItemSchema).max(8),
  preparationRecommendations: z.array(PreparationRecommendationSchema).min(3).max(10),
  officialFormats: z.array(z.string().min(10).max(500)).min(3).max(25),
  valueForMoney: z.object({
    score: z.number().int().min(0).max(100),
    signals: z.array(z.string().min(10).max(900)).max(8),
    actions: z.array(z.string().min(10).max(900)).max(8),
  }),
  circularEconomy: z.object({
    applicable: z.boolean(),
    signals: z.array(z.string().min(10).max(900)).max(8),
    actions: z.array(z.string().min(10).max(900)).max(8),
  }),
  sourceTraceability: z.array(SourceTraceabilitySchema).max(20),
  confidence: z.enum(["low", "medium", "high"]),
});

const NormativeAiResponseSchema = z.object({
  analysis: NormativeAiResultSchema,
});

const NormativeAiJsonSchema = {
  name: "normative_analysis_result",
  strict: true,
  schema: {
    type: "object",
    additionalProperties: false,
    required: ["analysis"],
    properties: {
      analysis: {
        type: "object",
        additionalProperties: false,
        required: [
          "executiveSummary",
          "participationViability",
          "risk",
          "requiredChecklist",
          "criticalRequirements",
          "lockIns",
          "preparationRecommendations",
          "officialFormats",
          "valueForMoney",
          "circularEconomy",
          "sourceTraceability",
          "confidence",
        ],
        properties: {
          executiveSummary: { type: "string" },
          participationViability: {
            type: "object",
            additionalProperties: false,
            required: ["score", "level", "rationale"],
            properties: {
              score: { type: "integer", minimum: 0, maximum: 100 },
              level: { type: "string", enum: ["alta", "media", "baja"] },
              rationale: { type: "string" },
            },
          },
          risk: {
            type: "object",
            additionalProperties: false,
            required: ["level", "description"],
            properties: {
              level: { type: "string", enum: ["alto", "medio", "bajo"] },
              description: { type: "string" },
            },
          },
          requiredChecklist: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["title", "description"],
              properties: {
                title: { type: "string" },
                description: { type: "string" },
              },
            },
          },
          criticalRequirements: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["title", "description", "severity"],
              properties: {
                title: { type: "string" },
                description: { type: "string" },
                severity: { type: "string", enum: ["alto", "medio", "bajo"] },
              },
            },
          },
          lockIns: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["title", "impact", "recommendation"],
              properties: {
                title: { type: "string" },
                impact: { type: "string" },
                recommendation: { type: "string" },
              },
            },
          },
          preparationRecommendations: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["title", "description", "priority"],
              properties: {
                title: { type: "string" },
                description: { type: "string" },
                priority: { type: "string", enum: ["alta", "media", "baja"] },
              },
            },
          },
          officialFormats: {
            type: "array",
            items: { type: "string" },
          },
          valueForMoney: {
            type: "object",
            additionalProperties: false,
            required: ["score", "signals", "actions"],
            properties: {
              score: { type: "integer", minimum: 0, maximum: 100 },
              signals: { type: "array", items: { type: "string" } },
              actions: { type: "array", items: { type: "string" } },
            },
          },
          circularEconomy: {
            type: "object",
            additionalProperties: false,
            required: ["applicable", "signals", "actions"],
            properties: {
              applicable: { type: "boolean" },
              signals: { type: "array", items: { type: "string" } },
              actions: { type: "array", items: { type: "string" } },
            },
          },
          sourceTraceability: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["documentType", "sourceLabel", "fields"],
              properties: {
                documentType: { type: "string", enum: ["BASES_LICITACION", "CONVOCATORIA", "ANEXOS", "REGLAMENTO", "LEY", "OTRO"] },
                sourceLabel: { type: "string" },
                fields: { type: "array", items: { type: "string" } },
              },
            },
          },
          confidence: { type: "string", enum: ["low", "medium", "high"] },
        },
      },
    },
  },
} as const;

function parsePositiveInteger(value: string | undefined, fallback: number) {
  const parsed = Number.parseInt((value ?? "").trim(), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function getOpenAiApiKey() {
  return process.env.OPENAI_API_KEY?.trim() || process.env.API_KEY?.trim() || process.env.api_key?.trim() || "";
}

function clampInputText(fullText: string) {
  const maxChars = parsePositiveInteger(process.env.OPENAI_NORMATIVE_MAX_CHARS, DEFAULT_MAX_TEXT_CHARS);
  if (fullText.length <= maxChars) {
    return fullText;
  }

  return `${fullText.slice(0, maxChars)}\n\n[TEXT_TRUNCATED_TO_${maxChars}_CHARS]`;
}

function extractJsonObject(rawContent: string) {
  const trimmed = rawContent.trim();

  try {
    return JSON.parse(trimmed) as unknown;
  } catch {
    // Continue with secondary parsing.
  }

  const withoutFence = trimmed
    .replace(/^```json\s*/i, "")
    .replace(/^```\s*/i, "")
    .replace(/\s*```$/i, "")
    .trim();

  try {
    return JSON.parse(withoutFence) as unknown;
  } catch {
    // Continue with bracket extraction.
  }

  const firstBrace = withoutFence.indexOf("{");
  const lastBrace = withoutFence.lastIndexOf("}");

  if (firstBrace >= 0 && lastBrace > firstBrace) {
    return JSON.parse(withoutFence.slice(firstBrace, lastBrace + 1)) as unknown;
  }

  throw new Error("La respuesta de OpenAI no contiene un JSON valido.");
}

function isObject(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function compactText(value: string) {
  return value.replace(/\s+/g, " ").trim();
}

function readString(value: unknown) {
  if (typeof value !== "string") {
    return null;
  }

  const normalized = compactText(value);
  return normalized.length > 0 ? normalized : null;
}

function truncate(value: string, maxLength: number) {
  return value.length <= maxLength ? value : value.slice(0, maxLength).trim();
}

function readArray(value: unknown) {
  return Array.isArray(value) ? value : [];
}

function readNumber(value: unknown, fallback: number) {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }

  if (typeof value === "string") {
    const parsed = Number.parseFloat(value.trim());
    if (Number.isFinite(parsed)) {
      return parsed;
    }
  }

  return fallback;
}

function readBoolean(value: unknown, fallback: boolean) {
  if (typeof value === "boolean") {
    return value;
  }

  if (typeof value === "string") {
    const normalized = value.trim().toLowerCase();
    if (normalized === "true" || normalized === "1" || normalized === "si" || normalized === "sí") {
      return true;
    }
    if (normalized === "false" || normalized === "0" || normalized === "no") {
      return false;
    }
  }

  return fallback;
}

function readEnum<T extends string>(value: unknown, values: readonly T[], fallback: T): T {
  if (typeof value !== "string") {
    return fallback;
  }

  const normalized = value.trim().toLowerCase();
  const match = values.find((candidate) => candidate.toLowerCase() === normalized);
  return match ?? fallback;
}

function uniqueByText<T>(items: T[], selector: (item: T) => string, maxItems: number) {
  const seen = new Set<string>();
  const result: T[] = [];

  for (const item of items) {
    const key = selector(item);
    if (!key || seen.has(key)) {
      continue;
    }

    seen.add(key);
    result.push(item);
    if (result.length >= maxItems) {
      break;
    }
  }

  return result;
}

function normalizeStringArray(value: unknown, fallback: string[], maxItems: number, minLength = 1) {
  const collected = readArray(value)
    .map((item) => readString(item))
    .filter((item): item is string => typeof item === "string" && item.length >= minLength);
  const source = collected.length > 0 ? collected : fallback;
  const normalized = source.map((item) => truncate(item, 900));
  return uniqueByText(normalized, (item) => item.toLowerCase(), maxItems);
}

function normalizeChecklist(value: unknown, fallback: NormativeAnalysisResult["requiredChecklist"]) {
  const collected = readArray(value)
    .map((item) => {
      if (!isObject(item)) {
        return null;
      }

      const title = readString(item.title) ?? readString(item.description);
      const description = readString(item.description) ?? title;
      if (!title || !description) {
        return null;
      }

      return {
        title: truncate(title, 180),
        description: truncate(description, 1500),
      };
    })
    .filter((item): item is NonNullable<typeof item> => item !== null);

  const source = collected.length > 0 ? collected : fallback;
  return uniqueByText(source, (item) => item.title.toLowerCase(), 20);
}

function normalizeCriticalRequirements(value: unknown, fallback: NormativeAnalysisResult["criticalRequirements"]) {
  const collected = readArray(value)
    .map((item) => {
      if (!isObject(item)) {
        return null;
      }

      const title = readString(item.title) ?? readString(item.description);
      const description = readString(item.description) ?? title;
      if (!title || !description) {
        return null;
      }

      return {
        title: truncate(title, 180),
        description: truncate(description, 1500),
        severity: readEnum(item.severity, RISK_LEVELS, "medio"),
      };
    })
    .filter((item): item is NonNullable<typeof item> => item !== null);

  const source = collected.length > 0 ? collected : fallback;
  return uniqueByText(source, (item) => item.title.toLowerCase(), 10);
}

function normalizeLockIns(value: unknown, fallback: NormativeAnalysisResult["lockIns"]) {
  const collected = readArray(value)
    .map((item) => {
      if (!isObject(item)) {
        return null;
      }

      const title = readString(item.title) ?? readString(item.impact);
      const impact = readString(item.impact) ?? title;
      const recommendation = readString(item.recommendation);

      if (!title || !impact || !recommendation) {
        return null;
      }

      return {
        title: truncate(title, 220),
        impact: truncate(impact, 1800),
        recommendation: truncate(recommendation, 1800),
      };
    })
    .filter((item): item is NonNullable<typeof item> => item !== null);

  const source = collected.length > 0 ? collected : fallback;
  return uniqueByText(source, (item) => item.title.toLowerCase(), 8);
}

function normalizePreparationRecommendations(value: unknown, fallback: NormativeAnalysisResult["preparationRecommendations"]) {
  const collected = readArray(value)
    .map((item) => {
      if (!isObject(item)) {
        return null;
      }

      const title = readString(item.title) ?? readString(item.description);
      const description = readString(item.description) ?? title;

      if (!title || !description) {
        return null;
      }

      return {
        title: truncate(title, 220),
        description: truncate(description, 1600),
        priority: readEnum(item.priority, PRIORITY_LEVELS, "media"),
      };
    })
    .filter((item): item is NonNullable<typeof item> => item !== null);

  const source = collected.length > 0 ? collected : fallback;
  return uniqueByText(source, (item) => item.title.toLowerCase(), 10);
}

function normalizeSourceTraceability(value: unknown, fallback: NormativeAnalysisResult["sourceTraceability"]) {
  const collected = readArray(value)
    .map((item) => {
      if (!isObject(item)) {
        return null;
      }

      const sourceLabel = readString(item.sourceLabel);
      if (!sourceLabel) {
        return null;
      }

      const fields = normalizeStringArray(item.fields, [], 40, 2);

      return {
        documentType: readEnum(item.documentType, DOCUMENT_TYPES, "OTRO"),
        sourceLabel: truncate(sourceLabel, 120),
        fields,
      };
    })
    .filter((item): item is NonNullable<typeof item> => item !== null);

  return collected.length > 0 ? collected.slice(0, 20) : fallback.slice(0, 20);
}

function appearsToBeAnalysisObject(value: Record<string, unknown>) {
  const keys = Object.keys(value);
  if (keys.length === 0) {
    return false;
  }

  const meaningfulKeys = [
    "executiveSummary",
    "participationViability",
    "risk",
    "requiredChecklist",
    "criticalRequirements",
    "lockIns",
    "preparationRecommendations",
    "officialFormats",
    "valueForMoney",
    "circularEconomy",
    "sourceTraceability",
    "confidence",
  ];

  let matched = 0;
  for (const key of meaningfulKeys) {
    if (key in value) {
      matched += 1;
    }
  }

  return matched >= 3;
}

function pickAnalysisCandidate(payload: unknown) {
  if (!isObject(payload)) {
    return null;
  }

  if (isObject(payload.analysis)) {
    return payload.analysis;
  }

  if (isObject(payload.data) && isObject(payload.data.analysis)) {
    return payload.data.analysis;
  }

  if (appearsToBeAnalysisObject(payload)) {
    return payload;
  }

  return null;
}

function zodIssueSummary(error: z.ZodError) {
  return error.issues
    .slice(0, MAX_WARNING_ISSUES)
    .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
    .join(" | ");
}

function normalizeFromCandidate(candidate: Record<string, unknown>, fallback: NormativeAnalysisResult): NormativeAnalysisResult {
  const participationCandidate = isObject(candidate.participationViability) ? candidate.participationViability : null;
  const riskCandidate = isObject(candidate.risk) ? candidate.risk : null;
  const valueForMoneyCandidate = isObject(candidate.valueForMoney) ? candidate.valueForMoney : null;
  const circularEconomyCandidate = isObject(candidate.circularEconomy) ? candidate.circularEconomy : null;

  return {
    executiveSummary: readString(candidate.executiveSummary) ?? fallback.executiveSummary,
    participationViability: {
      score: Math.max(0, Math.min(100, Math.round(readNumber(participationCandidate?.score, fallback.participationViability.score)))),
      level: readEnum(participationCandidate?.level, VIABILITY_LEVELS, fallback.participationViability.level),
      rationale: readString(participationCandidate?.rationale) ?? fallback.participationViability.rationale,
    },
    risk: {
      level: readEnum(riskCandidate?.level, RISK_LEVELS, fallback.risk.level),
      description: readString(riskCandidate?.description) ?? fallback.risk.description,
    },
    requiredChecklist: normalizeChecklist(candidate.requiredChecklist, fallback.requiredChecklist),
    criticalRequirements: normalizeCriticalRequirements(candidate.criticalRequirements, fallback.criticalRequirements),
    lockIns: normalizeLockIns(candidate.lockIns, fallback.lockIns),
    preparationRecommendations: normalizePreparationRecommendations(candidate.preparationRecommendations, fallback.preparationRecommendations),
    officialFormats: normalizeStringArray(candidate.officialFormats, fallback.officialFormats, 25, 3),
    valueForMoney: {
      score: Math.max(0, Math.min(100, Math.round(readNumber(valueForMoneyCandidate?.score, fallback.valueForMoney.score)))),
      signals: normalizeStringArray(valueForMoneyCandidate?.signals, fallback.valueForMoney.signals, 8, 3),
      actions: normalizeStringArray(valueForMoneyCandidate?.actions, fallback.valueForMoney.actions, 8, 3),
    },
    circularEconomy: {
      applicable: readBoolean(circularEconomyCandidate?.applicable, fallback.circularEconomy.applicable),
      signals: normalizeStringArray(circularEconomyCandidate?.signals, fallback.circularEconomy.signals, 8, 3),
      actions: normalizeStringArray(circularEconomyCandidate?.actions, fallback.circularEconomy.actions, 8, 3),
    },
    sourceTraceability: normalizeSourceTraceability(candidate.sourceTraceability, fallback.sourceTraceability),
    confidence: readEnum(candidate.confidence, CONFIDENCE_LEVELS, fallback.confidence),
  };
}

function normalizeResult(result: NormativeAnalysisResult): NormativeAnalysisResult {
  const normalizedChecklist = result.requiredChecklist.slice(0, 20);
  const normalizedCritical = result.criticalRequirements.slice(0, 10);
  const normalizedLockIns = result.lockIns.slice(0, 8);
  const normalizedRecommendations = result.preparationRecommendations.slice(0, 10);
  const normalizedFormats = result.officialFormats.slice(0, 25);
  const normalizedTraceability = result.sourceTraceability.slice(0, 20);
  const normalizedValueForMoneySignals = result.valueForMoney.signals.slice(0, 8);
  const normalizedValueForMoneyActions = result.valueForMoney.actions.slice(0, 8);
  const normalizedCircularSignals = result.circularEconomy.signals.slice(0, 8);
  const normalizedCircularActions = result.circularEconomy.actions.slice(0, 8);

  return {
    ...result,
    requiredChecklist: normalizedChecklist,
    criticalRequirements: normalizedCritical,
    lockIns: normalizedLockIns,
    preparationRecommendations: normalizedRecommendations,
    officialFormats: normalizedFormats,
    sourceTraceability: normalizedTraceability,
    valueForMoney: {
      score: Math.max(0, Math.min(100, Math.round(result.valueForMoney.score))),
      signals:
        normalizedValueForMoneySignals.length > 0
          ? normalizedValueForMoneySignals
          : ["No se detectaron senales explicitas de valor por dinero en el documento."],
      actions:
        normalizedValueForMoneyActions.length > 0
          ? normalizedValueForMoneyActions
          : ["Incorporar analisis de costo total de propiedad y evidencia de eficiencia."],
    },
    circularEconomy: {
      applicable: result.circularEconomy.applicable,
      signals:
        normalizedCircularSignals.length > 0
          ? normalizedCircularSignals
          : ["No se detectaron criterios explicitos de economia circular."],
      actions:
        normalizedCircularActions.length > 0
          ? normalizedCircularActions
          : ["Agregar acciones de reutilizacion, reciclaje o disposicion responsable cuando aplique."],
    },
    participationViability: {
      ...result.participationViability,
      score: Math.max(0, Math.min(100, Math.round(result.participationViability.score))),
    },
  };
}

function usageFromPayload(payload: OpenAiChatCompletionResponse): NormativeAiUsage {
  return {
    promptTokens: payload.usage?.prompt_tokens ?? null,
    completionTokens: payload.usage?.completion_tokens ?? null,
    totalTokens: payload.usage?.total_tokens ?? null,
  };
}

function buildPrompts(input: NormativeAnalysisInput) {
  const inputText = clampInputText(input.text);
  const documentType = input.documentType;
  const issuingEntity = input.issuingEntity ?? "No especificada";
  const extractionMethod = input.methodUsed;
  const pageCount = input.numPages;

  const systemPrompt = [
    "Eres un analista senior de contratacion publica en Mexico especializado en licitaciones y cumplimiento normativo.",
    "Debes entregar un analisis practico y accionable para equipos que preparan propuestas.",
    "No inventes datos fuera del texto proporcionado. Si algo no aparece claramente, se conservador.",
    "Responde siempre en espanol.",
    "Devuelve unicamente JSON valido siguiendo exactamente el esquema solicitado.",
  ].join(" ");

  const userPrompt = [
    "Analiza el siguiente documento normativo de licitacion y devuelve un analisis completo.",
    `Tipo de documento: ${documentType}`,
    `Entidad emisora: ${issuingEntity}`,
    `Metodo de extraccion: ${extractionMethod}`,
    `Numero de paginas: ${pageCount}`,
    "",
    "Criterios de salida:",
    "- executiveSummary: resumen ejecutivo claro y util para decision.",
    "- participationViability.score: probabilidad realista (0-100) de participar con exito.",
    "- requiredChecklist: requisitos obligatorios concretos detectados.",
    "- criticalRequirements: requisitos de alto impacto y severidad.",
    "- lockIns: posibles candados/restricciones y recomendacion de accion.",
    "- preparationRecommendations: plan de preparacion accionable con prioridad.",
    "- officialFormats: formatos/anexos/manifestos oficiales detectados o requeridos.",
    "- valueForMoney: senales y acciones sobre costo total, eficiencia y calidad-precio.",
    "- circularEconomy: senales y acciones de economia circular (si aplica).",
    "- sourceTraceability: trazabilidad por documento origen y campos detectados.",
    "",
    "Texto del documento:",
    inputText,
  ].join("\n");

  return {
    systemPrompt,
    userPrompt,
  };
}

async function callOpenAiNormativeAnalysis(input: NormativeAnalysisInput, mode: "json_schema" | "json_object") {
  const apiKey = getOpenAiApiKey();
  if (!apiKey) {
    throw new Error("No se encontro API key para OpenAI (OPENAI_API_KEY o api_key).");
  }

  const model = process.env.OPENAI_NORMATIVE_MODEL?.trim() || DEFAULT_OPENAI_MODEL;
  const timeoutMs = parsePositiveInteger(process.env.OPENAI_NORMATIVE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
  const baseUrl = (process.env.OPENAI_API_BASE_URL?.trim() || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
  const { systemPrompt, userPrompt } = buildPrompts(input);

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(`${baseUrl}/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model,
        temperature: 0,
        response_format:
          mode === "json_schema"
            ? {
                type: "json_schema",
                json_schema: NormativeAiJsonSchema,
              }
            : {
                type: "json_object",
              },
        messages: [
          { role: "system", content: systemPrompt },
          { role: "user", content: userPrompt },
        ],
      }),
      signal: controller.signal,
    });

    const payload = (await response.json().catch(() => ({}))) as OpenAiChatCompletionResponse;

    if (!response.ok) {
      const apiError = payload.error?.message ? ` ${payload.error.message}` : "";
      throw new Error(`OpenAI devolvio ${response.status}.${apiError}`);
    }

    const rawContent = payload.choices?.[0]?.message?.content;
    if (!rawContent || typeof rawContent !== "string") {
      throw new Error("OpenAI no devolvio contenido util.");
    }

    const parsedContent = extractJsonObject(rawContent);
    const strictParsed = NormativeAiResponseSchema.safeParse(parsedContent);

    if (strictParsed.success) {
      const normalized = normalizeResult(strictParsed.data.analysis);

      return {
        result: normalized,
        model: payload.model ?? model,
        usage: usageFromPayload(payload),
        warnings: [] as string[],
      };
    }

    const candidate = pickAnalysisCandidate(parsedContent);
    if (!candidate) {
      const detail = zodIssueSummary(strictParsed.error);
      throw new Error(`OpenAI devolvio JSON sin estructura de analisis util. ${detail}`);
    }

    const heuristicBaseline = analyzeNormativeText(input);
    const tolerantResult = normalizeResult(normalizeFromCandidate(candidate, heuristicBaseline));
    const issueSummary = zodIssueSummary(strictParsed.error);

    return {
      result: tolerantResult,
      model: payload.model ?? model,
      usage: usageFromPayload(payload),
      warnings: [`Se normalizo respuesta AI parcial fuera de schema estricto. ${issueSummary}`],
    };
  } finally {
    clearTimeout(timer);
  }
}

export async function analyzeNormativeTextWithAi(input: NormativeAnalysisInput): Promise<NormativeAiAnalysisOutput> {
  const warnings: string[] = [];

  try {
    const ai = await callOpenAiNormativeAnalysis(input, "json_schema");
    warnings.push(...ai.warnings);

    return {
      result: ai.result,
      engine: "openai_json_schema",
      model: ai.model,
      warnings,
      usage: ai.usage,
    };
  } catch (schemaError) {
    const schemaMessage = schemaError instanceof Error ? schemaError.message : String(schemaError);
    warnings.push(`OpenAI JSON schema no disponible: ${schemaMessage}`);
  }

  try {
    const ai = await callOpenAiNormativeAnalysis(input, "json_object");
    warnings.push(...ai.warnings);
    warnings.push("Se uso OpenAI JSON object como fallback del modo JSON schema.");

    return {
      result: ai.result,
      engine: "openai_json_object",
      model: ai.model,
      warnings,
      usage: ai.usage,
    };
  } catch (jsonObjectError) {
    const jsonObjectMessage = jsonObjectError instanceof Error ? jsonObjectError.message : String(jsonObjectError);
    warnings.push(`OpenAI JSON object fallo: ${jsonObjectMessage}`);
  }

  const fallback = analyzeNormativeText(input);
  warnings.push("Se activo el motor heuristico como ultimo recurso.");

  return {
    result: fallback,
    engine: "heuristic_fallback",
    model: null,
    warnings,
    usage: null,
  };
}
