import { readFile } from "node:fs/promises";
import path from "node:path";
import { type NormativeConfidence, type NormativeDocumentType as PrismaNormativeDocumentType, type NormativeRiskLevel, Prisma } from "@prisma/client";
import { NextResponse } from "next/server";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { isLikelyPdfUrl, parseLicitationDocumentLinks, pickPrimaryLicitationDocument, pickPrimaryLicitationPdfDocument } from "@/lib/licitations/documents";
import { type NormativeDocumentType } from "@/lib/normative-analysis/analyze";
import { analyzeNormativeTextWithAi } from "@/lib/normative-analysis/ai-analyze";
import { toNormativeHistoryView } from "@/lib/normative-analysis/history";
import { analyzePdf } from "@/lib/pdf/analyzePdf";
import { OcrFailedError, OcrUnavailableError, PdfEncryptedError, PdfNoTextDetectedError, PdfUnreadableError } from "@/lib/pdf/errors";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";

const MAX_NORMATIVE_PDF_BYTES = 20 * 1024 * 1024;
const SOURCE_FETCH_TIMEOUT_MS = 45_000;
const STORAGE_ROOT = path.resolve(process.cwd(), "storage");

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 guessFileNameFromUrl(url: string) {
  try {
    const parsed = new URL(url);
    const segments = parsed.pathname.split("/").filter(Boolean);
    const lastSegment = segments.at(-1) ?? "";
    const decoded = decodeURIComponent(lastSegment).trim();
    if (!decoded) {
      return "documento-principal.pdf";
    }

    return decoded.toLowerCase().endsWith(".pdf") ? decoded : `${decoded}.pdf`;
  } catch {
    return "documento-principal.pdf";
  }
}

async function fetchSourcePdf(url: string) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), SOURCE_FETCH_TIMEOUT_MS);

  try {
    const response = await fetch(url, {
      method: "GET",
      redirect: "follow",
      signal: controller.signal,
    });

    if (!response.ok) {
      throw new Error(`SOURCE_FETCH_FAILED_${response.status}`);
    }

    const headerLength = response.headers.get("content-length");
    const sizeHint = headerLength ? Number.parseInt(headerLength, 10) : 0;
    if (Number.isFinite(sizeHint) && sizeHint > MAX_NORMATIVE_PDF_BYTES) {
      throw new Error("SOURCE_PDF_TOO_LARGE");
    }

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

    if (!buffer.byteLength) {
      throw new Error("SOURCE_PDF_EMPTY");
    }

    if (buffer.byteLength > MAX_NORMATIVE_PDF_BYTES) {
      throw new Error("SOURCE_PDF_TOO_LARGE");
    }

    return buffer;
  } finally {
    clearTimeout(timer);
  }
}

type LicitationSourcePdf = {
  fileBuffer: Buffer;
  fileName: string;
  linkedLicitationId: string;
  issuingEntity: string | null;
};

type SourcePdfCandidate = {
  url: string;
  fileName: string;
};

