#!/usr/bin/env node

import { execFile, execFileSync } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);
import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const require = createRequire(import.meta.url);
const __dirname = dirname(fileURLToPath(import.meta.url));
const remotionRoot = resolve(__dirname, '..');
const outputRoot = resolve(remotionRoot, '..', 'app', 'public', 'generated', 'remotion');
const tempRoot = resolve(remotionRoot, 'out', 'runtime-assets');
const remotionBinary = join(remotionRoot, 'node_modules', '.bin', 'remotion');
const outputFps = 30;
const webpPackageRoot = dirname(require.resolve('webp-converter/package.json'));

function resolveWebpBinary(name) {
  switch (os.platform()) {
    case 'darwin':
      return join(webpPackageRoot, 'bin', 'libwebp_osx', 'bin', name);
    case 'linux':
      return join(webpPackageRoot, 'bin', 'libwebp_linux', 'bin', name);
    case 'win32':
      return join(webpPackageRoot, 'bin', 'libwebp_win64', 'bin', `${name}.exe`);
    default:
      throw new Error(`[remotion-runtime-assets] unsupported platform for vendored webp tools: ${os.platform()}`);
  }
}

const cwebpBinary = resolveWebpBinary('cwebp');
const webpmuxBinary = resolveWebpBinary('webpmux');

const ALL_COLORS = ['yellow', 'burgundy', 'black', 'navy', 'green'];

function resolveColorSet() {
  const raw = (process.env.MASCOT_COLORS ?? '').trim();
  if (!raw) return ['yellow'];
  if (raw.toLowerCase() === 'all') return ALL_COLORS;
  const requested = raw
    .split(',')
    .map(s => s.trim())
    .filter(Boolean);
  const unknown = requested.filter(c => !ALL_COLORS.includes(c));
  if (unknown.length > 0) {
    throw new Error(
      `[remotion-runtime-assets] unknown mascot color(s): ${unknown.join(', ')}. Allowed: ${ALL_COLORS.join(', ')}, or "all".`
    );
  }
  if (!requested.includes('yellow')) {
    requested.unshift('yellow');
  }
  return requested;
}

const colors = resolveColorSet();
console.log(`[remotion-runtime-assets] rendering colors: ${colors.join(', ')}`);
const baseVariants = [
  { composition: 'mascot-yellow-idle', profile: 'default', props: {} },
  { composition: 'mascot-yellow-talking', profile: 'default', props: {} },
  { composition: 'mascot-yellow-thinking', profile: 'default', props: {} },
  {
    composition: 'mascot-yellow-idle',
    profile: 'compact',
    props: { groundShadowOpacity: 0.75, compactArmShading: true },
  },
  {
    composition: 'mascot-yellow-talking',
    profile: 'compact',
    props: { groundShadowOpacity: 0.75, compactArmShading: true },
  },
  {
    composition: 'mascot-yellow-thinking',
    profile: 'compact',
    props: { groundShadowOpacity: 0.75, compactArmShading: true },
  },
];
const variants = baseVariants.flatMap(variant =>
  colors.map(color => ({
    ...variant,
    color,
    props: { ...variant.props, mascotColor: color },
  }))
);

function run(command, args, cwd) {
  try {
    execFileSync(command, args, {
      cwd,
      stdio: 'inherit',
      env: process.env,
    });
  } catch (error) {
    if (error?.code === 'ENOENT') {
      throw new Error(
        `[remotion-runtime-assets] missing required executable "${command}". Install it and ensure it is on PATH.`
      );
    }
    throw error;
  }
}

function ensureCleanDir(dir) {
  rmSync(dir, { recursive: true, force: true });
  mkdirSync(dir, { recursive: true });
}

function ensureExecutable(path) {
  if (os.platform() !== 'win32') {
    chmodSync(path, 0o755);
  }
}

function renderMov(composition, destination, props) {
  if (!existsSync(remotionBinary)) {
    throw new Error(`remotion CLI missing at ${remotionBinary}; run pnpm install in remotion/ first`);
  }

  const args = [
    'render',
    composition,
    destination,
    '--codec=prores',
    '--prores-profile=4444',
    '--pixel-format=yuva444p10le',
  ];
  if (Object.keys(props).length > 0) {
    args.push('--props', JSON.stringify(props));
  }
  run(remotionBinary, args, remotionRoot);
}

