const fs = require("fs");
const path = require("path");
const sharp = require("sharp");

const { buildPages } = require("./build");
const { buildThumbnails } = require("./generate-thumbnails");
const { eloPath } = require("./lib/elo");
const {
  getNextMealId,
  loadMeals,
  mealsPath,
  repoRoot,
  saveMeals,
} = require("./lib/meals");

const fullsDir = path.join(repoRoot, "images", "fulls");
const thumbsDir = path.join(repoRoot, "images", "thumbs");
const indexPath = path.join(repoRoot, "index.html");
const rankingsPath = path.join(repoRoot, "rankings.html");
const manifestPath = path.join(thumbsDir, ".thumbs-manifest.json");

const FULL_IMAGE_QUALITY = 90;

function printHelp() {
  console.log(`Usage:
  npm run ingest -- --image <path> --title <title> --description <text> [options]

Required:
  --image <path>         Source image to ingest
  --title <title>        Meal title
  --description <text>   Meal description

Optional:
  --position <value>     Viewer background position, e.g. "left center"
  --focus-x <0..1>       Thumbnail crop focal point x coordinate
  --focus-y <0..1>       Thumbnail crop focal point y coordinate
  --help                 Show this help message
`);
}

function parseArgs(argv) {
  const options = {};

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];

    if (arg === "--help" || arg === "-h") {
      options.help = true;
      continue;
    }

    if (!arg.startsWith("--")) {
      throw new Error(`Unexpected argument "${arg}"`);
    }

    const key = arg.slice(2);
    const value = argv[index + 1];

    if (value === undefined || value.startsWith("--")) {
      throw new Error(`Missing value for "${arg}"`);
    }

    options[key] = value;
    index += 1;
  }

  return options;
}

function parseFocusValue(value, axis) {
  const parsed = Number.parseFloat(value);

  if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
    throw new Error(`--focus-${axis} must be a number between 0 and 1`);
  }

  return parsed;
}

function buildMealFromOptions(id, options) {
  const meal = {
    id,
    title: options.title,
    description: options.description,
  };

  if (options.position) {
    meal.position = options.position;
  }

  if (options["focus-x"] !== undefined || options["focus-y"] !== undefined) {
    if (options["focus-x"] === undefined || options["focus-y"] === undefined) {
      throw new Error("Both --focus-x and --focus-y must be provided together");
    }

    meal.thumbnail = {
      focus: {
        x: parseFocusValue(options["focus-x"], "x"),
        y: parseFocusValue(options["focus-y"], "y"),
      },
    };
  }

  return meal;
}

function getResolvedSourcePath(imageArg) {
  const resolvedPath = path.resolve(process.cwd(), imageArg);

  if (!fs.existsSync(resolvedPath)) {
    throw new Error(`Source image not found: ${resolvedPath}`);
  }

  if (!fs.statSync(resolvedPath).isFile()) {
    throw new Error(`Source image is not a file: ${resolvedPath}`);
  }

  return resolvedPath;
}

async function writeFullImage(sourcePath, destinationPath) {
  await sharp(sourcePath)
    .rotate()
    .jpeg({ quality: FULL_IMAGE_QUALITY, mozjpeg: true })
    .toFile(destinationPath);
}

function readOptionalFile(filePath) {
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
}

async function restoreOptionalFile(filePath, previousContents) {
  if (previousContents === null) {
    if (fs.existsSync(filePath)) {
      await fs.promises.unlink(filePath);
    }

    return;
  }

  if (previousContents !== undefined) {
    fs.writeFileSync(filePath, previousContents);
  }
}

async function rollback({
  createdFullPath,
  createdThumbPath,
  previousElo,
  previousIndex,
  previousManifest,
  previousMeals,
  previousRankings,
}) {
  if (previousMeals !== undefined) {
    fs.writeFileSync(mealsPath, previousMeals);
  }

  await restoreOptionalFile(eloPath, previousElo);

  if (previousIndex !== undefined) {
    fs.writeFileSync(indexPath, previousIndex);
  }

  await restoreOptionalFile(rankingsPath, previousRankings);
  await restoreOptionalFile(manifestPath, previousManifest);

  if (createdThumbPath && fs.existsSync(createdThumbPath)) {
    await fs.promises.unlink(createdThumbPath);
  }

  if (createdFullPath && fs.existsSync(createdFullPath)) {
    await fs.promises.unlink(createdFullPath);
  }
}

async function ingestMeal(options) {
  const sourcePath = getResolvedSourcePath(options.image);
  const meals = loadMeals();
  const nextId = getNextMealId(meals);
  const meal = buildMealFromOptions(nextId, options);

  const fullPath = path.join(fullsDir, `${nextId}.jpg`);
  const thumbPath = path.join(thumbsDir, `${nextId}.jpg`);

  if (fs.existsSync(fullPath) || fs.existsSync(thumbPath)) {
    throw new Error(`Meal id ${nextId} already has generated image files`);
  }

  const previousMeals = fs.readFileSync(mealsPath, "utf8");
  const previousElo = readOptionalFile(eloPath);
  const previousIndex = fs.readFileSync(indexPath, "utf8");
  const previousRankings = readOptionalFile(rankingsPath);
  const previousManifest = readOptionalFile(manifestPath);

  await fs.promises.mkdir(fullsDir, { recursive: true });

  try {
    await writeFullImage(sourcePath, fullPath);
    saveMeals([...meals, meal]);
    await buildThumbnails();
    buildPages();
  } catch (error) {
    await rollback({
      createdFullPath: fullPath,
      createdThumbPath: thumbPath,
      previousElo,
      previousIndex,
      previousManifest,
      previousMeals,
      previousRankings,
    });
    throw error;
  }

  return meal;
}

async function main() {
  const options = parseArgs(process.argv.slice(2));

  if (options.help) {
    printHelp();
    return;
  }

  for (const field of ["image", "title", "description"]) {
    if (!options[field]) {
      throw new Error(`Missing required option "--${field}"`);
    }
  }

  const meal = await ingestMeal(options);
  console.log(`Ingested meal ${meal.id}: ${meal.title}`);
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
  });
}

module.exports = {
  ingestMeal,
};