function buildSourcePdfCandidates(documentsRaw: unknown, rawSourceUrl: string | null) {
  const sourceDocuments = parseLicitationDocumentLinks(documentsRaw);
  const primaryPdf = pickPrimaryLicitationPdfDocument(sourceDocuments);
  const primaryAny = pickPrimaryLicitationDocument(sourceDocuments);
  const candidates: SourcePdfCandidate[] = [];
  const seen = new Set<string>();

  function pushCandidate(url: string | null | undefined, fileName: string | null | undefined) {
    const normalizedUrl = typeof url === "string" ? url.trim() : "";
    if (!normalizedUrl || seen.has(normalizedUrl)) {
      return;
    }
    seen.add(normalizedUrl);
    candidates.push({
      url: normalizedUrl,
      fileName: (fileName?.trim() || guessFileNameFromUrl(normalizedUrl)),
    });
  }

  pushCandidate(primaryPdf?.url, primaryPdf?.name);
  pushCandidate(primaryAny?.url, primaryAny?.name);

  for (const document of sourceDocuments) {
    pushCandidate(document.url, document.name);
  }

  if (rawSourceUrl) {
    const normalizedSourceUrl = rawSourceUrl.trim();
    const existingIndex = candidates.findIndex((item) => item.url === normalizedSourceUrl);

    if (existingIndex >= 0) {
      if (isLikelyPdfUrl(normalizedSourceUrl) && existingIndex > 0) {
        const [entry] = candidates.splice(existingIndex, 1);
        candidates.unshift(entry);
      }
    } else if (isLikelyPdfUrl(normalizedSourceUrl)) {
      candidates.unshift({
        url: normalizedSourceUrl,
        fileName: guessFileNameFromUrl(normalizedSourceUrl),
      });
    } else {
      pushCandidate(normalizedSourceUrl, guessFileNameFromUrl(normalizedSourceUrl));
    }
  }

  return candidates;
}

async function resolveLicitationSourcePdf(sourceLicitationId: string, currentIssuingEntity: string | null): Promise<LicitationSourcePdf> {
  const sourceLicitation = await prisma.licitation.findUnique({
    where: { id: sourceLicitationId },
    select: {
      id: true,
      supplierAwarded: true,
      documents: true,
      rawSourceUrl: true,
    },
  });

  if (!sourceLicitation) {
    throw new Error("SOURCE_LICITATION_NOT_FOUND");
  }

  const candidates = buildSourcePdfCandidates(sourceLicitation.documents, sourceLicitation.rawSourceUrl);

  if (candidates.length === 0) {
    throw new Error("SOURCE_LICITATION_NO_PDF");
  }

  let sawSourceTooLarge = false;
  let sawSourceTimeout = false;

  for (const candidate of candidates) {
    try {
      const fileBuffer = await fetchSourcePdf(candidate.url);
      if (!hasPdfSignature(fileBuffer)) {
        continue;
      }

      return {
        fileBuffer,
        fileName: candidate.fileName,
        linkedLicitationId: sourceLicitation.id,
        issuingEntity: currentIssuingEntity || sourceLicitation.supplierAwarded?.trim() || null,
      };
    } catch (error) {
      if (error instanceof Error) {
        if (error.message === "SOURCE_PDF_TOO_LARGE") {
          sawSourceTooLarge = true;
        }

        if (error.name === "AbortError") {
          sawSourceTimeout = true;
        }
      }
    }
  }

  if (sawSourceTooLarge) {
    throw new Error("SOURCE_LICITATION_PDF_TOO_LARGE");
  }

  if (sawSourceTimeout) {
    throw new Error("SOURCE_LICITATION_PDF_TIMEOUT");
  }

  throw new Error("SOURCE_LICITATION_NO_VALID_PDF");
}

type ProposalDocumentSource = {
  fileName: string;
  filePath: string;
  mimeType: string;
};

function isPdfProposalDocument(document: ProposalDocumentSource) {
  return document.mimeType === "application/pdf" || document.fileName.toLowerCase().endsWith(".pdf");
}

function pickPrimaryProposalPdfDocument(documents: ProposalDocumentSource[]) {
  return documents.find((document) => isPdfProposalDocument(document)) ?? null;
}

async function readStoredProposalPdf(filePath: string) {
  const absolutePath = path.resolve(process.cwd(), filePath);

  if (!absolutePath.startsWith(`${STORAGE_ROOT}${path.sep}`)) {
    throw new Error("PROPOSAL_PDF_INVALID_PATH");
  }

  try {
    const buffer = await readFile(absolutePath);

    if (!buffer.byteLength) {
      throw new Error("PROPOSAL_PDF_EMPTY");
    }

    if (buffer.byteLength > MAX_NORMATIVE_PDF_BYTES) {
      throw new Error("PROPOSAL_PDF_TOO_LARGE");
    }

    return buffer;
  } catch (error) {
    if (
      error &&
      typeof error === "object" &&
      "code" in error &&
      (error as NodeJS.ErrnoException).code === "ENOENT"
    ) {
      throw new Error("PROPOSAL_PDF_NOT_FOUND");
    }

    throw error;
  }
}

