const path = require("path");
const { attestProvenance } = require("@actions/attest");
const { execSync } = require("child_process");
const fs = require("fs");
const { Octokit } = require("@octokit/rest");
const mime = require("mime-types");

// This script is used to publish the artifacts generated by the changeset workflow.
// It is triggered by the changeset action and is responsible for creating releases to the repository.
// We do this manually because we want specific/scoped assets to be uploaded to a corresponding SCOPED releases.
// e.g. nsis artifacts should be uploaded to the nsis release, not in a zstd release.
// JSON format: https://github.com/changesets/action?tab=readme-ov-file#outputs
const publishedPackages = process.env.PUBLISHED_PACKAGES;
if (!publishedPackages) {
  console.error("PUBLISHED_PACKAGES environment variable is not set. See script for documentation.");
  process.exit(1);
}

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

function getTargetCommitish() {
  const commit = execSync("git rev-parse HEAD").toString().trim();
  if (commit.indexOf("fatal") === -1) {
    return commit;
  }
  return "master";
}

async function uploadAssetStream({ owner, repo, release_id, asset }) {
  const { size, stream, contentType, artifactName } = asset;

  console.log(`Uploading ${artifactName} (${size} bytes)...`);

  const res = await octokit.repos.uploadReleaseAsset({
    owner,
    repo,
    release_id,
    name: artifactName,
    data: stream,
    headers: {
      "content-type": contentType,
      "content-length": size,
    },
  });

  console.log(`Uploaded: ${res.data.browser_download_url}`);
}

async function atomicReleaseWithStreams({ owner, repo, tag, name, body, assets, targetCommit }) {
  //
  // 1. Create release as draft
  //
  const { data: release } = await octokit.repos.createRelease({
    owner,
    repo,
    tag_name: tag,
    target_commitish: targetCommit,
    name,
    body,
    draft: true,
  });

  //
  // 2. Upload assets via streaming
  //
  for (const asset of assets) {
    await uploadAssetStream({
      owner,
      repo,
      release_id: release.id,
      asset,
    });
  }

  //
  // 3. Publish release
  //
  await octokit.repos.updateRelease({
    owner,
    repo,
    release_id: release.id,
    draft: false,
  });
}

async function run() {
  const releases = JSON.parse(publishedPackages);

  console.log("Release candidates:", releases);

  const isCi = !!process.env.CI;
  if (!isCi) {
    console.log("CI not detected, blocking remote release. Only logging release config to console...");
  }

  const artifactPath = (artifact) => path.resolve(__dirname, "../artifacts", artifact);

  for (const release of releases) {
    const { name, version } = release;
    const assets = fs
      .readdirSync(artifactPath(name))
      .filter((file) => {
        return !file.endsWith(".checksum.txt") && !file.startsWith(".");
      })
      .map((artifactName) => {
        const resolvedArtifactPath = artifactPath(path.join(name, artifactName));
        // Generate checksums
        const checksum512 = execSync(`shasum -a 512 "${resolvedArtifactPath}" | xxd -r -p | base64`).toString().trim();
        const digest256 = execSync(`shasum -a 256 "${resolvedArtifactPath}" | cut -d ' ' -f 1`).toString().trim();
        if (!checksum512 || !digest256) {
          throw new Error(`Failed to generate checksums for ${resolvedArtifactPath}`);
        }

        const contentType = mime.lookup(resolvedArtifactPath) || "application/octet-stream";
        const stream = fs.createReadStream(resolvedArtifactPath);
        const { size } = fs.statSync(resolvedArtifactPath);

        return { artifactName, resolvedArtifactPath, checksum512, digest256, size, contentType, stream };
      });

    const releaseName = `${name}@${version}`;
    if (assets.length === 0) {
      throw new Error(`No artifacts found for ${releaseName}`);
    }

    const bodyText = assets
      .map(({ artifactName, checksum512 }) => `\`${artifactName}\`: \`${checksum512}\``)
      .sort()
      .join("\n");

    const options = {
      owner: "electron-userland",
      repo: "electron-builder-binaries",
      tag: releaseName,
      name: releaseName,
      body: `*checksums*\n\n${bodyText}`,
      assets,
      targetCommit: getTargetCommitish(), // e.g. "master" or specific commit
    };

    if (!isCi) {
      console.log("\n\nRelease options:", options);
      continue;
    }
    // If CI, validate token is present
    const token = process.env.GITHUB_TOKEN;
    if (!token) {
      console.error("GITHUB_TOKEN environment variable is not set.");
      process.exit(1);
    }

    const subjects = assets.map(({ artifactName, digest256 }) => ({ name: artifactName, digest: { sha256: digest256 } }));
    console.log(`Attesting artifacts for ${releaseName}. Files: `, subjects);
    await attestProvenance({ token, subjects });
    console.log("Attestation successful for artifacts.");

    await atomicReleaseWithStreams(options);
    console.log(`Artifacts for ${releaseName} uploaded successfully.`);
  }
}

run();
