import fs from "fs/promises";
import path from "path";
import { Plan, Privacy } from "@prisma/client";
import { readAudioTags } from "../audio-tags";
import { isAllowedResolution } from "../constants";
import { prisma } from "../db";
import { hasProFeatures } from "../edition";
import { moveFile, writeUploadedFile } from "../fs-utils";
import { enqueueVideoJob } from "../queue/client";
import {
  getPlanLimits,
  isAudioExtensionAllowed,
  isResolutionAllowedForPlan,
} from "../plans";
import { releaseReservationSplit, reserveQuota } from "../quota";
import { getJobDir } from "../storage";
import {
  assertPathInUserUploads,
  getUserStagingDir,
  sanitizeUploadSessionKey,
} from "../upload-paths";
import type { CreateJobPayload } from "../types";

export function validateItemMetadata(metadata: CreateJobPayload["items"][0]["metadata"]) {
  if (!metadata.title?.trim()) return "Each video must have a title";
  if (!isAllowedResolution(metadata.resolution)) return "Invalid resolution";
  if (!["PUBLIC", "PRIVATE", "UNLISTED"].includes(metadata.privacy)) {
    return "Invalid privacy setting";
  }
  return null;
}

export async function validateJobPayload(user: { id: string; plan: Plan }, body: CreateJobPayload) {
  if (!body.imagePath || !body.items?.length) {
    return "Image and at least one audio file required";
  }

  for (const item of body.items) {
    const metaError = validateItemMetadata(item.metadata);
    if (metaError) return metaError;
    if (!isResolutionAllowedForPlan(item.metadata.resolution, user.plan)) {
      return `Resolution ${item.metadata.resolution} is not available on your plan`;
    }
    if (item.metadata.playlistId && !hasProFeatures(user.plan)) {
      return "Adding videos to a YouTube playlist requires the Pro plan";
    }
  }

  const limits = getPlanLimits(user.plan);

  try {
    const imagePath = assertPathInUserUploads(user.id, body.imagePath);
    await fs.access(imagePath);
    const imageStat = await fs.stat(imagePath);
    if (imageStat.size > limits.maxFileSizeBytes) {
      return "Image exceeds size limit";
    }

    for (const item of body.items) {
      if (!isAudioExtensionAllowed(item.audioFilename, user.plan)) {
        return `Audio file ${item.audioFilename} is not supported on your plan`;
      }
      const audioPath = assertPathInUserUploads(user.id, item.audioPath);
      await fs.access(audioPath);
      const stat = await fs.stat(audioPath);
      if (stat.size > limits.maxFileSizeBytes) {
        return `Audio file ${item.audioFilename} exceeds size limit`;
      }
    }
  } catch (err) {
    if (err instanceof Error && err.message === "Invalid upload path") {
      return "Invalid upload path";
    }
    return "One or more uploaded files not found";
  }

  return null;
}

export async function createVideoJob(
  user: { id: string; plan: Plan },
  body: CreateJobPayload,
) {
  const validationError = await validateJobPayload(user, body);
  if (validationError) {
    throw new Error(validationError);
  }

  const imagePath = assertPathInUserUploads(user.id, body.imagePath);
  const items = body.items.map((item) => ({
    ...item,
    audioPath: assertPathInUserUploads(user.id, item.audioPath),
  }));

  const reservation = await reserveQuota(user.id, items.length);

  try {
    const job = await prisma.job.create({
      data: {
        userId: user.id,
        imagePath,
        items: {
          create: items.map((item, index) => ({
            audioPath: item.audioPath,
            audioFilename: item.audioFilename,
            title: item.metadata.title.trim(),
            description: item.metadata.description || "",
            tags: item.metadata.tags || "",
            privacy: item.metadata.privacy as Privacy,
            categoryId: item.metadata.categoryId || "10",
            resolution: item.metadata.resolution,
            notifySubscribers: item.metadata.notifySubscribers,
            madeForKids: item.metadata.madeForKids,
            embeddable: item.metadata.embeddable,
            creativeCommons: item.metadata.creativeCommons,
            includeWatermark: item.metadata.includeWatermark,
            playlistId: item.metadata.playlistId?.trim() || null,
            billingSource: index < reservation.fromQuota ? "QUOTA" : "CREDIT",
          })),
        },
      },
      include: { items: true },
    });

    const jobDir = getJobDir(user.id, job.id);
    await fs.mkdir(jobDir, { recursive: true });

    const imageExt = path.extname(imagePath);
    const newImagePath = path.join(jobDir, `image${imageExt}`);
    await moveFile(imagePath, newImagePath);
    await prisma.job.update({ where: { id: job.id }, data: { imagePath: newImagePath } });

    await Promise.all(
      job.items.map(async (item) => {
        const audioExt = path.extname(item.audioPath);
        const newAudioPath = path.join(jobDir, `${item.id}${audioExt}`);
        await moveFile(item.audioPath, newAudioPath);
        await prisma.jobItem.update({
          where: { id: item.id },
          data: { audioPath: newAudioPath },
        });

        await enqueueVideoJob({
          jobItemId: item.id,
          userId: user.id,
          jobId: job.id,
        });
      }),
    );

    return job;
  } catch (err) {
    await releaseReservationSplit(user.id, reservation).catch(() => {});
    throw err;
  }
}

export async function saveUploadedFile(
  userId: string,
  file: File,
  type: "image" | "audio",
  plan: Plan,
  options?: { sessionKey?: string },
) {
  const limits = getPlanLimits(plan);

  if (file.size > limits.maxFileSizeBytes) {
    throw new Error(`File exceeds ${limits.maxFileSizeBytes / (1024 * 1024)} MB limit`);
  }

  const allowedImageTypes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
  const allowedAudioTypes = [
    "audio/mpeg",
    "audio/mp3",
    "audio/wav",
    "audio/x-wav",
    "audio/ogg",
    "audio/flac",
    "audio/aac",
    "audio/mp4",
    "audio/x-m4a",
  ];
  const allowedTypes = type === "image" ? allowedImageTypes : allowedAudioTypes;

  if (
    !allowedTypes.includes(file.type) &&
    !file.name.match(/\.(mp3|wav|ogg|flac|aac|m4a|jpg|jpeg|png|webp|gif)$/i)
  ) {
    throw new Error("Invalid file type");
  }

  if (type === "audio" && !isAudioExtensionAllowed(file.name, plan)) {
    const allowed = limits.allowedAudioExtensions.join(", ");
    throw new Error(`Your plan supports ${allowed} audio files only`);
  }

  const sessionKey = sanitizeUploadSessionKey(options?.sessionKey);
  const sessionDir = getUserStagingDir(userId, sessionKey);
  await fs.mkdir(sessionDir, { recursive: true });

  const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
  const uniqueName = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safeName}`;
  const filePath = path.join(sessionDir, uniqueName);
  assertPathInUserUploads(userId, filePath);
  await writeUploadedFile(file, filePath);

  let audioTags = null;
  if (type === "audio" && limits.id3TagSupport && file.name.toLowerCase().endsWith(".mp3")) {
    audioTags = await readAudioTags(filePath);
  }

  return {
    path: filePath,
    filename: file.name,
    size: file.size,
    audioTags,
  };
}