function parseDocumentType(value: unknown): NormativeDocumentType {
  if (typeof value !== "string") {
    return "OTRO";
  }

  const normalized = value.trim().toUpperCase();
  if (normalized === "BASES_LICITACION" || normalized === "CONVOCATORIA" || normalized === "ANEXOS" || normalized === "REGLAMENTO" || normalized === "LEY") {
    return normalized;
  }

  return "OTRO";
}

function toPrismaDocumentType(value: NormativeDocumentType): PrismaNormativeDocumentType {
  if (value === "BASES_LICITACION" || value === "CONVOCATORIA" || value === "REGLAMENTO" || value === "LEY") {
    return value;
  }

  return "OTRO";
}

function toPrismaRiskLevel(value: "alto" | "medio" | "bajo"): NormativeRiskLevel {
  if (value === "alto") {
    return "ALTO";
  }

  if (value === "medio") {
    return "MEDIO";
  }

  return "BAJO";
}

function toPrismaConfidence(value: "low" | "medium" | "high"): NormativeConfidence {
  if (value === "high") {
    return "HIGH";
  }

  if (value === "medium") {
    return "MEDIUM";
  }

  return "LOW";
}

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 danado.",
      code: error.code,
    };
  }

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

  if (error instanceof PdfNoTextDetectedError) {
    return {
      status: 422,
      error: "No se detecto texto util en el PDF para analizar.",
      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 analizar el PDF.",
    code: "NORMATIVE_ANALYSIS_FAILED",
  };
}

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

