import { spawn } from "child_process";
import fs from "fs/promises";
import path from "path";
import ffmpegStatic from "ffmpeg-static";
import { getVideoAttributionText } from "../branding";
import { getResolution } from "../constants";
import {
  isCuratedFontKey,
  sanitizeFontfileForFilter,
} from "../fonts";
import { resolveCuratedFontPath } from "../fonts-server";
import {
  buildArtTrackFilterComplex,
  type LayoutSettings,
} from "../layout";
import {
  WATERMARK_WIDTH_FRACTION,
  watermarkFontSizeForWidth,
} from "../preview-typography";
import { getWatermarkPath } from "../storage";
import {
  buildDrawtextFilter,
  normalizeWatermarkSettings,
  overlayXy,
  sanitizeDrawtext,
  type WatermarkSettings,
} from "../watermark";
import { buildBrandWatermarkOverlayFilters } from "./brand-watermark";

function getFfmpegPath(): string {
  if (process.env.FFMPEG_PATH) return process.env.FFMPEG_PATH;
  if (ffmpegStatic) return ffmpegStatic;
  return "ffmpeg";
}

export { getFfmpegPath };

async function fileExists(p: string): Promise<boolean> {
  try {
    await fs.access(p);
    return true;
  } catch {
    return false;
  }
}

/** Verify PNG magic bytes (prevents MIME spoof → FFmpeg surprises). */
export async function assertPngFile(filePath: string): Promise<void> {
  const fh = await fs.open(filePath, "r");
  try {
    const buf = Buffer.alloc(8);
    await fh.read(buf, 0, 8, 0);
    const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
    if (!buf.equals(sig)) {
      throw new Error("Logo must be a valid PNG file");
    }
  } finally {
    await fh.close();
  }
}

async function resolveFontfileEscaped(
  settings: WatermarkSettings,
): Promise<string | null> {
  const key = settings.fontKey ?? "system";
  if (key === "system") return null;

  if (key === "custom") {
    if (!settings.fontPath) return null;
    if (!(await fileExists(settings.fontPath))) {
      throw new Error("Custom watermark font file not found");
    }
    return sanitizeFontfileForFilter(path.resolve(settings.fontPath));
  }

  if (isCuratedFontKey(key)) {
    const fontPath = resolveCuratedFontPath(key);
    if (!(await fileExists(fontPath))) {
      console.warn(`[ffmpeg] curated font missing: ${key} at ${fontPath}; using system font`);
      return null;
    }
    return sanitizeFontfileForFilter(fontPath);
  }

  return null;
}

