#!/usr/bin/env node

/**
 * Generates a unified GitHub release body for a trigger.dev version release.
 *
 * Usage:
 *   node scripts/generate-github-release.mjs <version>
 *
 * Reads:
 *   - The enhanced changeset release PR body (via RELEASE_PR_BODY env var or stdin).
 *     By the time this runs, the PR body has already been enhanced by enhance-release-pr.mjs
 *     to include server changes, deduplication, and categorization. The .server-changes/ files
 *     themselves are already deleted (consumed on the release branch, same as .changeset/ files).
 *   - Git log for contributor info
 *
 * Outputs the formatted GitHub release body to stdout.
 */

import { execSync } from "child_process";
import { readdirSync, readFileSync } from "fs";
import { join } from "path";

const version = process.argv[2];
if (!version) {
  console.error("Usage: node scripts/generate-github-release.mjs <version>");
  process.exit(1);
}

const ROOT_DIR = join(import.meta.dirname, "..");

// --- Parse the enhanced PR body ---
// The PR body from enhance-release-pr.mjs has sections like:
//   ## Highlights
//   ## Improvements
//   ## Bug fixes
//   ## Server changes
//   ## Breaking changes
//   <details>...</details>
// We extract the content between the first heading and the <details> block.

function extractChangesFromPrBody(body) {
  if (!body) return "";

  const lines = body.split("\n");
  const outputLines = [];
  let inDetails = false;
  let inSummary = false;
  let foundContent = false;

  for (const line of lines) {
    // Skip the title line (# trigger.dev vX.Y.Z)
    if (line.startsWith("# trigger.dev v")) continue;

    // Skip the entire Summary section (heading + content until next heading)
    if (line.startsWith("## Summary")) {
      inSummary = true;
      continue;
    }
    if (inSummary) {
      if (line.startsWith("## ")) {
        inSummary = false;
      } else {
        continue;
      }
    }

    // Stop before raw changeset output
    if (line.trim() === "<details>") {
      inDetails = true;
      continue;
    }
    if (inDetails) continue;

    // Collect everything from the first ## heading onward
    if (line.startsWith("## ") && !foundContent) {
      foundContent = true;
    }

    if (foundContent) {
      outputLines.push(line);
    }
  }

  return outputLines.join("\n").trim();
}

// --- Get contributors from git log ---

function getContributors(previousVersion) {
  try {
    const range = previousVersion ? `v${previousVersion}...HEAD` : "HEAD~50..HEAD";
    const log = execSync(`git log ${range} --format="%aN|%aE" --no-merges`, {
      cwd: ROOT_DIR,
      encoding: "utf-8",
    });

    const contributors = new Map();
    for (const line of log.split("\n").filter(Boolean)) {
      const [name, email] = line.split("|");
      if (!name || email?.endsWith("@users.noreply.github.com")) {
        // Try to extract username from noreply email
        const match = email?.match(/(\d+\+)?(.+)@users\.noreply\.github\.com/);
        if (match) {
          const username = match[2];
          contributors.set(username, (contributors.get(username) || 0) + 1);
        }
        continue;
      }
      contributors.set(name, (contributors.get(name) || 0) + 1);
    }

    return [...contributors.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name);
  } catch {
    return [];
  }
}

// --- Get published packages ---

function getPublishedPackages() {
  try {
    const packagesDir = join(ROOT_DIR, "packages");
    const names = [];
    for (const dir of readdirSync(packagesDir, { withFileTypes: true })) {
      if (!dir.isDirectory()) continue;
      try {
        const pkg = JSON.parse(readFileSync(join(packagesDir, dir.name, "package.json"), "utf-8"));
        if (pkg.name && !pkg.private) {
          names.push(pkg.name);
        }
      } catch {
        // skip directories without package.json
      }
    }
    return names.sort();
  } catch {
    return [
      "@trigger.dev/build",
      "@trigger.dev/core",
      "@trigger.dev/react-hooks",
      "@trigger.dev/sdk",
      "trigger.dev",
    ];
  }
}

