import fs from "fs/promises";
import path from "path";
import { Privacy } from "@prisma/client";
import { readAudioTags } from "../audio-tags";
import { filenameWithoutExtension, isAllowedResolution } from "../constants";
import { prisma } from "../db";
import {
  assertArtTrackLayoutAllowed,
  assertCustomWatermarkAllowed,
  assertPerItemImagesAllowed,
} from "../entitlements";
import { FONT_UPLOAD_MAX_BYTES } from "../fonts";
import { assertFontFile } from "../fonts-server";
import { assertPngFile } from "../ffmpeg/encode";
import { moveFile, writeUploadedFile } from "../fs-utils";
import {
  ARTIST_MAX,
  INVALID_LAYOUT_TEMPLATE_MESSAGE,
  isLowerCornerTemplate,
  normalizeLayoutSettings,
  SONG_TITLE_MAX,
  type LayoutSettings,
} from "../layout";
import { enqueueVideoJob } from "../queue/client";
import {
  getPlanLimits,
  isAudioExtensionAllowed,
  isResolutionAllowedForPlan,
} from "../plans";
import { reserveQuota } from "../quota";
import { getJobDir } from "../storage";
import {
  assertPathInUserUploads,
  getUserStagingDir,
  sanitizeUploadSessionKey,
} from "../upload-paths";
import type { CreateJobPayload, ItemMetadata } from "../types";
import { resolveBurnedSongTitle, resolveYouTubeTitle, validateYouTubeTitle } from "../titles";
import {
  normalizeWatermarkSettings,
  WATERMARK_TEXT_MAX,
} from "../watermark";
import { applyBrandWatermarkPolicy } from "../watermark-policy";

/** Merge nested + flat layout fields, then normalize (throws on bad template / coords). */
export function resolveLayoutFromMetadata(metadata: ItemMetadata): LayoutSettings {
  return normalizeLayoutSettings({
    ...(metadata.layout ?? {}),
    template:
      metadata.layout?.template ??
      metadata.layout?.layoutTemplate ??
      metadata.layout?.layout_template ??
      metadata.layoutTemplate ??
      metadata.layout_template,
    blurAmount:
      metadata.layout?.blurAmount ??
      metadata.layout?.blur_amount ??
      metadata.blurAmount ??
      metadata.blur_amount,
    blurFill:
      metadata.layout?.blurFill ??
      (metadata.layout as { blur_fill?: boolean } | null | undefined)?.blur_fill ??
      metadata.blurFill ??
      metadata.blur_fill,
    blurOpacity:
      metadata.layout?.blurOpacity ??
      (metadata.layout as { blur_opacity?: number } | null | undefined)?.blur_opacity ??
      metadata.blurOpacity ??
      metadata.blur_opacity,
    textPadding:
      metadata.layout?.textPadding ??
      metadata.layout?.text_padding ??
      metadata.textPadding ??
      metadata.text_padding,
    titleArtistGap:
      metadata.layout?.titleArtistGap ??
      (metadata.layout as { title_artist_gap?: number } | null | undefined)?.title_artist_gap ??
      metadata.titleArtistGap ??
      metadata.title_artist_gap,
    textOffsetX:
      metadata.layout?.textOffsetX ??
      (metadata.layout as { text_offset_x?: number } | null | undefined)?.text_offset_x ??
      metadata.textOffsetX ??
      metadata.text_offset_x,
    textOffsetY:
      metadata.layout?.textOffsetY ??
      (metadata.layout as { text_offset_y?: number } | null | undefined)?.text_offset_y ??
      metadata.textOffsetY ??
      metadata.text_offset_y,
    titleBold:
      metadata.layout?.titleBold ??
      (metadata.layout as { title_bold?: boolean } | null | undefined)?.title_bold ??
      metadata.titleBold ??
      metadata.title_bold,
  });
}

