import { OrganizationDocumentType, Prisma } from "@prisma/client";
import { NextResponse } from "next/server";
import { getSessionPayload } from "@/lib/auth/session";
import { extractActaDataWithAiBaseline } from "@/lib/extraction/aiExtractFields";
import { type ActaFields, type ActaLookupDictionary } from "@/lib/extraction/schema";
import { analyzePdf } from "@/lib/pdf/analyzePdf";
import {
  OcrFailedError,
  OcrUnavailableError,
  PdfEncryptedError,
  PdfNoTextDetectedError,
  PdfUnreadableError,
} from "@/lib/pdf/errors";
import { prisma } from "@/lib/prisma";
import { MAX_ACTA_PDF_BYTES, removeStoredActaPdf, storeActaPdf } from "@/lib/onboarding/acta-storage";

export const runtime = "nodejs";

function isLegacySchemaError(error: unknown) {
  return error instanceof Prisma.PrismaClientKnownRequestError && (error.code === "P2021" || error.code === "P2022");
}

function toOptionalString(value: string | null | undefined) {
  return value ? value.trim() || undefined : undefined;
}

function toNullableString(value: string | null | undefined) {
  return value ? value.trim() || null : null;
}

function isPdfFile(file: File) {
  const extension = file.name.toLowerCase().endsWith(".pdf");
  const mimeType = file.type === "application/pdf";
  return extension || mimeType;
}

function hasPdfSignature(buffer: Buffer) {
  return buffer.subarray(0, 5).toString("utf8") === "%PDF-";
}

function getExtractionConfidence(fields: ActaFields) {
  const detected = Object.values(fields).filter(Boolean).length;

  if (detected >= 6) {
    return "high" as const;
  }

  if (detected >= 3) {
    return "medium" as const;
  }

  return "low" as const;
}

function getDetectedFields(fields: ActaFields) {
  return Object.entries(fields)
    .filter(([, value]) => Boolean(value))
    .map(([field]) => field);
}

function getDetectedLookupDictionaryFields(dictionary: ActaLookupDictionary) {
  const entries: string[] = [];

  for (const [key, value] of Object.entries(dictionary)) {
    if (key === "version") {
      continue;
    }

    if (value === null || value === undefined) {
      continue;
    }

    if (typeof value === "object") {
      const nestedKeys = Object.entries(value)
        .filter(([, nestedValue]) => {
          if (nestedValue === null || nestedValue === undefined) {
            return false;
          }

          if (Array.isArray(nestedValue)) {
            return nestedValue.length > 0;
          }

          return typeof nestedValue === "boolean" || Boolean(String(nestedValue).trim());
        })
        .map(([nestedKey]) => `${key}.${nestedKey}`);

      entries.push(...nestedKeys);
      continue;
    }

    entries.push(key);
  }

  return entries;
}

function mapAnalysisError(error: unknown) {
  if (error instanceof PdfEncryptedError) {
    return {
      status: 422,
      error: "El PDF esta protegido/encriptado. Sube una version sin bloqueo para extraer texto.",
      code: error.code,
    };
  }

  if (error instanceof PdfUnreadableError) {
    return {
      status: 422,
      error: "No fue posible leer el PDF. Verifica que el archivo no este dañado.",
      code: error.code,
    };
  }

  if (error instanceof OcrUnavailableError) {
    return {
      status: 503,
      error: "No se detecto texto suficiente y OCRmyPDF no esta disponible. Revisa instalacion local en README.",
      code: error.code,
    };
  }

  if (error instanceof PdfNoTextDetectedError) {
    return {
      status: 422,
      error: "No se detecto texto en el PDF y OCR tampoco pudo recuperarlo.",
      code: error.code,
    };
  }

  if (error instanceof OcrFailedError) {
    return {
      status: 422,
      error: "No se detecto texto suficiente y el OCR fallo durante el procesamiento.",
      code: error.code,
    };
  }

  return {
    status: 422,
    error: "No fue posible extraer texto del PDF.",
    code: "PDF_ANALYSIS_FAILED",
  };
}

