// app/api/admin/upload/route.ts
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { Readable } from "stream";
import { IncomingMessage } from "http";
import { spawn } from "child_process";
import formidable, { Fields, Files, File as FormidableFile } from "formidable";
import { prisma } from "@/lib/prisma";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth-options";

// For 2GB uploads over Tailscale (~10-50 Mbps), need 30-45 minutes
export const maxDuration = 2700; // 45 minutes for very large uploads over slow connections

// Use ConfigurableUPLOADS_DIR from environment or default to /uploads
const UPLOADS_DIR = process.env.UPLOADS_DIR || "/uploads";

// Temp dir must live on the same filesystem as the final destination so the
// post-parse rename is a cheap atomic move instead of a multi-GB copy
const TMP_DIR = path.join(UPLOADS_DIR, "tmp");

// Helper to extract video duration using FFprobe
async function getVideoDurationFromFile(filePath: string): Promise<number | null> {
  return new Promise((resolve) => {
    try {
      const ffprobe = spawn("ffprobe", [
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1:nokey=1",
        filePath,
      ]);

      let output = "";
      let timedOut = false;

      // Large 2GB files may take longer to scan; timeout after 30s
      const timeoutId = setTimeout(() => {
        timedOut = true;
        ffprobe.kill();
        resolve(null); // Return null instead of blocking
      }, 30000);

      ffprobe.stdout.on("data", (data) => {
        output += data.toString();
      });

      ffprobe.on("close", (code) => {
        clearTimeout(timeoutId);
        if (!timedOut && code === 0) {
          const duration = parseFloat(output.trim());
          if (!isNaN(duration) && isFinite(duration) && duration > 0) {
            resolve(Math.round(duration));
          } else {
            resolve(null);
          }
        } else {
          resolve(null);
        }
      });

      ffprobe.on("error", () => {
        clearTimeout(timeoutId);
        resolve(null);
      });
    } catch (err) {
      resolve(null);
    }
  });
}

// Formidable expects a Node IncomingMessage, but App Router route handlers get
// a web Request. Bridging the body stream lets formidable write the multipart
// payload to disk as it arrives — req.formData() would buffer the whole file
// (1.6GB+) in memory and OOM the process.
function toNodeRequest(req: Request): IncomingMessage {
  if (!req.body) throw { status: 400, message: "empty request body" };
  const nodeStream = Readable.fromWeb(req.body as any) as any;
  const headers = Object.fromEntries(req.headers.entries());
  // Formidable treats a missing content-length without transfer-encoding as an
  // empty body (DummyParser); proxies that re-chunk uploads strip content-length
  if (!headers["content-length"] && headers["transfer-encoding"] === undefined) {
    headers["transfer-encoding"] = "chunked";
  }
  nodeStream.headers = headers;
  nodeStream.method = "POST";
  return nodeStream as IncomingMessage;
}

// formidable v3 returns every field/file as an array
function first<T>(value: T | T[] | undefined): T | undefined {
  return Array.isArray(value) ? value[0] : value;
}

async function parseForm(req: Request, tempPaths: string[]): Promise<{ fields: Fields; files: Files }> {
  await fs.promises.mkdir(TMP_DIR, { recursive: true });
  const form = formidable({
    multiples: false,
    uploadDir: TMP_DIR,
    maxFileSize: 2.5 * 1024 * 1024 * 1024, // 2.5GB max file size
    maxFieldsSize: 10 * 1024 * 1024, // 10MB for all fields combined
    maxFields: 50,
    keepExtensions: true,
  });
  // record temp paths as files start writing, so aborted uploads still get cleaned up
  form.on("fileBegin", (_name, file) => {
    if (file.filepath) tempPaths.push(file.filepath);
  });
  return new Promise((resolve, reject) => {
    form.parse(toNodeRequest(req), (err, fields, files) => {
      if (err) {
        console.error("[Upload] Formidable parse error:", err.code, err.message);
        reject(err);
      } else resolve({ fields, files });
    });
  });
}

