import { z } from "zod";
import { extractActaLookupDictionary, mapLookupDictionaryToActaFields } from "@/lib/extraction/extractFields";
import {
  ActaAdministrationTypeSchema,
  ActaFieldsSchema,
  ActaForeignersClauseSchema,
  ActaLookupCompanyTypeSchema,
  ActaLookupDictionarySchema,
  type ActaFields,
  type ActaLookupDictionary,
} from "@/lib/extraction/schema";

const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
const DEFAULT_TIMEOUT_MS = 60_000;
const DEFAULT_MAX_TEXT_CHARS = 45_000;

const ActaAiResponseSchema = z.object({
  fields: z.record(z.string(), z.unknown()).nullable().optional(),
  lookupDictionary: z.record(z.string(), z.unknown()).nullable().optional(),
});

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

export type ActaAiExtractionEngine = "ai" | "regex_fallback";

export type ActaAiExtractionResult = {
  fields: ActaFields;
  lookupDictionary: ActaLookupDictionary;
  engine: ActaAiExtractionEngine;
  warnings: string[];
  model: string | null;
  usage:
    | {
        promptTokens: number | null;
        completionTokens: number | null;
        totalTokens: number | null;
      }
    | null;
};

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

function clampActaText(fullText: string) {
  const maxChars = parsePositiveInteger(process.env.OPENAI_ACTA_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 strategies.
  }

  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 AI no contiene un JSON valido.");
}

function getErrorMessage(error: unknown) {
  if (error instanceof Error) {
    return error.message;
  }

  return String(error);
}

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

function compactString(value: string, maxLength = 600) {
  const compacted = value.replace(/\s+/g, " ").trim();
  if (!compacted) {
    return null;
  }
  return compacted.length > maxLength ? compacted.slice(0, maxLength) : compacted;
}

function toStringOrNull(value: unknown, maxLength = 600): string | null {
  if (value === null || value === undefined) {
    return null;
  }

  if (typeof value === "string") {
    return compactString(value, maxLength);
  }

  if (typeof value === "number" || typeof value === "boolean") {
    return compactString(String(value), maxLength);
  }

  if (Array.isArray(value)) {
    const joined = value
      .map((item) => toStringOrNull(item, maxLength))
      .filter((item): item is string => Boolean(item))
      .join(", ");
    return compactString(joined, maxLength);
  }

  if (isRecord(value)) {
    const joined = Object.values(value)
      .map((item) => toStringOrNull(item, maxLength))
      .filter((item): item is string => Boolean(item))
      .join(", ");
    return compactString(joined, maxLength);
  }

  return null;
}

type ParseResultSuccess<T> = { success: true; data: T };
type ParseResultFailure = { success: false };
type EnumLikeSchema<T extends string> = {
  safeParse: (value: unknown) => ParseResultSuccess<T> | ParseResultFailure;
};

function parseOptionalEnum<T extends string>(schema: EnumLikeSchema<T>, raw: unknown): T | null {
  const normalized = toStringOrNull(raw, 80);
  if (!normalized) {
    return null;
  }

  const parsed = schema.safeParse(normalized);
  return parsed.success ? parsed.data : null;
}

function normalizeAiFields(rawFields: unknown, fallback: ActaFields): ActaFields {
  if (!isRecord(rawFields)) {
    return fallback;
  }

  return ActaFieldsSchema.parse({
    name: toStringOrNull(rawFields.name, 220),
    rfc: null,
    legalRepresentative: toStringOrNull(rawFields.legalRepresentative, 220),
    incorporationDate: toStringOrNull(rawFields.incorporationDate, 120),
    deedNumber: toStringOrNull(rawFields.deedNumber, 80),
    notaryName: toStringOrNull(rawFields.notaryName, 220),
    fiscalAddress: toStringOrNull(rawFields.fiscalAddress, 800),
    businessPurpose: toStringOrNull(rawFields.businessPurpose, 1200),
    stateOfIncorporation: toStringOrNull(rawFields.stateOfIncorporation, 120),
  });
}

