/**
 * 静态资源压缩 · 把 public/portraits & public/videos 转成桶友好的体积
 *
 * 输入: <repoRoot>/public/portraits/*.png, public/videos/{hero-pv.mp4, artists/*.mp4}
 * 输出: <repoRoot>/../assets-compressed/{portraits,videos}/...
 *
 * 处理:
 *  - 立绘 / 氛围图 (.png) → .webp, 最大宽 1600, quality 82, 目标 ≤ 800KB
 *  - 三视图 (-view.png)   → .webp, 最大宽 2400, quality 82, 目标 ≤ 1.5MB
 *  - 视频 (.mp4)          → libx264 CRF 28, 最大宽 1920, AAC 96k, faststart
 *
 * 已存在且 mtime 新于源文件的输出会跳过 (可反复运行恢复中断的批处理)
 */
import sharp from "sharp";
import pLimit from "p-limit";
import { spawn } from "node:child_process";
import { mkdir, readdir, stat } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join, resolve, basename } from "node:path";
import { fileURLToPath } from "node:url";
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..", "..");
const OUT_ROOT = resolve(REPO_ROOT, "..", "assets-compressed");
const FFMPEG_BIN = ffmpegInstaller.path;

const PORTRAITS_SRC = join(REPO_ROOT, "public", "portraits");
const VIDEOS_SRC = join(REPO_ROOT, "public", "videos");
const PORTRAITS_OUT = join(OUT_ROOT, "portraits");
const VIDEOS_OUT = join(OUT_ROOT, "videos");

const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a);

async function fileSize(p) {
  try {
    return (await stat(p)).size;
  } catch {
    return 0;
  }
}

function fmt(n) {
  if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`;
  if (n >= 1024) return `${(n / 1024).toFixed(0)}KB`;
  return `${n}B`;
}

async function isStale(src, out) {
  if (!existsSync(out)) return true;
  const [s, o] = await Promise.all([stat(src), stat(out)]);
  return o.mtimeMs < s.mtimeMs;
}

async function compressImage(src, out, maxWidth, quality) {
  await mkdir(dirname(out), { recursive: true });
  await sharp(src)
    .rotate()
    .resize({ width: maxWidth, withoutEnlargement: true })
    .webp({ quality, effort: 6 })
    .toFile(out);
}

function runFfmpeg(args) {
  return new Promise((resolveP, rejectP) => {
    const p = spawn(FFMPEG_BIN, args, { stdio: ["ignore", "ignore", "pipe"] });
    let err = "";
    p.stderr.on("data", (c) => (err += c.toString()));
    p.on("close", (code) => {
      if (code === 0) resolveP();
      else rejectP(new Error(`ffmpeg exit ${code}: ${err.slice(-500)}`));
    });
  });
}

async function compressVideo(src, out, { crf = 28, maxWidth = 1920 } = {}) {
  await mkdir(dirname(out), { recursive: true });
  const args = [
    "-y",
    "-loglevel", "error",
    "-i", src,
    "-c:v", "libx264",
    "-preset", "medium",
    "-crf", String(crf),
    "-vf", `scale='min(${maxWidth},iw)':-2`,
    "-c:a", "aac",
    "-b:a", "96k",
    "-movflags", "+faststart",
    "-pix_fmt", "yuv420p",
    out,
  ];
  await runFfmpeg(args);
}

async function processPortraits() {
  if (!existsSync(PORTRAITS_SRC)) {
    log("⚠️  portraits 源目录不存在, 跳过:", PORTRAITS_SRC);
    return { count: 0, saved: 0 };
  }
  const files = (await readdir(PORTRAITS_SRC)).filter((f) =>
    /\.png$/i.test(f),
  );
  log(`portraits: 找到 ${files.length} 张待处理`);

  const limit = pLimit(4); // 4 并发即可,sharp 本身有线程
  let done = 0;
  let totalSrc = 0;
  let totalOut = 0;
  let skipped = 0;

  await Promise.all(
    files.map((name) =>
      limit(async () => {
        const src = join(PORTRAITS_SRC, name);
        const isView = name.endsWith("-view.png");
        const outName = name.replace(/\.png$/i, ".webp");
        const out = join(PORTRAITS_OUT, outName);

        if (!(await isStale(src, out))) {
          skipped++;
          done++;
          return;
        }

        try {
          await compressImage(
            src,
            out,
            isView ? 2400 : 1600,
            82,
          );
          const sSize = await fileSize(src);
          const oSize = await fileSize(out);
          totalSrc += sSize;
          totalOut += oSize;
          done++;
          log(
            `[${done}/${files.length}] ${name} → ${outName}  ${fmt(sSize)} → ${fmt(oSize)}  (-${Math.round((1 - oSize / sSize) * 100)}%)`,
          );
        } catch (e) {
          log(`❌ 失败 ${name}: ${e.message}`);
        }
      }),
    ),
  );

  log(
    `portraits 完成: ${done}/${files.length} (跳过 ${skipped}), 总体积 ${fmt(totalSrc)} → ${fmt(totalOut)}`,
  );
  return { count: done, savedRatio: totalSrc ? 1 - totalOut / totalSrc : 0 };
}

async function processVideos() {
  const tasks = [];

  // hero pv
  const heroSrc = join(VIDEOS_SRC, "hero-pv.mp4");
  if (existsSync(heroSrc)) {
    tasks.push({
      src: heroSrc,
      out: join(VIDEOS_OUT, "hero-pv.mp4"),
      label: "hero-pv.mp4",
    });
  }

  // artist solos
  const artistsDir = join(VIDEOS_SRC, "artists");
  if (existsSync(artistsDir)) {
    const files = (await readdir(artistsDir)).filter((f) => /\.mp4$/i.test(f));
    for (const name of files) {
      tasks.push({
        src: join(artistsDir, name),
        out: join(VIDEOS_OUT, "artists", name),
        label: `artists/${name}`,
      });
    }
  }

  log(`videos: 找到 ${tasks.length} 个 mp4 待处理`);

  // 视频用 libx264 编码很吃 CPU, 串行处理避免互相争抢
  let done = 0;
  let totalSrc = 0;
  let totalOut = 0;
  let skipped = 0;
  for (const t of tasks) {
    if (!(await isStale(t.src, t.out))) {
      skipped++;
      done++;
      continue;
    }
    try {
      const start = Date.now();
      await compressVideo(t.src, t.out, { crf: 28, maxWidth: 1920 });
      const sSize = await fileSize(t.src);
      const oSize = await fileSize(t.out);
      totalSrc += sSize;
      totalOut += oSize;
      done++;
      log(
        `[${done}/${tasks.length}] ${t.label}  ${fmt(sSize)} → ${fmt(oSize)}  (-${Math.round((1 - oSize / sSize) * 100)}%, ${((Date.now() - start) / 1000).toFixed(1)}s)`,
      );
    } catch (e) {
      log(`❌ 失败 ${t.label}: ${e.message}`);
    }
  }

  log(
    `videos 完成: ${done}/${tasks.length} (跳过 ${skipped}), 总体积 ${fmt(totalSrc)} → ${fmt(totalOut)}`,
  );
}

async function main() {
  log("ffmpeg:", FFMPEG_BIN);
  log("输出目录:", OUT_ROOT);
  await mkdir(OUT_ROOT, { recursive: true });

  const t0 = Date.now();
  await processPortraits();
  await processVideos();

  log(`全部完成, 用时 ${((Date.now() - t0) / 1000).toFixed(1)}s`);
  log(`下一步: cd ${OUT_ROOT}/.. && tar -cvf cyber-star-assets.tar assets-compressed`);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