export function validateItemMetadata(
  metadata: CreateJobPayload["items"][0]["metadata"],
) {
  const titleErr = validateYouTubeTitle(metadata);
  if (titleErr) return titleErr;
  if (!isAllowedResolution(metadata.resolution)) return "Invalid resolution";
  if (!["PUBLIC", "PRIVATE", "UNLISTED"].includes(metadata.privacy)) {
    return "Invalid privacy setting";
  }
  if (metadata.watermark?.text && metadata.watermark.text.length > WATERMARK_TEXT_MAX) {
    return `Watermark text must be at most ${WATERMARK_TEXT_MAX} characters`;
  }
  if (metadata.artist && metadata.artist.length > ARTIST_MAX) {
    return `Artist must be at most ${ARTIST_MAX} characters`;
  }
  if (metadata.songTitle && metadata.songTitle.length > SONG_TITLE_MAX) {
    return `Song title must be at most ${SONG_TITLE_MAX} characters`;
  }
  try {
    resolveLayoutFromMetadata(metadata);
  } catch (err) {
    if (err instanceof Error && err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE) {
      return INVALID_LAYOUT_TEMPLATE_MESSAGE;
    }
    throw err;
  }
  return null;
}

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

  const itemImagePaths: Array<string | null | undefined> = [];

  for (const item of body.items) {
    const metaError = validateItemMetadata(item.metadata);
    if (metaError) return metaError;
    if (!isResolutionAllowedForPlan(item.metadata.resolution)) {
      return `Resolution ${item.metadata.resolution} is not available`;
    }

    const wm = normalizeWatermarkSettings(
      item.metadata.watermark,
      item.metadata.includeWatermark,
    );
    try {
      assertCustomWatermarkAllowed(wm);
      assertArtTrackLayoutAllowed(resolveLayoutFromMetadata(item.metadata));
    } catch (err) {
      if (err instanceof Error && err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE) {
        return INVALID_LAYOUT_TEMPLATE_MESSAGE;
      }
      throw err;
    }

    itemImagePaths.push(item.metadata.imagePath);
  }

  try {
    assertPerItemImagesAllowed(body.imagePath, itemImagePaths);
  } catch (err) {
    throw err;
  }

  const limits = getPlanLimits();

  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)) {
        return `Audio file ${item.audioFilename} is not supported`;
      }
      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`;
      }

      if (item.metadata.imagePath) {
        const itemImg = assertPathInUserUploads(user.id, item.metadata.imagePath);
        await fs.access(itemImg);
        const st = await fs.stat(itemImg);
        if (st.size > limits.maxFileSizeBytes) {
          return `Per-item image for ${item.audioFilename} exceeds size limit`;
        }
      }

      const layoutForBg = resolveLayoutFromMetadata(item.metadata);
      const bgRaw =
        item.metadata.backgroundImagePath ?? item.metadata.background_image_path ?? null;
      if (bgRaw && isLowerCornerTemplate(layoutForBg.template)) {
        const bgPath = assertPathInUserUploads(user.id, bgRaw);
        await fs.access(bgPath);
        const st = await fs.stat(bgPath);
        if (st.size > limits.maxFileSizeBytes) {
          return `Background image for ${item.audioFilename} exceeds size limit`;
        }
      }

      const wm = normalizeWatermarkSettings(
        item.metadata.watermark,
        item.metadata.includeWatermark,
      );
      if (wm.mode === "logo" && wm.logoPath) {
        const logoPath = assertPathInUserUploads(user.id, wm.logoPath);
        await fs.access(logoPath);
        await assertPngFile(logoPath);
        const st = await fs.stat(logoPath);
        if (st.size > limits.maxFileSizeBytes) {
          return "Watermark logo exceeds size limit";
        }
      }
      if (wm.mode === "text" && !wm.text?.trim()) {
        return "Watermark text mode requires non-empty text";
      }
      if (wm.fontKey === "custom") {
        if (!wm.fontPath) {
          return "Custom font selected but no font file uploaded";
        }
        const fontPath = assertPathInUserUploads(user.id, wm.fontPath);
        await fs.access(fontPath);
        await assertFontFile(fontPath);
        const st = await fs.stat(fontPath);
        if (st.size > FONT_UPLOAD_MAX_BYTES) {
          return "Custom font exceeds 10 MB limit";
        }
      }
    }
  } catch (err) {
    if (err instanceof Error && err.message === "Invalid upload path") {
      return "Invalid upload path";
    }
    if (
      err instanceof Error &&
      (err.message.includes("PNG") ||
        err.message.includes("font") ||
        err.message.includes("TTF") ||
        err.message.includes("OTF") ||
        err.message.includes("WOFF"))
    ) {
      return err.message;
    }
    return "One or more uploaded files not found";
  }

  return null;
}

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

  const imagePath = assertPathInUserUploads(user.id, body.imagePath);
  const items = body.items.map((item) => {
    const layout = resolveLayoutFromMetadata(item.metadata);
    const bgRaw =
      item.metadata.backgroundImagePath ?? item.metadata.background_image_path ?? null;
    const backgroundImagePath =
      bgRaw && isLowerCornerTemplate(layout.template)
        ? assertPathInUserUploads(user.id, bgRaw)
        : null;
    return {
      ...item,
      audioPath: assertPathInUserUploads(user.id, item.audioPath),
      itemImagePath: item.metadata.imagePath
        ? assertPathInUserUploads(user.id, item.metadata.imagePath)
        : null,
      backgroundImagePath,
      watermarkLogoPath: item.metadata.watermark?.logoPath
        ? assertPathInUserUploads(user.id, item.metadata.watermark.logoPath)
        : null,
      watermarkFontPath:
        item.metadata.watermark?.fontKey === "custom" && item.metadata.watermark?.fontPath
          ? assertPathInUserUploads(user.id, item.metadata.watermark.fontPath)
          : null,
    };
  });

  await reserveQuota(user.id, items.length);

  const webhookRaw = body.webhookUrl?.trim() || null;
  if (webhookRaw) {
    try {
      const u = new URL(webhookRaw);
      if (u.protocol !== "https:" && u.protocol !== "http:") {
        throw new Error("webhookUrl must be http(s)");
      }
    } catch {
      throw new Error("Invalid webhookUrl. Provide an absolute http(s) URL for n8n callbacks.");
    }
  }

  try {
    const job = await prisma.job.create({
      data: {
        userId: user.id,
        imagePath,
        webhookUrl: webhookRaw,
        items: {
          create: items.map((item, index) => {
            const wm = applyBrandWatermarkPolicy(
              item.metadata.watermark
                ? {
                    ...item.metadata.watermark,
                    logoPath: item.watermarkLogoPath,
                    fontPath: item.watermarkFontPath,
                  }
                : null,
              item.metadata.includeWatermark,
            );
            const layout = resolveLayoutFromMetadata(item.metadata);
            const youtubeTitle = resolveYouTubeTitle(item.metadata);
            const burnedTitle = resolveBurnedSongTitle(
              item.metadata,
              filenameWithoutExtension(item.audioFilename),
            );
            return {
              audioPath: item.audioPath,
              audioFilename: item.audioFilename,
              title: youtubeTitle,
              songTitle: burnedTitle,
              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: wm.mode !== "none",
              itemImagePath: item.itemImagePath,
              backgroundImagePath: item.backgroundImagePath,
              watermarkMode: wm.mode,
              watermarkText: wm.mode === "text" ? wm.text?.trim() || null : null,
              watermarkLogoPath: wm.mode === "logo" ? item.watermarkLogoPath : null,
              watermarkFontKey: wm.fontKey ?? "system",
              watermarkFontPath:
                wm.fontKey === "custom" ? item.watermarkFontPath : null,
              watermarkPosition: wm.position,
              watermarkOffsetX: wm.offsetX,
              watermarkOffsetY: wm.offsetY,
              artist: item.metadata.artist?.trim().slice(0, ARTIST_MAX) || null,
              layoutTemplate: layout.template,
              blurFill: layout.blurFill,
              blurAmount: layout.blurAmount,
              blurOpacity: layout.blurOpacity,
              textPadding: layout.textPadding,
              titleArtistGap: layout.titleArtistGap,
              titleBold: layout.titleBold,
              textOffsetX: layout.textOffsetX,
              textOffsetY: layout.textOffsetY,
              playlistId: item.metadata.playlistId?.trim() || null,
            };
          }),
        },
      },
      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 } });

    const moved = new Map<string, string>();
    moved.set(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);

        let newItemImage: string | null = null;
        if (item.itemImagePath) {
          const src = item.itemImagePath;
          if (moved.has(src)) {
            newItemImage = moved.get(src)!;
          } else {
            const ext = path.extname(src);
            newItemImage = path.join(jobDir, `${item.id}-cover${ext}`);
            await moveFile(src, newItemImage);
            moved.set(src, newItemImage);
          }
        }

        let newBackgroundImage: string | null = null;
        if (item.backgroundImagePath) {
          const src = item.backgroundImagePath;
          if (moved.has(src)) {
            newBackgroundImage = moved.get(src)!;
          } else {
            const ext = path.extname(src);
            newBackgroundImage = path.join(jobDir, `${item.id}-bg${ext}`);
            await moveFile(src, newBackgroundImage);
            moved.set(src, newBackgroundImage);
          }
        }

        let newLogo: string | null = null;
        if (item.watermarkLogoPath) {
          const src = item.watermarkLogoPath;
          if (moved.has(src)) {
            newLogo = moved.get(src)!;
          } else {
            newLogo = path.join(jobDir, `${item.id}-logo.png`);
            await moveFile(src, newLogo);
            moved.set(src, newLogo);
          }
        }

        let newFont: string | null = null;
        if (item.watermarkFontPath) {
          const src = item.watermarkFontPath;
          if (moved.has(src)) {
            newFont = moved.get(src)!;
          } else {
            const ext = path.extname(src).toLowerCase() === ".otf" ? ".otf" : ".ttf";
            newFont = path.join(jobDir, `${item.id}-font${ext}`);
            await moveFile(src, newFont);
            moved.set(src, newFont);
          }
        }

        await prisma.jobItem.update({
          where: { id: item.id },
          data: {
            audioPath: newAudioPath,
            itemImagePath: newItemImage,
            backgroundImagePath: newBackgroundImage,
            watermarkLogoPath: newLogo,
            watermarkFontPath: newFont,
          },
        });

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

    return job;
  } catch (err) {
    throw err;
  }
}

export type UploadFileType = "image" | "audio" | "logo" | "font";

export async function saveUploadedFile(
  userId: string,
  file: File,
  type: UploadFileType,
  options?: { sessionKey?: string },
) {
  const limits = getPlanLimits();

  if (type === "font") {
    if (file.size > FONT_UPLOAD_MAX_BYTES) {
      throw new Error("Font file must be 10 MB or smaller");
    }
    if (!/\.(ttf|otf)$/i.test(file.name)) {
      throw new Error("Font must be a .ttf or .otf file");
    }
  } else if (file.size > limits.maxFileSizeBytes) {
    throw new Error(`File exceeds ${limits.maxFileSizeBytes / (1024 * 1024)} MB limit`);
  }

  if (type === "logo") {
    const nameOk = /\.png$/i.test(file.name);
    const typeOk = file.type === "image/png" || file.type === "";
    if (!nameOk && !typeOk) {
      throw new Error("Watermark logo must be a PNG file");
    }
  }

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

  if (type === "image") {
    if (
      !allowedImageTypes.includes(file.type) &&
      !file.name.match(/\.(jpg|jpeg|png|webp|gif)$/i)
    ) {
      throw new Error("Invalid image file type");
    }
  } else if (type === "audio") {
    if (
      !allowedAudioTypes.includes(file.type) &&
      !file.name.match(/\.(mp3|wav|ogg|flac|aac|m4a)$/i)
    ) {
      throw new Error("Invalid audio file type");
    }
    if (!isAudioExtensionAllowed(file.name)) {
      const allowed = limits.allowedAudioExtensions.join(", ");
      throw new Error(`Supported audio formats: ${allowed}`);
    }
  }

  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);

  if (type === "logo") {
    try {
      await assertPngFile(filePath);
    } catch (err) {
      await fs.unlink(filePath).catch(() => {});
      throw err;
    }
  }

  if (type === "font") {
    try {
      await assertFontFile(filePath);
    } catch (err) {
      await fs.unlink(filePath).catch(() => {});
      throw err;
    }
  }

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