function getPreviousVersion(version) {
  const parts = version.split(".").map(Number);
  if (parts[2] > 0) {
    parts[2]--;
  } else if (parts[1] > 0) {
    parts[1]--;
    parts[2] = 0;
  } else if (parts[0] > 0) {
    parts[0]--;
    parts[1] = 0;
    parts[2] = 0;
  } else {
    return null;
  }
  return parts.join(".");
}

// --- Format the release body ---

function formatRelease({ version, changesContent, contributors, packages }) {
  const lines = [];

  lines.push(`# trigger.dev v${version}`);
  lines.push("");
  lines.push("## Upgrade");
  lines.push("");
  lines.push("```sh");
  lines.push("npx trigger.dev@latest update  # npm");
  lines.push("pnpm dlx trigger.dev@latest update  # pnpm");
  lines.push("yarn dlx trigger.dev@latest update  # yarn");
  lines.push("bunx trigger.dev@latest update  # bun");
  lines.push("```");
  lines.push("");
  // The Docker image link initially points to the container page without a tag filter.
  // After Docker images are built, the update-release job patches this with the exact tag URL.
  lines.push(
    `Self-hosted Docker image: [\`ghcr.io/triggerdotdev/trigger.dev:v${version}\`](https://github.com/triggerdotdev/trigger.dev/pkgs/container/trigger.dev)`
  );
  lines.push("");
  lines.push("## Release notes");
  lines.push("");
  lines.push(
    `Read the full release notes: https://trigger.dev/changelog/v${version.replace(/\./g, "-")}`
  );
  lines.push("");

  // What's changed — extracted from the enhanced PR body
  if (changesContent) {
    lines.push("## What's changed");
    lines.push("");
    lines.push(changesContent);
    lines.push("");
  }

  // Packages
  if (packages.length > 0) {
    lines.push(`## All packages: v${version}`);
    lines.push("");
    lines.push(packages.join(", "));
    lines.push("");
  }

  // Contributors
  if (contributors.length > 0) {
    lines.push("## Contributors");
    lines.push("");
    lines.push(
      contributors.map((c) => (/^[A-Za-z0-9][-A-Za-z0-9]*$/.test(c) ? `@${c}` : c)).join(", ")
    );
    lines.push("");
  }

  // Comparison link
  const prevVersion = getPreviousVersion(version);
  if (prevVersion) {
    lines.push(
      `**Full changelog**: https://github.com/triggerdotdev/trigger.dev/compare/v${prevVersion}...v${version}`
    );
  }

  return lines.join("\n");
}

// --- Main ---

async function main() {
  // Read PR body from env or stdin
  let prBody = process.env.RELEASE_PR_BODY || "";
  if (!prBody && !process.stdin.isTTY) {
    const chunks = [];
    for await (const chunk of process.stdin) chunks.push(chunk);
    prBody = Buffer.concat(chunks).toString("utf-8");
  }

  const changesContent = extractChangesFromPrBody(prBody);

  // Fail loudly when a stable (non-prerelease) release resolved no changelog. This
  // guards against publishing an empty "What's changed" section, which previously
  // happened silently on workflow_dispatch re-runs where RELEASE_PR_BODY was empty.
  // Prereleases (any version with a hyphen, e.g. 4.5.0-rc.0) are allowed to be empty.
  const isPrerelease = version.includes("-");
  if (!changesContent && !isPrerelease) {
    console.error(
      `Error: no "What's changed" content could be resolved for v${version}. ` +
        `RELEASE_PR_BODY was empty or contained no changelog entries. ` +
        `Refusing to publish a release with an empty changelog.`
    );
    process.exit(1);
  }

  const contributors = getContributors(getPreviousVersion(version));
  const packages = getPublishedPackages();

  const body = formatRelease({
    version,
    changesContent,
    contributors,
    packages,
  });

  process.stdout.write(body);
}

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