/**
 * Streaming multipart parser for large PDF uploads.
 *
 * 1. Streams the request body to a raw temp file (constant memory)
 * 2. Scans the raw file for multipart boundaries using small buffer reads
 * 3. Copies just the file part to a separate PDF file (stream copy)
 *
 * Peak memory: ~64KB regardless of file size.
 */

import { NextRequest } from "next/server";
import {
  createWriteStream,
  createReadStream,
  openSync,
  readSync,
  closeSync,
  statSync,
} from "fs";
import { mkdir, unlink } from "fs/promises";
import { randomUUID } from "crypto";
import { join } from "path";
import { tmpdir } from "os";
import { pipeline } from "stream/promises";

export interface ParsedUpload {
  filePath: string;
  filename: string;
  size: number;
  tmpDir: string;
  fields: Record<string, string>;
}

/**
 * Scan a file on disk for a Buffer pattern starting from `offset`.
 * Reads in 64KB chunks — constant memory.
 */
function findInFile(
  filePath: string,
  pattern: Buffer,
  startOffset: number,
): number {
  const CHUNK = 65536;
  const fd = openSync(filePath, "r");
  try {
    const buf = Buffer.alloc(CHUNK + pattern.length);
    let fileOffset = startOffset;
    const fileSize = statSync(filePath).size;

    while (fileOffset < fileSize) {
      const bytesRead = readSync(
        fd,
        buf,
        0,
        Math.min(buf.length, fileSize - fileOffset),
        fileOffset,
      );
      if (bytesRead === 0) break;

      const idx = buf.subarray(0, bytesRead).indexOf(pattern);
      if (idx !== -1) {
        return fileOffset + idx;
      }

      // Advance, but overlap by pattern length to catch split matches
      fileOffset += bytesRead - pattern.length;
    }
    return -1;
  } finally {
    closeSync(fd);
  }
}

/**
 * Read a small chunk from a file at a given offset.
 */
function readChunk(filePath: string, offset: number, length: number): Buffer {
  const fd = openSync(filePath, "r");
  try {
    const buf = Buffer.alloc(length);
    const bytesRead = readSync(fd, buf, 0, length, offset);
    return buf.subarray(0, bytesRead);
  } finally {
    closeSync(fd);
  }
}

/**
 * Copy a byte range from one file to another using streams.
 */
async function copyFileRange(
  srcPath: string,
  destPath: string,
  start: number,
  end: number,
): Promise<void> {
  const rs = createReadStream(srcPath, { start, end: end - 1 });
  const ws = createWriteStream(destPath);
  await pipeline(rs, ws);
}

export async function parseMultipartUpload(
  req: NextRequest,
): Promise<ParsedUpload> {
  const contentType = req.headers.get("content-type") ?? "";
  if (!req.body) throw new Error("Lipsește body-ul cererii.");

  const boundaryMatch = contentType.match(/boundary=(.+?)(?:;|$)/);
  if (!boundaryMatch?.[1]) throw new Error("Lipsește boundary din Content-Type.");
  const boundary = boundaryMatch[1].trim();

  const tmpDir = join(tmpdir(), `pdf-upload-${randomUUID()}`);
  await mkdir(tmpDir, { recursive: true });

  // Step 1: Stream entire body to disk (constant memory)
  const rawPath = join(tmpDir, "raw-body");
  const ws = createWriteStream(rawPath);
  const reader = req.body.getReader();

  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      const ok = ws.write(Buffer.from(value));
      if (!ok) await new Promise<void>((r) => ws.once("drain", r));
    }
  } finally {
    ws.end();
    await new Promise<void>((r) => ws.once("finish", r));
  }

  const rawSize = statSync(rawPath).size;
  console.log(`[parse-upload] Raw body saved: ${rawSize} bytes`);

  // Step 2: Find file part boundaries using small buffer reads
  const boundaryBuf = Buffer.from(`--${boundary}`);
  const headerEndBuf = Buffer.from("\r\n\r\n");
  const closingBuf = Buffer.from(`\r\n--${boundary}`);

  let filename = "input.pdf";
  let fileStart = -1;
  let searchFrom = 0;
  const fields: Record<string, string> = {};

  while (searchFrom < rawSize) {
    const partStart = findInFile(rawPath, boundaryBuf, searchFrom);
    if (partStart === -1) break;

    const headerEnd = findInFile(
      rawPath,
      headerEndBuf,
      partStart + boundaryBuf.length,
    );
    if (headerEnd === -1) break;

    // Read just the headers (small — typically <500 bytes)
    const headersLen = headerEnd - (partStart + boundaryBuf.length);
    const headers = readChunk(
      rawPath,
      partStart + boundaryBuf.length,
      Math.min(headersLen, 2048),
    ).toString("utf8");

    if (headers.includes("filename=")) {
      const fnMatch = headers.match(/filename="([^"]+)"/);
      if (fnMatch?.[1]) filename = fnMatch[1];
      fileStart = headerEnd + 4;
      break;
    }

    // Parse form field value
    const nameMatch = headers.match(
      /Content-Disposition:\s*form-data;\s*name="([^"]+)"/,
    );
    if (nameMatch?.[1]) {
      const valStart = headerEnd + 4;
      const nextBoundary = findInFile(rawPath, closingBuf, valStart);
      if (nextBoundary !== -1 && nextBoundary - valStart < 10000) {
        fields[nameMatch[1]] = readChunk(
          rawPath,
          valStart,
          nextBoundary - valStart,
        ).toString("utf8");
      }
    }

    searchFrom = headerEnd + 4;
  }

  if (fileStart === -1) throw new Error("Lipsește fișierul PDF din upload.");

  const fileEnd = findInFile(rawPath, closingBuf, fileStart);
  const pdfEnd = fileEnd > fileStart ? fileEnd : rawSize;
  const pdfSize = pdfEnd - fileStart;

  if (pdfSize < 100) throw new Error("Fișierul PDF extras este gol sau prea mic.");

  console.log(
    `[parse-upload] PDF extracted: ${pdfSize} bytes (offset ${fileStart}..${pdfEnd})`,
  );

  // Step 3: Copy just the PDF bytes to a new file (stream copy)
  const filePath = join(tmpDir, filename);
  await copyFileRange(rawPath, filePath, fileStart, pdfEnd);

  // Delete raw body — no longer needed
  await unlink(rawPath).catch(() => {});

  return { filePath, filename, size: pdfSize, tmpDir, fields };
}
