#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { webcrypto } from "node:crypto";

const crypto = globalThis.crypto ?? webcrypto;

function b64url(input) {
  const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
  return buf
    .toString("base64")
    .replace(/=/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
}

function parseArgs(argv) {
  const out = { _: [] };
  for (let i = 2; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === "--help" || arg === "-h") out.help = true;
    else if (arg.startsWith("--")) {
      const key = arg.slice(2);
      const value = argv[i + 1];
      if (!value || value.startsWith("--")) throw new Error(`missing value for --${key}`);
      out[key] = value;
      i++;
    } else {
      out._.push(arg);
    }
  }
  return out;
}

async function makeJwt({ keyId, issuerId, keyPem }) {
  const header = { alg: "ES256", kid: keyId, typ: "JWT" };
  const now = Math.floor(Date.now() / 1000);
  const payload = { iss: issuerId, exp: now + 20 * 60, aud: "appstoreconnect-v1" };
  const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
  const pkcs8Der = Buffer.from(
    keyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\s+/g, ""),
    "base64",
  );
  const key = await crypto.subtle.importKey(
    "pkcs8",
    pkcs8Der,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["sign"],
  );
  const sig = await crypto.subtle.sign(
    { name: "ECDSA", hash: "SHA-256" },
    key,
    new TextEncoder().encode(input),
  );
  return `${input}.${b64url(Buffer.from(sig))}`;
}

async function ascFetch(jwt, url, opts = {}) {
  const res = await fetch(url, {
    ...opts,
    headers: {
      ...(opts.headers ?? {}),
      Authorization: `Bearer ${jwt}`,
      "Content-Type": "application/json",
    },
  });
  const text = await res.text();
  let body;
  try {
    body = text ? JSON.parse(text) : {};
  } catch {
    body = { raw: text };
  }
  if (!res.ok) {
    throw new Error(`ASC ${res.status} ${url}: ${JSON.stringify(body).slice(0, 2000)}`);
  }
  return body;
}

function certificateFileName(type, id) {
  return `${type.toLowerCase().replace(/_/g, "-")}-${id}.cer`;
}

function csrContentForApi(csrFile) {
  const bytes = fs.readFileSync(csrFile);
  const text = bytes.toString("ascii");
  const pemMatch = text.match(/-----BEGIN CERTIFICATE REQUEST-----([\s\S]+?)-----END CERTIFICATE REQUEST-----/);
  if (pemMatch) {
    return pemMatch[1].replace(/\s+/g, "");
  }
  return bytes.toString("base64");
}

async function main() {
  const args = parseArgs(process.argv);
  if (args.help) {
    console.log("Usage: create-asc-certificate.mjs --key-id ID --issuer-id ID --key-file AuthKey.p8 --certificate-type IOS_DISTRIBUTION --csr-file file.csr --out-dir DIR");
    process.exit(0);
  }

  const keyId = args["key-id"] ?? process.env.APP_STORE_CONNECT_KEY_ID;
  const issuerId = args["issuer-id"] ?? process.env.APP_STORE_CONNECT_ISSUER_ID;
  const keyFile = args["key-file"] ?? process.env.APP_STORE_CONNECT_KEY_FILE;
  const certificateType = args["certificate-type"] ?? "IOS_DISTRIBUTION";
  const csrFile = args["csr-file"];
  const outDir = args["out-dir"] ?? ".";
  const outFile = args["out-file"];
  const metadataFile = args["metadata-file"];

  if (!keyId || !issuerId || !keyFile || !csrFile) {
    throw new Error("required: --key-id --issuer-id --key-file --csr-file");
  }

  const jwt = await makeJwt({
    keyId,
    issuerId,
    keyPem: fs.readFileSync(keyFile, "utf8"),
  });
  const csrContent = csrContentForApi(csrFile);
  const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/certificates", {
    method: "POST",
    body: JSON.stringify({
      data: {
        type: "certificates",
        attributes: {
          certificateType,
          csrContent,
        },
      },
    }),
  });

  const cert = created?.data;
  const id = cert?.id;
  const content = cert?.attributes?.certificateContent;
  if (!id || !content) {
    throw new Error(`ASC response did not include certificate id/content: ${JSON.stringify(created).slice(0, 2000)}`);
  }

  fs.mkdirSync(outDir, { recursive: true });
  const certificatePath = outFile ?? path.join(outDir, certificateFileName(certificateType, id));
  fs.writeFileSync(certificatePath, Buffer.from(content, "base64"));

  const metadata = {
    id,
    certificateType: cert?.attributes?.certificateType ?? certificateType,
    displayName: cert?.attributes?.displayName,
    name: cert?.attributes?.name,
    platform: cert?.attributes?.platform,
    expirationDate: cert?.attributes?.expirationDate,
    serialNumber: cert?.attributes?.serialNumber,
    certificatePath,
  };
  if (metadataFile) {
    fs.writeFileSync(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`);
  }
  process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
}

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