// rename() is atomic when TMP_DIR shares a filesystem with dest; fall back to
// copy+unlink if the volumes are ever split across devices
async function moveFile(src: string, dest: string) {
  await fs.promises.mkdir(path.dirname(dest), { recursive: true });
  try {
    await fs.promises.rename(src, dest);
  } catch (err: any) {
    if (err?.code === "EXDEV") {
      await fs.promises.copyFile(src, dest);
      await fs.promises.unlink(src).catch(() => {});
    } else {
      throw err;
    }
  }
  // Ensure the file is readable by all processes (mode 644)
  await fs.promises.chmod(dest, 0o644);
}

async function checkAdmin() {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) throw { status: 401, message: "Unauthorized" };
  const allowed = (process.env.ALLOWED_ADMINS || "")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
  if (allowed.length && !allowed.includes(session.user.email))
    throw { status: 403, message: "Forbidden" };
  return session;
}

export async function POST(req: Request) {
  // track saved paths so we can cleanup on error
  let savedVideoDest: string | undefined;
  let savedThumbDest: string | undefined;
  // formidable temp files not yet moved to their final location
  const tempPaths: string[] = [];

  try {
    console.log('[Upload] Request received, content-type:', req.headers.get("content-type"));

    const session = await checkAdmin();

    console.log('[Upload] Admin check passed for:', session.user?.email);

    const { fields, files } = await parseForm(req, tempPaths);
    console.log('[Upload] Parsing complete, fields:', Object.keys(fields), 'files:', Object.keys(files));

    const videoFile = first(files.file) as FormidableFile | undefined;
    const thumbFile = first(files.thumbnail) as FormidableFile | undefined;

    const isManualCopy = first(fields.manualFileCopy) === "true";
    const title = (first(fields.title) as string) || undefined;
    const playlistId = (first(fields.playlistId) as string) || undefined;

    let durationSec: number | null = null;
    const durationField = first(fields.durationSec) ?? first(fields.duration);
    if (durationField) {
      const n = Number(durationField);
      if (!isNaN(n)) durationSec = Math.round(n);
    }

    let savedFilename: string | undefined;
    let thumbnailUrl: string | null = null;

    // thumbnail (optional, non-fatal on failure)
    if (thumbFile) {
      try {
        const originalThumbName = thumbFile.originalFilename || path.basename(thumbFile.filepath);
        const tExt = path.extname(originalThumbName) || ".jpg";
        const thumbFilename = `${Date.now()}-thumb${tExt}`;
        const thumbDest = path.join(UPLOADS_DIR, "thumbnails", thumbFilename);
        await moveFile(thumbFile.filepath, thumbDest);
        thumbnailUrl = `/api/thumbnails/${thumbFilename}`;
        savedThumbDest = thumbDest;
      } catch (err) {
        console.warn("thumbnail save failed", err);
      }
    }

    if (!isManualCopy) {
      if (!videoFile) return NextResponse.json({ error: "no file" }, { status: 400 });

      const originalFilename = videoFile.originalFilename || path.basename(videoFile.filepath);
      const filename = `${Date.now()}-${originalFilename}`;
      const dest = path.join(UPLOADS_DIR, "videos", filename);

      console.log('[Upload] Moving video file', { size: videoFile.size, path: videoFile.filepath, dest });
      await moveFile(videoFile.filepath, dest);
      console.log('[Upload] Video file moved successfully');

      savedFilename = filename;
      savedVideoDest = dest;
    } else {
      // Manual copy mode: don't save file, just mark for manual copy
      console.log('[Upload] Manual file copy mode enabled');
      if (videoFile) await fs.promises.unlink(videoFile.filepath).catch(() => {});
    }

    // validate playlist: avoid FK errors
    if (!playlistId) {
      // cleanup if needed
      if (savedVideoDest) await fs.promises.unlink(savedVideoDest).catch(() => {});
      if (savedThumbDest) await fs.promises.unlink(savedThumbDest).catch(() => {});
      return NextResponse.json({ error: "playlistId required" }, { status: 400 });
    }

    const playlist = await prisma.playlist.findUnique({ where: { id: playlistId } });
    if (!playlist) {
      if (savedVideoDest) await fs.promises.unlink(savedVideoDest).catch(() => {});
      if (savedThumbDest) await fs.promises.unlink(savedThumbDest).catch(() => {});
      return NextResponse.json({ error: "playlist not found" }, { status: 400 });
    }

    // Get current user
    const user = await prisma.user.findUnique({
      where: { email: session.user?.email ?? "" },
      select: { id: true },
    });
    if (!user) {
      if (savedVideoDest) await fs.promises.unlink(savedVideoDest).catch(() => {});
      if (savedThumbDest) await fs.promises.unlink(savedThumbDest).catch(() => {});
      return NextResponse.json({ error: "user not found" }, { status: 400 });
    }

    // If duration not provided by client, try to extract it from the saved video file
    if (durationSec === null && savedVideoDest) {
      try {
        const extractedDuration = await getVideoDurationFromFile(savedVideoDest);
        if (extractedDuration !== null) {
          durationSec = extractedDuration;
          console.log(`[Upload] Extracted duration: ${durationSec}s from ${savedVideoDest}`);
        } else {
          console.warn(`[Upload] Could not extract duration from ${savedVideoDest}`);
        }
      } catch (err) {
        console.warn(`[Upload] Duration extraction error:`, err);
      }
    }

    // For manual copy mode, check if we have a file
    const isManualMode = !savedVideoDest;

    // Calculate index: find max index in playlist and add 1
    const maxIndexVideo = await prisma.video.findFirst({
      where: { playlistId },
      orderBy: { index: 'desc' },
      select: { index: true },
    });
    const desiredIndex = (maxIndexVideo?.index ?? -1) + 1;

    // First, create the video record to get its ID
    const video = await prisma.video.create({
      data: {
        title: title ?? savedFilename ?? 'Untitled',
        url: '', // Will be set below or after rename
        thumbnail: thumbnailUrl,
        index: desiredIndex,
        playlistId,
        userId: user.id,
        transcodingStatus: isManualMode ? 'pending_manual_file' : 'uploaded',
        ...(durationSec !== null && { durationSec }),
      },
    });

    // If manual copy mode, set the URL and return
    if (isManualMode) {
      const videoUrl = `/uploads/videos/${video.id}.mp4`;
      await prisma.video.update({
        where: { id: video.id },
        data: { url: videoUrl },
      });
      console.log('[Upload] Manual copy mode: video created with ID', video.id, 'URL:', videoUrl);
      return NextResponse.json({ video: { ...video, url: videoUrl } }, { status: 201 });
    }

    // Now rename the uploaded file to use the video ID
    const finalFilename = `${video.id}.mp4`;
    const finalDest = path.join(UPLOADS_DIR, "videos", finalFilename);
    try {
      if (savedVideoDest) {
        await fs.promises.rename(savedVideoDest, finalDest);
      }
      // Update the URL in the database to reflect the final filename
      await prisma.video.update({
        where: { id: video.id },
        data: { url: `/uploads/videos/${finalFilename}` },
      });
    } catch (err) {
      console.error("Error renaming video file:", err);
      // If rename fails, cleanup and delete the record
      await prisma.video.delete({ where: { id: video.id } });
      throw err;
    }

    return NextResponse.json({ video: { ...video, url: `/uploads/videos/${finalFilename}` } }, { status: 201 });
  } catch (err: any) {
    console.error("upload error", {
      code: err?.code,
      message: err?.message,
      errno: err?.errno,
    }, err);

    // cleanup saved files if something failed
    if (savedVideoDest) {
      try {
        await fs.promises.unlink(savedVideoDest);
      } catch (_) {}
    }
    if (savedThumbDest) {
      try {
        await fs.promises.unlink(savedThumbDest);
      } catch (_) {}
    }
    // cleanup any formidable temp files that never got moved
    for (const p of tempPaths) {
      await fs.promises.unlink(p).catch(() => {});
    }

    const status = err?.status ?? 500;
    const message = err?.message ?? (err?.toString ? err.toString() : "server error");
    return NextResponse.json({ error: message }, { status });
  }
}