export async function POST(request: Request) {
  const session = await getSessionPayload();

  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const formData = await request.formData();
  const actaFile = formData.get("file") ?? formData.get("acta");

  if (!(actaFile instanceof File)) {
    return NextResponse.json({ error: "Debes adjuntar un archivo PDF de Acta constitutiva." }, { status: 400 });
  }

  if (actaFile.size <= 0) {
    return NextResponse.json({ error: "El archivo esta vacio." }, { status: 400 });
  }

  if (actaFile.size > MAX_ACTA_PDF_BYTES) {
    return NextResponse.json({ error: "El archivo excede el limite de 15MB." }, { status: 400 });
  }

  if (!isPdfFile(actaFile)) {
    return NextResponse.json({ error: "Solo se permiten archivos PDF." }, { status: 400 });
  }

  const fileBuffer = Buffer.from(await actaFile.arrayBuffer());

  if (!hasPdfSignature(fileBuffer)) {
    return NextResponse.json({ error: "El archivo no parece ser un PDF valido." }, { status: 400 });
  }

  let fields: ActaFields;
  let lookupDictionary: ActaLookupDictionary;
  let rawText: string;
  let methodUsed: "direct" | "ocr";
  let numPages: number;
  let warnings: string[];
  let extractionEngine: "ai" | "regex_fallback";
  let aiModel: string | null;
  let aiUsage:
    | {
        promptTokens: number | null;
        completionTokens: number | null;
        totalTokens: number | null;
      }
    | null;

  try {
    const analyzed = await analyzePdf(fileBuffer);
    const extracted = await extractActaDataWithAiBaseline(analyzed.text);
    lookupDictionary = extracted.lookupDictionary;
    fields = extracted.fields;
    rawText = analyzed.text;
    methodUsed = analyzed.methodUsed;
    numPages = analyzed.numPages;
    warnings = [...analyzed.warnings, ...extracted.warnings];
    extractionEngine = extracted.engine;
    aiModel = extracted.model;
    aiUsage = extracted.usage;
  } catch (error) {
    const mapped = mapAnalysisError(error);
    const errorName = error instanceof Error ? error.name : "UnknownError";
    const errorMessage = error instanceof Error ? error.message : String(error);
    const errorCause = error instanceof Error && "cause" in error ? (error.cause as Error | undefined) : undefined;
    const ocrStderr = error instanceof OcrFailedError ? error.stderr : undefined;
    console.error("Acta analysis failed", {
      mappedCode: mapped.code,
      mappedStatus: mapped.status,
      errorName,
      errorMessage,
      causeName: errorCause?.name,
      causeMessage: errorCause?.message,
      ocrStderr: ocrStderr ? ocrStderr.slice(0, 1200) : undefined,
    });
    return NextResponse.json(
      { error: mapped.error, code: mapped.code },
      {
        status: mapped.status,
        headers: {
          "x-acta-error-code": mapped.code,
        },
      },
    );
  }

  const extractedFields = getDetectedFields(fields);
  const detectedLookupFields = getDetectedLookupDictionaryFields(lookupDictionary);
  const confidence = getExtractionConfidence(fields);
  const extractionPayload = {
    ...fields,
    industry: lookupDictionary.industry,
    country: lookupDictionary.countryOfOperation,
    lookupDictionary,
    extractedFields,
    detectedLookupFields,
    confidence,
    methodUsed,
    numPages,
    warnings,
    extractionEngine,
    aiModel,
    aiUsage,
  };

  const storedFile = await storeActaPdf(session.userId, actaFile.name, fileBuffer);

  try {
    const existingOrg = await prisma.organization.findUnique({
      where: { userId: session.userId },
      select: {
        id: true,
        name: true,
      },
    });

    const fallbackName = session.email.split("@")[0]?.trim() || "empresa";
    const organizationName = toOptionalString(fields.name) ?? existingOrg?.name ?? fallbackName;

    let organization: { id: string };
    try {
      organization = await prisma.organization.upsert({
        where: { userId: session.userId },
        update: {
          name: organizationName,
          tradeName: toNullableString(fields.name),
          legalRepresentative: toNullableString(fields.legalRepresentative),
          incorporationDate: toNullableString(fields.incorporationDate),
          deedNumber: toNullableString(fields.deedNumber),
          notaryName: toNullableString(fields.notaryName),
          stateOfIncorporation: toNullableString(fields.stateOfIncorporation),
          companyType: toNullableString(lookupDictionary.companyType),
          fiscalAddress: toNullableString(fields.fiscalAddress),
          businessPurpose: toNullableString(fields.businessPurpose),
          industry: toNullableString(lookupDictionary.industry),
          country: toNullableString(lookupDictionary.countryOfOperation),
          actaExtractedData: extractionPayload as Prisma.InputJsonValue,
          actaLookupDictionary: lookupDictionary as Prisma.InputJsonValue,
          actaUploadedAt: new Date(),
        },
        create: {
          userId: session.userId,
          name: organizationName,
          tradeName: toNullableString(fields.name),
          legalRepresentative: toNullableString(fields.legalRepresentative),
          incorporationDate: toNullableString(fields.incorporationDate),
          deedNumber: toNullableString(fields.deedNumber),
          notaryName: toNullableString(fields.notaryName),
          stateOfIncorporation: toNullableString(fields.stateOfIncorporation),
          companyType: toNullableString(lookupDictionary.companyType),
          fiscalAddress: toNullableString(fields.fiscalAddress),
          businessPurpose: toNullableString(fields.businessPurpose),
          industry: toNullableString(lookupDictionary.industry),
          country: toNullableString(lookupDictionary.countryOfOperation),
          actaExtractedData: extractionPayload as Prisma.InputJsonValue,
          actaLookupDictionary: lookupDictionary as Prisma.InputJsonValue,
          actaUploadedAt: new Date(),
        },
        select: { id: true },
      });
    } catch (error) {
      if (!isLegacySchemaError(error)) {
        throw error;
      }

      organization = await prisma.organization.upsert({
        where: { userId: session.userId },
        update: {
          name: organizationName,
        },
        create: {
          userId: session.userId,
          name: organizationName,
        },
        select: { id: true },
      });
    }

    let existingDocument: { filePath: string } | null = null;
    try {
      existingDocument = await prisma.organizationDocument.findUnique({
        where: {
          userId_type: {
            userId: session.userId,
            type: OrganizationDocumentType.ACTA_CONSTITUTIVA,
          },
        },
        select: {
          filePath: true,
        },
      });

      await prisma.organizationDocument.upsert({
        where: {
          userId_type: {
            userId: session.userId,
            type: OrganizationDocumentType.ACTA_CONSTITUTIVA,
          },
        },
        update: {
          organizationId: organization.id,
          fileName: storedFile.fileName,
          storedFileName: storedFile.storedFileName,
          filePath: storedFile.filePath,
          mimeType: storedFile.mimeType,
          sizeBytes: storedFile.sizeBytes,
          checksumSha256: storedFile.checksumSha256,
          extractedData: extractionPayload as Prisma.InputJsonValue,
          extractedTextSnippet: rawText.slice(0, 1600),
        },
        create: {
          organizationId: organization.id,
          userId: session.userId,
          type: OrganizationDocumentType.ACTA_CONSTITUTIVA,
          fileName: storedFile.fileName,
          storedFileName: storedFile.storedFileName,
          filePath: storedFile.filePath,
          mimeType: storedFile.mimeType,
          sizeBytes: storedFile.sizeBytes,
          checksumSha256: storedFile.checksumSha256,
          extractedData: extractionPayload as Prisma.InputJsonValue,
          extractedTextSnippet: rawText.slice(0, 1600),
        },
      });
    } catch (error) {
      if (!isLegacySchemaError(error)) {
        throw error;
      }
    }

    if (existingDocument?.filePath && existingDocument.filePath !== storedFile.filePath) {
      await removeStoredActaPdf(existingDocument.filePath);
    }

    return NextResponse.json({
      ok: true,
      fields,
      lookupDictionary,
      rawText,
      methodUsed,
      numPages,
      warnings,
      extractionEngine,
      aiModel,
      aiUsage,
      extractedData: {
        ...fields,
        industry: lookupDictionary.industry,
        country: lookupDictionary.countryOfOperation,
        lookupDictionary,
        extractedFields,
        detectedLookupFields,
        confidence,
        extractionEngine,
      },
      actaUploadedAt: new Date().toISOString(),
    });
  } catch (error) {
    await removeStoredActaPdf(storedFile.filePath);
    console.error("Failed to process acta upload", error);
    return NextResponse.json({ error: "No fue posible procesar el acta constitutiva." }, { status: 500 });
  }
}