function extractPngFrames(inputMov, frameDir) {
  mkdirSync(frameDir, { recursive: true });
  run(
    'ffmpeg',
    [
      '-y',
      '-i',
      inputMov,
      '-an',
      '-vsync',
      'passthrough',
      '-pix_fmt',
      'rgba',
      '-start_number',
      '0',
      join(frameDir, 'frame-%04d.png'),
    ],
    remotionRoot
  );
}

function listFrames(frameDir, extension) {
  return readdirSync(frameDir)
    .filter(entry => entry.endsWith(extension))
    .sort()
    .map(entry => join(frameDir, entry));
}

async function convertPngFramesToWebp(frameDir) {
  const pngFrames = listFrames(frameDir, '.png');
  const webpFrames = new Array(pngFrames.length);
  const concurrency = Math.max(1, Math.min(os.cpus()?.length ?? 4, 8));
  let nextIndex = 0;
  let completed = 0;
  const total = pngFrames.length;

  async function worker() {
    while (true) {
      const idx = nextIndex++;
      if (idx >= total) return;
      const pngFrame = pngFrames[idx];
      const webpFrame = pngFrame.replace(/\.png$/u, '.webp');
      try {
        await execFileAsync(
          cwebpBinary,
          ['-quiet', '-q', '82', '-m', '4', '-alpha_q', '100', pngFrame, '-o', webpFrame],
          { cwd: remotionRoot }
        );
      } catch (error) {
        if (error?.code === 'ENOENT') {
          throw new Error(
            `[remotion-runtime-assets] missing required executable "${cwebpBinary}". Install it and ensure it is on PATH.`
          );
        }
        throw error;
      }
      webpFrames[idx] = webpFrame;
      completed += 1;
      if (completed === total || completed % 30 === 0) {
        console.log(`[remotion-runtime-assets]   cwebp ${completed}/${total} frames`);
      }
    }
  }

  await Promise.all(Array.from({ length: concurrency }, () => worker()));
  return webpFrames;
}

async function transcodeAnimatedWebp(inputMov, outputWebp, frameDir) {
  extractPngFrames(inputMov, frameDir);
  const webpFrames = await convertPngFramesToWebp(frameDir);
  const frameDurationMs = String(Math.round(1000 / outputFps));
  const args = ['-loop', '0', '-bgcolor', '0,0,0,0'];

  // webpmux frame options: +duration+xoff+yoff+dispose+blend
  //   dispose=1 → clear canvas to background (transparent) before drawing the
  //     next frame. Without this, frames composite over previous ones and
  //     transparent mascot poses ghost on top of each other.
  //   -b → no blending; the frame's RGBA replaces the canvas pixels. With
  //     blending the alpha of the prior frame leaks through even after a
  //     dispose, producing a faint overlay around the silhouette.
  for (const framePath of webpFrames) {
    args.push('-frame', framePath, `+${frameDurationMs}+0+0+1-b`);
  }

  args.push('-o', outputWebp);
  run(webpmuxBinary, args, remotionRoot);
}

ensureCleanDir(outputRoot);
ensureCleanDir(tempRoot);
ensureExecutable(cwebpBinary);
ensureExecutable(webpmuxBinary);

const manifest = {
  generatedAt: new Date().toISOString(),
  format: 'image/webp',
  variants: [],
};

for (const variant of variants) {
  const profileDir = join(outputRoot, variant.profile, variant.color);
  const tempVariantDir = join(tempRoot, variant.profile, variant.color, variant.composition);
  mkdirSync(profileDir, { recursive: true });
  mkdirSync(tempVariantDir, { recursive: true });

  const movPath = join(tempVariantDir, `${variant.composition}.mov`);
  const webpPath = join(profileDir, `${variant.composition}.webp`);
  const frameDir = join(tempVariantDir, 'frames');

  console.log(
    `[remotion-runtime-assets] rendering ${variant.profile}/${variant.color}/${variant.composition}`
  );
  renderMov(variant.composition, movPath, variant.props);
  await transcodeAnimatedWebp(movPath, webpPath, frameDir);

  manifest.variants.push({
    color: variant.color,
    composition: variant.composition,
    profile: variant.profile,
    path: `${variant.profile}/${variant.color}/${variant.composition}.webp`,
    props: variant.props,
  });
}

writeFileSync(join(outputRoot, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
rmSync(tempRoot, { recursive: true, force: true });

console.log(`[remotion-runtime-assets] wrote assets to ${outputRoot}`);
