#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  const args = parseArgs(process.argv.slice(2));
  const actual = JSON.parse(await readFile(args.actualPlan, "utf8"));
  const expected = decodeReviewedPlan(args.reviewedPlan);
  const result = compareReviewedPlan(expected, actual);
  process.stdout.write(`${JSON.stringify(result)}\n`);
  if (!result.ok) process.exit(42);
}

export function decodeReviewedPlan(value) {
  const prefix = "unidesk-release-plan-v1.";
  if (!String(value).startsWith(prefix)) throw new Error("reviewed plan envelope is missing or unsupported");
  const decoded = Buffer.from(String(value).slice(prefix.length), "base64url").toString("utf8");
  const plan = JSON.parse(decoded);
  if (plan?.version !== "v1") throw new Error("reviewed plan version is unsupported");
  return plan;
}

export function compareReviewedPlan(expected, actual) {
  const actualScope = scope(actual);
  const expectedScope = scope(expected);
  const expectedIdentity = text(expected?.planIdentity);
  const actualIdentity = text(actual?.planIdentity?.value);
  const expectedMetadata = {
    target: text(expected?.target),
    consumer: text(expected?.consumer),
    sourceCommit: text(expected?.sourceCommit),
    baseCommit: text(expected?.baseCommit)
  };
  const actualMetadata = {
    target: text(actual?.planIdentity?.inputs?.target),
    consumer: text(actual?.planIdentity?.inputs?.consumer),
    sourceCommit: text(actual?.sourceCommitId),
    baseCommit: text(actual?.baseCommitId)
  };
  const additions = {
    buildServices: difference(actualScope.buildServices, expectedScope.buildServices),
    rolloutServices: difference(actualScope.rolloutServices, expectedScope.rolloutServices),
    affectedServices: difference(actualScope.affectedServices, expectedScope.affectedServices)
  };
  const identityMatches = Boolean(expectedIdentity) && expectedIdentity === actualIdentity;
  const metadataMatches = Object.keys(expectedMetadata).every((key) => expectedMetadata[key] && expectedMetadata[key] === actualMetadata[key]);
  const scopeExpanded = Object.values(additions).some((items) => items.length > 0);
  return {
    event: "reviewed-plan-admission",
    ok: identityMatches && metadataMatches && !scopeExpanded,
    identityMatches,
    metadataMatches,
    scopeExpanded,
    expectedIdentity,
    actualIdentity,
    expectedMetadata,
    actualMetadata,
    expectedScope,
    actualScope,
    additions,
    valuesPrinted: false
  };
}

function scope(value) {
  return {
    buildServices: sortedStrings(value?.buildServices ?? value?.scope?.buildServices),
    rolloutServices: sortedStrings(value?.rolloutServices ?? value?.scope?.rolloutServices),
    affectedServices: sortedStrings(value?.affectedServices ?? value?.scope?.affectedServices)
  };
}

function difference(actual, expected) {
  const expectedSet = new Set(expected);
  return actual.filter((item) => !expectedSet.has(item));
}

function sortedStrings(value) {
  return [...new Set(Array.isArray(value) ? value.map(text).filter(Boolean) : [])].sort();
}

function text(value) {
  return typeof value === "string" ? value.trim() : "";
}

function parseArgs(argv) {
  const result = { actualPlan: "", reviewedPlan: "" };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--actual-plan") result.actualPlan = readOption(argv, ++index, arg);
    else if (arg === "--reviewed-plan") result.reviewedPlan = readOption(argv, ++index, arg);
    else throw new Error(`unknown argument ${arg}`);
  }
  if (!result.actualPlan) throw new Error("--actual-plan is required");
  if (!result.reviewedPlan) throw new Error("--reviewed-plan is required");
  return result;
}

function readOption(argv, index, name) {
  const value = argv[index];
  if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}