function normalizeAiLookupDictionary(rawLookup: unknown, fallback: ActaLookupDictionary): ActaLookupDictionary {
  if (!isRecord(rawLookup)) {
    return fallback;
  }

  const candidate: ActaLookupDictionary = {
    ...fallback,
    version: "mx_acta_constitutiva_reference_v1",
    instrumentType: toStringOrNull(rawLookup.instrumentType, 220),
    instrumentNumber: toStringOrNull(rawLookup.instrumentNumber, 120),
    protocolVolumeBook: toStringOrNull(rawLookup.protocolVolumeBook, 120),
    instrumentDate: toStringOrNull(rawLookup.instrumentDate, 160),
    placeOfGranting: toStringOrNull(rawLookup.placeOfGranting, 220),
    notaryName: toStringOrNull(rawLookup.notaryName, 220),
    notaryNumber: toStringOrNull(rawLookup.notaryNumber, 40),
    notaryState: toStringOrNull(rawLookup.notaryState, 120),
    legalName: toStringOrNull(rawLookup.legalName, 220),
    normalizedName: toStringOrNull(rawLookup.normalizedName, 220),
    companyType: parseOptionalEnum(ActaLookupCompanyTypeSchema, rawLookup.companyType),
    domicile: toStringOrNull(rawLookup.domicile, 800),
    duration: toStringOrNull(rawLookup.duration, 160),
    corporatePurpose: toStringOrNull(rawLookup.corporatePurpose, 1200),
    industry: toStringOrNull(rawLookup.industry, 220),
    countryOfOperation: toStringOrNull(rawLookup.countryOfOperation, 120),
    stateOfIncorporation: toStringOrNull(rawLookup.stateOfIncorporation, 120),
    legalRepresentative: toStringOrNull(rawLookup.legalRepresentative, 220),
    rfcCompany: toStringOrNull(rawLookup.rfcCompany, 40),
    capitalTotal: toStringOrNull(rawLookup.capitalTotal, 120),
    capitalFixed: toStringOrNull(rawLookup.capitalFixed, 120),
    capitalVariable: toStringOrNull(rawLookup.capitalVariable, 120),
    shareNominalValue: toStringOrNull(rawLookup.shareNominalValue, 120),
    administrationType: parseOptionalEnum(ActaAdministrationTypeSchema, rawLookup.administrationType),
    foreignersClause: parseOptionalEnum(ActaForeignersClauseSchema, rawLookup.foreignersClause) ?? "NO_DETECTADO",
    sreNameAuthorization: toStringOrNull(rawLookup.sreNameAuthorization, 220),
    rpcReference: (() => {
      if (!isRecord(rawLookup.rpcReference)) {
        return null;
      }

      return {
        folioMercantil: toStringOrNull(rawLookup.rpcReference.folioMercantil, 80),
        registroPublico: toStringOrNull(rawLookup.rpcReference.registroPublico, 120),
        inscriptionDate: toStringOrNull(rawLookup.rpcReference.inscriptionDate, 120),
      };
    })(),
    derivedFields: (() => {
      const source = isRecord(rawLookup.derivedFields) ? rawLookup.derivedFields : {};
      const rawIsConstituted = source.isCompanyValidlyConstituted;
      const isCompanyValidlyConstituted =
        typeof rawIsConstituted === "boolean" ? rawIsConstituted : fallback.derivedFields.isCompanyValidlyConstituted;
      const keySignersCandidates = Array.isArray(source.keySignersCandidates)
        ? source.keySignersCandidates
            .map((value) => toStringOrNull(value, 120))
            .filter((value): value is string => Boolean(value))
        : fallback.derivedFields.keySignersCandidates;

      return {
        isCompanyValidlyConstituted,
        keySignersCandidates: keySignersCandidates && keySignersCandidates.length > 0 ? keySignersCandidates : null,
      };
    })(),
  };

  const parsed = ActaLookupDictionarySchema.safeParse(candidate);
  return parsed.success ? parsed.data : fallback;
}

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

async function callOpenAiActaExtraction(fullText: string) {
  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_ACTA_MODEL?.trim() || DEFAULT_OPENAI_MODEL;
  const timeoutMs = parsePositiveInteger(process.env.OPENAI_ACTA_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
  const baseUrl = (process.env.OPENAI_API_BASE_URL?.trim() || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
  const inputText = clampActaText(fullText);

  const systemPrompt = [
    "Eres un analista legal experto en actas constitutivas mexicanas.",
    "Debes extraer datos solo del texto proporcionado.",
    "Responde exclusivamente JSON valido, sin markdown ni explicaciones.",
    "Si un dato no aparece de forma clara, usa null.",
    "No inventes RFC de empresa; rfc en fields debe ser null.",
    "lookupDictionary.version debe ser exactamente 'mx_acta_constitutiva_reference_v1'.",
  ].join(" ");

  const userPrompt = [
    "Extrae y devuelve este objeto JSON con dos claves: fields y lookupDictionary.",
    "fields debe seguir los campos de onboarding legal (name, rfc, legalRepresentative, incorporationDate, deedNumber, notaryName, fiscalAddress, businessPurpose, stateOfIncorporation).",
    "lookupDictionary debe seguir la estructura completa del diccionario de acta constitutiva.",
    "Texto de acta:",
    inputText,
  ].join("\n\n");

  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: {
          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 parsed = ActaAiResponseSchema.parse(parsedContent);

    if (!parsed.fields && !parsed.lookupDictionary) {
      throw new Error("OpenAI no devolvio las claves esperadas: fields/lookupDictionary.");
    }

    const regexLookup = extractActaLookupDictionary(fullText);
    const regexFields = mapLookupDictionaryToActaFields(regexLookup);
    const normalizedFields = normalizeAiFields(parsed.fields, regexFields);
    const normalizedLookup = normalizeAiLookupDictionary(parsed.lookupDictionary, regexLookup);

    return {
      fields: normalizedFields,
      lookupDictionary: normalizedLookup,
      model: payload.model ?? model,
      usage: {
        promptTokens: payload.usage?.prompt_tokens ?? null,
        completionTokens: payload.usage?.completion_tokens ?? null,
        totalTokens: payload.usage?.total_tokens ?? null,
      },
    };
  } finally {
    clearTimeout(timer);
  }
}

function buildRegexFallback(fullText: string, warning: string): ActaAiExtractionResult {
  const lookupDictionary = extractActaLookupDictionary(fullText);
  const fields = mapLookupDictionaryToActaFields(lookupDictionary);

  return {
    fields,
    lookupDictionary,
    engine: "regex_fallback",
    warnings: [warning],
    model: process.env.OPENAI_ACTA_MODEL?.trim() || DEFAULT_OPENAI_MODEL,
    usage: null,
  };
}

export async function extractActaDataWithAiBaseline(fullText: string): Promise<ActaAiExtractionResult> {
  try {
    const ai = await callOpenAiActaExtraction(fullText);

    return {
      fields: ai.fields,
      lookupDictionary: ai.lookupDictionary,
      engine: "ai",
      warnings: [],
      model: ai.model,
      usage: ai.usage,
    };
  } catch (error) {
    const reason = getErrorMessage(error);
    return buildRegexFallback(fullText, `AI extraction fallo y se uso respaldo regex. Detalle: ${reason}`);
  }
}
