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 { evaluateSignaturePolicy } from "@/lib/compliance/signature-policy";
import { parseLicitationDocumentLinks, rankLicitationDocumentsForBundle, type LicitationBundleDocumentKind } from "@/lib/licitations/documents";
import { type NormativeDocumentType } from "@/lib/normative-analysis/analyze";
import { analyzeNormativeTextWithAi } from "@/lib/normative-analysis/ai-analyze";
import { mergeNormativeBundleSources, type NormativeBundleSourceAnalysis } from "@/lib/normative-analysis/bundle";
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";
import { canTransitionProposalStatus } from "@/lib/proposals/status";
import { buildDefaultWorkflowDraft } from "@/lib/proposals/workflow-default";
import { applyAiSeedsToWorkflow, buildMilestoneSeedsFromAnalysis, buildRequirementSeeds, buildTechnicalSectionSeeds, type WorkflowMilestoneSeed } from "@/lib/proposals/workflow";
import { ProposalWorkflowStateSchema, computeWorkflowCompletionPercent, computeWorkflowGate } from "@/lib/proposals/workflow-state";

export const runtime = "nodejs";

const MAX_NORMATIVE_PDF_BYTES = 20 * 1024 * 1024;
const SOURCE_FETCH_TIMEOUT_MS = 45_000;

type BundleCandidate = {
  sourceId: string;
  fileName: string;
  documentType: NormativeDocumentType;
  fileBuffer: Buffer;
};

function logContinuityHandoff(event: string, data: Record<string, unknown>) {
  console.info(
    "[continuity_handoff]",
    JSON.stringify({
      event,
      at: new Date().toISOString(),
      ...data,
    }),
  );
}

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

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

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

  return "OTRO";
}

function mapBundleKindToDocumentType(kind: LicitationBundleDocumentKind): NormativeDocumentType {
  if (kind === "BASES_LICITACION") {
    return "BASES_LICITACION";
  }

  if (kind === "CONVOCATORIA") {
    return "CONVOCATORIA";
  }

  if (kind === "ANEXOS") {
    return "ANEXOS";
  }

  return "OTRO";
}

function inferDocumentTypeFromName(fileName: string): NormativeDocumentType {
  const lower = fileName
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase();

  if (/\bbase(s)?\b|\bpliego\b|\bterminos\b/.test(lower)) {
    return "BASES_LICITACION";
  }

  if (/\bconvocatoria\b|\binvitacion\b/.test(lower)) {
    return "CONVOCATORIA";
  }

  if (/\banexo(s)?\b|\bformato(s)?\b/.test(lower)) {
    return "ANEXOS";
  }

  return "OTRO";
}

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";
  }
}

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 hasPdfSignature(buffer: Buffer) {
  return buffer.subarray(0, 5).toString("utf8") === "%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");
    }

    if (!hasPdfSignature(buffer)) {
      throw new Error("SOURCE_PDF_INVALID");
    }

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

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

  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    return null;
  }

  return parsed;
}

function toMilestoneSeeds(args: {
  openingDate: Date | null;
  closingDate: Date | null;
  eventDates: Prisma.JsonValue | null;
}): WorkflowMilestoneSeed[] {
  const milestones: WorkflowMilestoneSeed[] = [];

  if (args.openingDate) {
    milestones.push({
      id: "opening-date",
      title: "Apertura del proceso",
      dateIso: args.openingDate.toISOString(),
      location: "",
      note: "",
      source: "licitation",
    });
  }

  if (args.closingDate) {
    milestones.push({
      id: "closing-date",
      title: "Presentacion de propuestas",
      dateIso: args.closingDate.toISOString(),
      location: "",
      note: "",
      source: "licitation",
    });
  }

  if (args.eventDates && typeof args.eventDates === "object" && !Array.isArray(args.eventDates)) {
    const entries = Object.entries(args.eventDates as Record<string, unknown>);
    for (let index = 0; index < entries.length; index += 1) {
      const [key, value] = entries[index];
      const parsed = parseDateCandidate(value);
      milestones.push({
        id: `event-date-${index + 1}`,
        title: key || `Evento ${index + 1}`,
        dateIso: parsed ? parsed.toISOString() : null,
        location: "",
        note: "",
        source: "licitation",
      });
    }
  }

  return milestones;
}

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",
  };
}