export async function POST(request: Request) {
  const user = await requireAdminApiUser();

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

  const formData = await request.formData();
  const documentType = parseDocumentType(formData.get("documentType"));
  const issuingEntityRaw = formData.get("issuingEntity");
  let issuingEntity = typeof issuingEntityRaw === "string" ? issuingEntityRaw.trim() || null : null;
  const sourceLicitationIdRaw = formData.get("sourceLicitationId");
  const sourceLicitationId = typeof sourceLicitationIdRaw === "string" ? sourceLicitationIdRaw.trim() || null : null;
  const sourceProposalIdRaw = formData.get("sourceProposalId");
  const sourceProposalId = typeof sourceProposalIdRaw === "string" ? sourceProposalIdRaw.trim() || null : null;
  const fileInput = formData.get("file");
  const uploadedFile = fileInput instanceof File && fileInput.size > 0 ? fileInput : null;
  const useSourceDocument = String(formData.get("useSourceDocument") ?? "") === "1";

  try {
    let fileName = "";
    let fileBuffer: Buffer | null = null;
    let linkedLicitationId: string | null = null;

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

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

      fileBuffer = Buffer.from(await uploadedFile.arrayBuffer());
      fileName = uploadedFile.name;

      if (!hasPdfSignature(fileBuffer)) {
        return NextResponse.json({ error: "El archivo no parece ser un PDF valido." }, { status: 400 });
      }
    } else if (sourceProposalId && useSourceDocument) {
      const sourceProposal = await prisma.proposal.findFirst({
        where: {
          id: sourceProposalId,
          userId: user.id,
        },
        select: {
          id: true,
          issuingEntity: true,
          sourceLicitationId: true,
          documents: {
            orderBy: [{ createdAt: "desc" }],
            select: {
              fileName: true,
              filePath: true,
              mimeType: true,
            },
          },
        },
      });

      if (!sourceProposal) {
        return NextResponse.json({ error: "No se encontro la propuesta vinculada de Modulo 5." }, { status: 404 });
      }

      if (!issuingEntity) {
        issuingEntity = sourceProposal.issuingEntity?.trim() || null;
      }

      linkedLicitationId = sourceProposal.sourceLicitationId ?? null;
      const proposalPdf = pickPrimaryProposalPdfDocument(sourceProposal.documents);

      if (proposalPdf) {
        fileBuffer = await readStoredProposalPdf(proposalPdf.filePath);
        fileName = proposalPdf.fileName;

        if (!hasPdfSignature(fileBuffer)) {
          return NextResponse.json({ error: "El PDF principal de Modulo 5 no tiene una firma valida." }, { status: 422 });
        }
      } else if (sourceProposal.sourceLicitationId) {
        const licitationSource = await resolveLicitationSourcePdf(sourceProposal.sourceLicitationId, issuingEntity);
        fileBuffer = licitationSource.fileBuffer;
        fileName = licitationSource.fileName;
        linkedLicitationId = licitationSource.linkedLicitationId;
        issuingEntity = licitationSource.issuingEntity;
      } else {
        return NextResponse.json(
          { error: "La propuesta de Modulo 5 no tiene un PDF disponible para analisis automatico. Sube un PDF manualmente." },
          { status: 400 },
        );
      }
    } else if (sourceLicitationId && useSourceDocument) {
      const licitationSource = await resolveLicitationSourcePdf(sourceLicitationId, issuingEntity);
      fileBuffer = licitationSource.fileBuffer;
      fileName = licitationSource.fileName;
      linkedLicitationId = licitationSource.linkedLicitationId;
      issuingEntity = licitationSource.issuingEntity;
    } else {
      return NextResponse.json(
        { error: "Debes adjuntar un PDF o vincular una propuesta de Modulo 5 / oportunidad de Modulo 3 para usar su documento principal." },
        { status: 400 },
      );
    }

    if (!fileBuffer) {
      return NextResponse.json({ error: "No fue posible obtener un documento PDF para analizar." }, { status: 400 });
    }

    const analyzed = await analyzePdf(fileBuffer);
    const aiAnalysis = await analyzeNormativeTextWithAi({
      text: analyzed.text,
      fileName,
      documentType,
      issuingEntity,
      methodUsed: analyzed.methodUsed,
      numPages: analyzed.numPages,
      warnings: analyzed.warnings,
    });
    const result = aiAnalysis.result;
    const combinedWarnings = [
      ...analyzed.warnings,
      ...aiAnalysis.warnings,
      `Motor de analisis: ${aiAnalysis.engine}${aiAnalysis.model ? ` (${aiAnalysis.model})` : ""}`,
    ];

    if (!linkedLicitationId && sourceLicitationId) {
      const linkedLicitation = await prisma.licitation.findUnique({
        where: { id: sourceLicitationId },
        select: { id: true },
      });
      linkedLicitationId = linkedLicitation?.id ?? null;
    }

    if (!linkedLicitationId && sourceProposalId) {
      const linkedProposal = await prisma.proposal.findFirst({
        where: {
          id: sourceProposalId,
          userId: user.id,
        },
        select: {
          sourceLicitationId: true,
        },
      });
      linkedLicitationId = linkedProposal?.sourceLicitationId ?? null;
    }

    const created = await prisma.normativeAnalysisHistory.create({
      data: {
        userId: user.id,
        sourceLicitationId: linkedLicitationId,
        fileName,
        documentType: toPrismaDocumentType(documentType),
        issuingEntity,
        methodUsed: analyzed.methodUsed === "ocr" ? "OCR" : "DIRECT",
        numPages: analyzed.numPages,
        warnings: combinedWarnings,
        extractedChars: analyzed.text.length,
        confidence: toPrismaConfidence(result.confidence),
        viabilityScore: result.participationViability.score,
        riskLevel: toPrismaRiskLevel(result.risk.level),
        executiveSummary: result.executiveSummary,
        result,
        analyzedAt: new Date(),
      },
      select: {
        id: true,
        sourceLicitationId: true,
        fileName: true,
        documentType: true,
        issuingEntity: true,
        methodUsed: true,
        numPages: true,
        warnings: true,
        extractedChars: true,
        analyzedAt: true,
        viabilityScore: true,
        riskLevel: true,
        confidence: true,
        result: true,
      },
    });

    return NextResponse.json({
      ok: true,
      item: toNormativeHistoryView(created),
    });
  } catch (error) {
    if (isSchemaNotReadyError(error)) {
      return NextResponse.json(
        { error: "La base de datos aun no tiene las tablas de Analisis Normativo. Ejecuta prisma migrate para continuar." },
        { status: 503 },
      );
    }

    if (error instanceof Error) {
      if (error.message === "SOURCE_LICITATION_NOT_FOUND") {
        return NextResponse.json({ error: "No se encontro la oportunidad vinculada de Modulo 3." }, { status: 404 });
      }

      if (error.message === "SOURCE_LICITATION_NO_PDF") {
        return NextResponse.json(
          { error: "La oportunidad de Modulo 3 no tiene un PDF principal disponible para analisis automatico." },
          { status: 400 },
        );
      }

      if (error.message === "SOURCE_LICITATION_NO_VALID_PDF") {
        return NextResponse.json(
          { error: "No se pudo obtener un PDF valido desde los enlaces de la oportunidad en Modulo 3." },
          { status: 422 },
        );
      }

      if (error.message === "SOURCE_LICITATION_PDF_TOO_LARGE") {
        return NextResponse.json({ error: "Los documentos vinculados de Modulo 3 exceden el limite de 20MB." }, { status: 400 });
      }

      if (error.message === "SOURCE_LICITATION_PDF_TIMEOUT") {
        return NextResponse.json({ error: "Se excedio el tiempo al descargar documentos vinculados de Modulo 3." }, { status: 504 });
      }

      if (error.message === "SOURCE_LICITATION_INVALID_PDF") {
        return NextResponse.json(
          { error: "El documento principal de Modulo 3 no tiene una firma PDF valida." },
          { status: 422 },
        );
      }

      if (error.message === "PROPOSAL_PDF_INVALID_PATH") {
        return NextResponse.json({ error: "El archivo de Modulo 5 no esta disponible para lectura segura." }, { status: 422 });
      }

      if (error.message === "PROPOSAL_PDF_NOT_FOUND") {
        return NextResponse.json({ error: "No se encontro el PDF principal cargado en Modulo 5." }, { status: 422 });
      }

      if (error.message === "PROPOSAL_PDF_EMPTY") {
        return NextResponse.json({ error: "El PDF principal de Modulo 5 esta vacio." }, { status: 422 });
      }

      if (error.message === "PROPOSAL_PDF_TOO_LARGE") {
        return NextResponse.json({ error: "El PDF principal de Modulo 5 excede el limite de 20MB." }, { status: 400 });
      }

      if (error.name === "AbortError") {
        return NextResponse.json({ error: "Se excedio el tiempo al descargar el PDF principal de Modulo 3." }, { status: 504 });
      }

      if (error.message === "SOURCE_PDF_TOO_LARGE") {
        return NextResponse.json({ error: "El PDF principal de Modulo 3 excede el limite de 20MB." }, { status: 400 });
      }

      if (error.message === "SOURCE_PDF_EMPTY") {
        return NextResponse.json({ error: "El PDF principal de Modulo 3 esta vacio." }, { status: 422 });
      }

      if (error.message.startsWith("SOURCE_FETCH_FAILED_")) {
        return NextResponse.json({ error: "No fue posible descargar el PDF principal desde la fuente de Modulo 3." }, { status: 422 });
      }
    }

    const mapped = mapAnalysisError(error);
    return NextResponse.json({ error: mapped.error, code: mapped.code }, { status: mapped.status });
  }
}