function classicScaleFilter(width: number, height: number): string {
  return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black`;
}

/**
 * Art-track filter that ends at `outLabel` instead of hardcoded [laid].
 */
function artTrackFilterEndingAt(
  opts: Parameters<typeof buildArtTrackFilterComplex>[0],
  outLabel: string,
): string {
  return buildArtTrackFilterComplex(opts).replace(/\[laid\]$/, `[${outLabel}]`);
}

export async function encodeVideo(options: {
  imagePath: string;
  audioPath: string;
  outputPath: string;
  resolution: string;
  includeWatermark: boolean;
  watermark?: Partial<WatermarkSettings> | null;
  layout?: LayoutSettings | null;
  /** On-video song title (art-track layouts). */
  songTitle?: string;
  artist?: string | null;
}): Promise<void> {
  const res = getResolution(options.resolution);
  if (!res) throw new Error(`Invalid resolution: ${options.resolution}`);

  await fs.mkdir(path.dirname(options.outputPath), { recursive: true });

  const settings = normalizeWatermarkSettings(options.watermark, options.includeWatermark);
  const layout = options.layout?.template ? options.layout : null;
  const watermarkWidth = Math.max(1, Math.round(res.width * WATERMARK_WIDTH_FRACTION));
  const fontSize = watermarkFontSizeForWidth(res.width);
  const fontfile = await resolveFontfileEscaped(settings);

  const args = ["-y", "-loop", "1", "-r", "1", "-i", options.imagePath, "-i", options.audioPath];

  let nextInput = 2;
  let logoInputIndex: number | null = null;
  let defaultWmInputIndex: number | null = null;

  const needsLogo = settings.mode === "logo" && Boolean(settings.logoPath);
  const defaultPath = getWatermarkPath();
  const useDefaultPng =
    settings.mode === "default" && (await fileExists(defaultPath));

  if (needsLogo && settings.logoPath) {
    if (!(await fileExists(settings.logoPath))) {
      throw new Error("Watermark logo file not found");
    }
    await assertPngFile(settings.logoPath);
    args.push("-i", settings.logoPath);
    logoInputIndex = nextInput++;
  } else if (useDefaultPng) {
    args.push("-i", defaultPath);
    defaultWmInputIndex = nextInput++;
  }

  const applyWm =
    settings.mode === "logo" && logoInputIndex !== null
      ? ("logo" as const)
      : settings.mode === "text" && settings.text?.trim()
        ? ("text" as const)
        : settings.mode === "default" && defaultWmInputIndex !== null
          ? ("default-png" as const)
          : settings.mode === "default"
            ? ("default-text" as const)
            : ("none" as const);

  // Fast path: classic letterbox, no watermark
  if (!layout && applyWm === "none") {
    args.push("-vf", classicScaleFilter(res.width, res.height));
    args.push(
      "-c:v",
      "libx264",
      "-tune",
      "stillimage",
      "-c:a",
      "copy",
      "-shortest",
      "-pix_fmt",
      "yuv420p",
      options.outputPath,
    );
    await runFfmpeg(args);
    return;
  }

  const outDirect = applyWm === "none";
  const baseLabel = outDirect ? "vout" : "base";
  const filterParts: string[] = [];

  if (layout) {
    const titleEscaped = sanitizeDrawtext(options.songTitle?.trim() || "Untitled");
    const artistRaw = options.artist?.trim();
    const artistEscaped = artistRaw ? sanitizeDrawtext(artistRaw) : null;
    filterParts.push(
      artTrackFilterEndingAt(
        {
          width: res.width,
          height: res.height,
          layout,
          titleEscaped,
          artistEscaped,
          fontfileEscaped: fontfile,
        },
        baseLabel,
      ),
    );
  } else {
    filterParts.push(`[0:v]${classicScaleFilter(res.width, res.height)}[${baseLabel}]`);
  }

  if (applyWm === "logo" && logoInputIndex !== null) {
    const { x, y } = overlayXy(settings.position, settings.offsetX, settings.offsetY);
    filterParts.push(
      `[${logoInputIndex}:v]scale=${watermarkWidth}:-1[wm]`,
      `[${baseLabel}][wm]overlay=${x}:${y}[vout]`,
    );
  } else if (applyWm === "text") {
    const draw = buildDrawtextFilter({
      text: settings.text!.trim(),
      fontSize,
      fontColor: "white@0.9",
      position: settings.position,
      offsetX: settings.offsetX,
      offsetY: settings.offsetY,
      fontfileEscaped: fontfile,
    });
    filterParts.push(`[${baseLabel}]${draw}[vout]`);
  } else if (applyWm === "default-png" && defaultWmInputIndex !== null) {
    filterParts.push(
      ...buildBrandWatermarkOverlayFilters({
        baseLabel,
        watermarkInputIndex: defaultWmInputIndex,
        watermarkWidthPx: watermarkWidth,
        offsetX: settings.offsetX,
        offsetY: settings.offsetY,
      }),
    );
  } else if (applyWm === "default-text") {
    const draw = buildDrawtextFilter({
      text: getVideoAttributionText(),
      fontSize,
      fontColor: "white@0.85",
      position: settings.position,
      offsetX: settings.offsetX,
      offsetY: settings.offsetY,
      fontfileEscaped: null,
    });
    filterParts.push(`[${baseLabel}]${draw}[vout]`);
  }

  args.push("-filter_complex", filterParts.join(";"), "-map", "[vout]", "-map", "1:a");
  args.push(
    "-c:v",
    "libx264",
    "-tune",
    "stillimage",
    "-c:a",
    "copy",
    "-shortest",
    "-pix_fmt",
    "yuv420p",
    options.outputPath,
  );

  await runFfmpeg(args);
}

function runFfmpeg(args: string[]): Promise<void> {
  return new Promise((resolve, reject) => {
    const proc = spawn(getFfmpegPath(), args, { stdio: ["ignore", "pipe", "pipe"] });
    let stderr = "";
    proc.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
      if (stderr.length > 64_000) stderr = stderr.slice(-32_000);
    });
    proc.on("close", (code) => {
      if (code === 0) resolve();
      else reject(new Error(`FFmpeg failed (code ${code}): ${stderr.slice(-500)}`));
    });
    proc.on("error", (err) => {
      reject(new Error(`FFmpeg not found or failed to start: ${err.message}`));
    });
  });
}

export async function cleanupFiles(paths: string[]) {
  for (const p of paths) {
    try {
      await fs.unlink(p);
    } catch {
      // ignore missing files
    }
  }
}