export async function POST(request: Request, context: { params: Promise<{ id: string }> }) {
  const user = await requireAdminApiUser();

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

  try {
    const { id } = await context.params;
    const proposal = await prisma.proposal.findFirst({
      where: {
        id,
        userId: user.id,
      },
      select: {
        id: true,
        title: true,
        issuingEntity: true,
        summary: true,
        status: true,
        sourceLicitationId: true,
        workflowDraft: true,
      },
    });

    if (!proposal) {
      return NextResponse.json({ error: "Propuesta no encontrada." }, { status: 404 });
    }

    const linkedSource = proposal.sourceLicitationId
      ? await prisma.licitation.findUnique({
          where: { id: proposal.sourceLicitationId },
          include: {
            municipality: {
              select: {
                stateName: true,
                municipalityName: true,
              },
            },
          },
        })
      : null;

    const formData = await request.formData().catch(() => null);
    const filesFromMulti = formData
      ? formData.getAll("files").filter((item): item is File => item instanceof File && item.size > 0)
      : [];
    const maybeSingleFile = formData ? formData.get("file") : null;
    const singleFile = maybeSingleFile instanceof File && maybeSingleFile.size > 0 ? maybeSingleFile : null;
    const uploadedFiles = filesFromMulti.length > 0 ? filesFromMulti : singleFile ? [singleFile] : [];
    const documentType = parseDocumentType(formData?.get("documentType"));
    const issuingEntityFromRequest = typeof formData?.get("issuingEntity") === "string" ? String(formData?.get("issuingEntity")).trim() : "";
    const issuingEntity = issuingEntityFromRequest || proposal.issuingEntity || linkedSource?.supplierAwarded || null;
    const requestedDocumentTypes = formData
      ? formData
          .getAll("documentTypes")
          .map((value) => parseDocumentType(value))
      : [];

    logContinuityHandoff("m4_to_m5_extract_requested", {
      userId: user.id,
      proposalId: proposal.id,
      sourceLicitationId: proposal.sourceLicitationId,
      uploadedFiles: uploadedFiles.length,
    });

    let analysisResult: ReturnType<typeof toNormativeHistoryView>["result"] | null = null;
    let analysisMeta: {
      source: "module4_history" | "uploaded_pdf";
      engine: string | null;
      model: string | null;
      warnings: string[];
      historyId: string | null;
      bundle?: {
        sources: Array<{ sourceId: string; fileName: string; documentType: NormativeDocumentType; historyId: string | null }>;
        conflicts: Array<{ title: string; field: string; keptDocumentType: NormativeDocumentType; replacedDocumentType: NormativeDocumentType }>;
      };
    } = {
      source: "module4_history",
      engine: null,
      model: null,
      warnings: [],
      historyId: null,
      bundle: {
        sources: [],
        conflicts: [],
      },
    };

    const bundleCandidates: BundleCandidate[] = [];

    if (uploadedFiles.length > 0) {
      for (let index = 0; index < uploadedFiles.length; index += 1) {
        const file = uploadedFiles[index];
        if (!file.name.toLowerCase().endsWith(".pdf") && file.type !== "application/pdf") {
          return NextResponse.json({ error: "Solo se permiten archivos PDF para extraccion IA." }, { status: 400 });
        }

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

        const buffer = Buffer.from(await file.arrayBuffer());
        if (!hasPdfSignature(buffer)) {
          return NextResponse.json({ error: "El archivo no parece ser un PDF valido." }, { status: 400 });
        }

        const inferredType = inferDocumentTypeFromName(file.name);
        const resolvedType =
          requestedDocumentTypes[index] ??
          (uploadedFiles.length === 1 && documentType !== "OTRO" ? documentType : inferredType !== "OTRO" ? inferredType : "OTRO");

        bundleCandidates.push({
          sourceId: `upload-${index + 1}`,
          fileName: file.name,
          fileBuffer: buffer,
          documentType: resolvedType,
        });
      }
    } else if (linkedSource) {
      const sourceDocuments = parseLicitationDocumentLinks(linkedSource.documents);
      const ranked = rankLicitationDocumentsForBundle(sourceDocuments, 1);
      const ordered = [...ranked.bases, ...ranked.convocatoria, ...ranked.anexos, ...ranked.extras];
      const rawSourceUrl = linkedSource.rawSourceUrl?.trim() ?? "";
      const candidateDocuments = [...ordered];

      if (rawSourceUrl && !candidateDocuments.some((item) => item.url === rawSourceUrl)) {
        candidateDocuments.push({
          name: guessFileNameFromUrl(rawSourceUrl),
          url: rawSourceUrl,
          type: null,
          bundleKind: "OTRO",
          bundleScore: -10_000,
        });
      }

      for (let index = 0; index < candidateDocuments.length && index < 6; index += 1) {
        const document = candidateDocuments[index];
        try {
          const buffer = await fetchSourcePdf(document.url);
          bundleCandidates.push({
            sourceId: `source-${index + 1}`,
            fileName: document.name,
            fileBuffer: buffer,
            documentType: mapBundleKindToDocumentType(document.bundleKind),
          });
        } catch (error) {
          analysisMeta.warnings.push(error instanceof Error ? `No se pudo leer ${document.name}: ${error.message}` : `No se pudo leer ${document.name}.`);
        }
      }
    }

    if (bundleCandidates.length > 0) {
      const bundleAnalyses: NormativeBundleSourceAnalysis[] = [];

      for (const candidate of bundleCandidates.slice(0, 3)) {
        try {
          const analyzed = await analyzePdf(candidate.fileBuffer);
          const aiAnalysis = await analyzeNormativeTextWithAi({
            text: analyzed.text,
            fileName: candidate.fileName,
            documentType: candidate.documentType,
            issuingEntity,
            methodUsed: analyzed.methodUsed,
            numPages: analyzed.numPages,
            warnings: analyzed.warnings,
          });

          const combinedWarnings = [
            ...analyzed.warnings,
            ...aiAnalysis.warnings,
            `Motor de analisis: ${aiAnalysis.engine}${aiAnalysis.model ? ` (${aiAnalysis.model})` : ""}`,
          ];

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

          bundleAnalyses.push({
            sourceId: candidate.sourceId,
            fileName: candidate.fileName,
            documentType: candidate.documentType,
            result: aiAnalysis.result,
          });

          analysisMeta.source = "uploaded_pdf";
          analysisMeta.engine = analysisMeta.engine ?? aiAnalysis.engine;
          analysisMeta.model = analysisMeta.model ?? aiAnalysis.model;
          analysisMeta.historyId = analysisMeta.historyId ?? createdHistory.id;
          analysisMeta.warnings.push(...combinedWarnings);
          analysisMeta.bundle?.sources.push({
            sourceId: candidate.sourceId,
            fileName: candidate.fileName,
            documentType: candidate.documentType,
            historyId: createdHistory.id,
          });
        } catch (error) {
          const mapped = mapAnalysisError(error);
          analysisMeta.warnings.push(`${candidate.fileName}: ${mapped.error}`);
        }
      }

      if (bundleAnalyses.length === 0) {
        return NextResponse.json({ error: "No fue posible analizar los documentos del bundle." }, { status: 422 });
      }

      if (bundleAnalyses.length === 1) {
        analysisResult = bundleAnalyses[0]?.result ?? null;
      } else {
        const merged = mergeNormativeBundleSources(bundleAnalyses);
        analysisResult = merged.result;
        analysisMeta.bundle = {
          sources: merged.sources.map((item) => ({
            ...item,
            historyId: analysisMeta.bundle?.sources.find((source) => source.sourceId === item.sourceId)?.historyId ?? null,
          })),
          conflicts: merged.conflicts.map((conflict) => ({
            title: conflict.title,
            field: conflict.field,
            keptDocumentType: conflict.keptDocumentType,
            replacedDocumentType: conflict.replacedDocumentType,
          })),
        };
      }
    } else {
      if (!proposal.sourceLicitationId) {
        logContinuityHandoff("m4_to_m5_extract_blocked_no_source_context", {
          userId: user.id,
          proposalId: proposal.id,
        });
        return NextResponse.json(
          { error: "La propuesta no tiene fuente de licitacion vinculada de Modulo 4. Sube un PDF para extraer requisitos con IA." },
          { status: 400 },
        );
      }

      const historyEntry = await prisma.normativeAnalysisHistory.findFirst({
        where: {
          userId: user.id,
          deletedAt: null,
          sourceLicitationId: proposal.sourceLicitationId,
        },
        orderBy: [{ analyzedAt: "desc" }],
        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,
        },
      });

      if (!historyEntry) {
        logContinuityHandoff("m4_to_m5_extract_blocked_no_history", {
          userId: user.id,
          proposalId: proposal.id,
          sourceLicitationId: proposal.sourceLicitationId,
        });
        return NextResponse.json(
          { error: "No hay analisis previo de Modulo 4 vinculado. Sube un PDF para extraer requisitos con IA." },
          { status: 400 },
        );
      }

      const historyView = toNormativeHistoryView(historyEntry);
      analysisResult = historyView.result;
      analysisMeta = {
        source: "module4_history",
        engine: "module4_history",
        model: null,
        warnings: historyView.warnings,
        historyId: historyView.id,
        bundle: {
          sources: [
            {
              sourceId: `history-${historyView.id}`,
              fileName: historyView.fileName,
              documentType: historyView.documentType,
              historyId: historyView.id,
            },
          ],
          conflicts: [],
        },
      };
    }

    if (!analysisResult) {
      return NextResponse.json({ error: "No se pudo recuperar resultado de analisis IA." }, { status: 400 });
    }

    const requirementSeeds = buildRequirementSeeds(analysisResult);
    const technicalSectionSeeds = buildTechnicalSectionSeeds(analysisResult);
    const aiMilestoneSeeds = buildMilestoneSeedsFromAnalysis(analysisResult);
    const sourceMilestoneSeeds = linkedSource
      ? toMilestoneSeeds({
          openingDate: linkedSource.openingDate,
          closingDate: linkedSource.closingDate,
          eventDates: linkedSource.eventDates,
        })
      : [];
    const milestoneSeeds = [...sourceMilestoneSeeds, ...aiMilestoneSeeds];
    const existingWorkflowParsed = ProposalWorkflowStateSchema.safeParse(proposal.workflowDraft);
    const defaultDraft = buildDefaultWorkflowDraft({
      proposal: {
        title: proposal.title,
        issuingEntity: proposal.issuingEntity,
        summary: proposal.summary,
        status: proposal.status,
      },
      linkedSource: linkedSource
        ? {
            title: linkedSource.title,
            description: linkedSource.description,
            issuingEntity: linkedSource.supplierAwarded,
            procedureType: linkedSource.procedureType,
            stateName: linkedSource.municipality.stateName,
            municipalityName: linkedSource.municipality.municipalityName,
            sourceUrl: linkedSource.rawSourceUrl,
            milestones: milestoneSeeds,
          }
        : null,
      requirementSeeds,
      technicalSectionSeeds,
    });

    const baseWorkflow = existingWorkflowParsed.success ? existingWorkflowParsed.data : defaultDraft;
    const mergedWorkflow = applyAiSeedsToWorkflow(baseWorkflow, {
      requirementSeeds,
      technicalSectionSeeds,
      milestoneSeeds,
    });
    const extractedDocumentType = analysisMeta.bundle?.sources[0]?.documentType ?? documentType;
    const signaturePolicy = evaluateSignaturePolicy({
      stateName: linkedSource?.municipality.stateName ?? null,
      municipalityName: linkedSource?.municipality.municipalityName ?? null,
      documentType: extractedDocumentType,
    });
    const workflow = {
      ...mergedWorkflow,
      signatureCompliance: {
        ...mergedWorkflow.signatureCompliance,
        policyStatus: signaturePolicy.policyStatus,
        policyName: signaturePolicy.policyName,
        jurisdictionLabel: signaturePolicy.jurisdictionLabel,
        sourceUrl: signaturePolicy.sourceUrl ?? "",
        minimumEvidence: signaturePolicy.evidenceRequired,
        notes: signaturePolicy.notes,
      },
      step: Math.max(2, mergedWorkflow.step),
    };

    const completionPercent = computeWorkflowCompletionPercent(workflow);
    const gate = computeWorkflowGate(workflow);

    const nextStatus =
      proposal.status === "DRAFT" && completionPercent > 0 && canTransitionProposalStatus("DRAFT", "IN_PROGRESS") ? "IN_PROGRESS" : proposal.status;

    const updated = await prisma.proposal.update({
      where: { id: proposal.id },
      data: {
        workflowDraft: workflow as Prisma.InputJsonValue,
        currentStep: workflow.step,
        completionPercent,
        ...(nextStatus !== proposal.status ? { status: nextStatus } : {}),
        ...(gate.allReady ? {} : { readyForSubmissionAt: null }),
      },
      select: {
        id: true,
        status: true,
        currentStep: true,
        completionPercent: true,
        readyForSubmissionAt: true,
      },
    });

    logContinuityHandoff("m4_to_m5_extract_completed", {
      userId: user.id,
      proposalId: proposal.id,
      sourceLicitationId: proposal.sourceLicitationId,
      analysisSource: analysisMeta.source,
      historyId: analysisMeta.historyId,
      bundleSources: analysisMeta.bundle?.sources.length ?? 0,
      bundleConflicts: analysisMeta.bundle?.conflicts.length ?? 0,
      completionPercent,
      currentStep: updated.currentStep,
    });

    return NextResponse.json({
      ok: true,
      proposal: {
        id: updated.id,
        status: updated.status,
        currentStep: updated.currentStep,
        completionPercent: updated.completionPercent,
        readyForSubmissionAt: updated.readyForSubmissionAt ? updated.readyForSubmissionAt.toISOString() : null,
      },
      workflow,
      gate,
      analysis: analysisMeta,
    });
  } catch (error) {
    if (isSchemaNotReadyError(error)) {
      return NextResponse.json(
        { error: "La base de datos aun no tiene los campos de persistencia de Gestion de Licitaciones. Ejecuta prisma migrate para continuar." },
        { status: 503 },
      );
    }

    return NextResponse.json({ error: "No fue posible extraer requisitos con IA." }, { status: 400 });
  }
}
