#!/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 truthy(value) {
  return ["1", "true", "yes", "on"].includes(String(value ?? "").toLowerCase());
}

function safeFileName(identifier, suffix) {
  return `${identifier.replace(/[^A-Za-z0-9]/g, "").toLowerCase()}${suffix}`;
}

function bundleDisplayName(identifier, appId) {
  return identifier === appId ? "Burrow App" : "Burrow Network Extension";
}

async function findCertificate(jwt, preferredTypes, preferredId) {
  const certs = await ascFetch(
    jwt,
    "https://api.appstoreconnect.apple.com/v1/certificates?limit=200",
  );
  const data = certs.data ?? [];
  if (preferredId) {
    const cert = data.find((item) => item?.id === preferredId);
    if (!cert) throw new Error(`unable to locate certificate id ${preferredId} via ASC API`);
    const type = cert?.attributes?.certificateType;
    if (preferredTypes.length && !preferredTypes.includes(type)) {
      throw new Error(`certificate ${preferredId} has type ${type}, expected ${preferredTypes.join(" or ")}`);
    }
    return cert;
  }
  for (const type of preferredTypes) {
    const cert = data.find((item) => item?.attributes?.certificateType === type);
    if (cert) return cert;
  }
  throw new Error(`unable to locate certificate type ${preferredTypes.join(" or ")} via ASC API`);
}

async function resolveBundle(jwt, target, { createMissing, platform }) {
  const bundleResp = await ascFetch(
    jwt,
    `https://api.appstoreconnect.apple.com/v1/bundleIds?limit=5&filter[identifier]=${encodeURIComponent(target.identifier)}`,
  );
  let bundle = (bundleResp.data ?? []).find(
    (item) => item?.attributes?.identifier === target.identifier,
  );
  if (bundle || !createMissing) return bundle;

  const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIds", {
    method: "POST",
    body: JSON.stringify({
      data: {
        type: "bundleIds",
        attributes: {
          identifier: target.identifier,
          name: target.bundleName ?? target.name,
          platform,
        },
      },
    }),
  });
  bundle = created?.data;
  if (bundle?.id) {
    process.stdout.write(`created bundle id ${target.identifier} (${platform})\n`);
  }
  return bundle;
}

async function listCapabilityTypes(jwt, bundleId) {
  const resp = await ascFetch(
    jwt,
    `https://api.appstoreconnect.apple.com/v1/bundleIds/${bundleId}/bundleIdCapabilities`,
  );
  return new Set((resp.data ?? []).map((item) => item?.attributes?.capabilityType).filter(Boolean));
}

async function enableCapability(jwt, bundleId, capabilityType, settings) {
  await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/bundleIdCapabilities", {
    method: "POST",
    body: JSON.stringify({
      data: {
        type: "bundleIdCapabilities",
        attributes: {
          capabilityType,
          ...(settings ? { settings } : {}),
        },
        relationships: {
          bundleId: {
            data: {
              id: bundleId,
              type: "bundleIds",
            },
          },
        },
      },
    }),
  });
}

async function profileIncludesCertificate(jwt, profileId, certificateId) {
  const resp = await ascFetch(
    jwt,
    `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}/certificates?limit=200`,
  );
  return (resp.data ?? []).some((item) => item?.id === certificateId);
}

async function ensureCapabilities(jwt, bundle, capabilities) {
  const existing = await listCapabilityTypes(jwt, bundle.id);
  for (const capability of capabilities) {
    if (existing.has(capability.type)) continue;
    try {
      await enableCapability(jwt, bundle.id, capability.type, capability.settings);
      process.stdout.write(`enabled ${capability.type} for ${bundle.attributes.identifier}\n`);
    } catch (err) {
      process.stderr.write(`warning: could not enable ${capability.type} for ${bundle.attributes.identifier}: ${err.message}\n`);
    }
  }
}

async function syncTarget(jwt, cert, profileType, target, outDir, options) {
  const bundle = await resolveBundle(jwt, target, options);
  if (!bundle) throw new Error(`bundleId not found in ASC: ${target.identifier}`);

  if (options.enableCapabilities && target.capabilities?.length) {
    await ensureCapabilities(jwt, bundle, target.capabilities);
  }

  const existing = await ascFetch(
    jwt,
    `https://api.appstoreconnect.apple.com/v1/profiles?limit=10&filter[profileType]=${encodeURIComponent(profileType)}&filter[name]=${encodeURIComponent(target.name)}`,
  );
  let profileId = (existing.data ?? []).find((item) => item?.attributes?.name === target.name)?.id;
  if (profileId && options.replaceCertificateMismatch) {
    const includesCertificate = await profileIncludesCertificate(jwt, profileId, cert.id);
    if (!includesCertificate) {
      await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`, {
        method: "DELETE",
      });
      process.stdout.write(`deleted stale profile ${target.name}; it did not include certificate ${cert.id}\n`);
      profileId = undefined;
    }
  }
  if (!profileId) {
    const created = await ascFetch(jwt, "https://api.appstoreconnect.apple.com/v1/profiles", {
      method: "POST",
      body: JSON.stringify({
        data: {
          type: "profiles",
          attributes: { name: target.name, profileType },
          relationships: {
            bundleId: { data: { type: "bundleIds", id: bundle.id } },
            certificates: { data: [{ type: "certificates", id: cert.id }] },
          },
        },
      }),
    });
    profileId = created?.data?.id;
  }
  if (!profileId) throw new Error(`failed to resolve/create profile for ${target.identifier}`);

  const profile = await ascFetch(jwt, `https://api.appstoreconnect.apple.com/v1/profiles/${profileId}`);
  const content = profile?.data?.attributes?.profileContent;
  if (!content) throw new Error(`profileContent missing for ${target.name} (${profileId})`);

  fs.mkdirSync(outDir, { recursive: true });
  const profileBytes = Buffer.from(content, "base64");
  const profilePath = path.join(outDir, target.file);
  fs.writeFileSync(profilePath, profileBytes);
  if (target.mobileFile) {
    fs.writeFileSync(path.join(outDir, target.mobileFile), profileBytes);
  }
  process.stdout.write(`synced ${target.identifier} -> ${profilePath}\n`);
}

async function main() {
  const args = parseArgs(process.argv);
  if (args.help) {
    console.log("Usage: sync-provisioning-profiles.mjs --platform ios|macos|all --key-id ID --issuer-id ID --key-file AuthKey.p8 --out-dir DIR [--ios-certificate-id ID] [--macos-certificate-id ID] [--replace-certificate-mismatch true]");
    process.exit(0);
  }

  const platform = args.platform ?? "all";
  const keyId = args["key-id"];
  const issuerId = args["issuer-id"];
  const keyFile = args["key-file"];
  const outDir = args["out-dir"] ?? ".forgejo/actions/export";
  const appId = args["app-id"] ?? "net.burrow.app";
  const networkId = args["network-id"] ?? `${appId}.network`;
  const iosCertificateId = args["ios-certificate-id"] ?? process.env.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID;
  const macosCertificateId = args["macos-certificate-id"] ?? process.env.BURROW_DEVELOPER_ID_CERTIFICATE_ID;
  const createMissing = truthy(args["create-missing-bundle-ids"]);
  const enableCapabilities = createMissing || truthy(args["enable-capabilities"]);
  const replaceCertificateMismatch = truthy(args["replace-certificate-mismatch"]);
  const bundlePlatform = args["bundle-platform"] ?? "UNIVERSAL";
  const associatedDomains = (args["associated-domains"] ?? "applinks:burrow.rs?mode=developer,webcredentials:burrow.rs?mode=developer")
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
  const appCapabilities = [
    { type: "NETWORK_EXTENSIONS" },
    { type: "APP_GROUPS" },
    {
      type: "ASSOCIATED_DOMAINS",
      settings: [{ key: "ASSOCIATED_DOMAIN_IDS", options: associatedDomains.map((key) => ({ key, enabled: true })) }],
    },
  ];
  const extensionCapabilities = [
    { type: "NETWORK_EXTENSIONS" },
    { type: "APP_GROUPS" },
  ];
  const options = { createMissing, enableCapabilities, replaceCertificateMismatch, platform: bundlePlatform };

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

  const jwt = await makeJwt({ keyId, issuerId, keyPem: fs.readFileSync(keyFile, "utf8") });

  if (platform === "ios" || platform === "all") {
    const cert = await findCertificate(jwt, ["IOS_DISTRIBUTION", "DISTRIBUTION", "APPLE_DISTRIBUTION"], iosCertificateId);
    const targets = [appId, networkId].map((identifier) => ({
      identifier,
      name: `${identifier}.app-store`,
      bundleName: bundleDisplayName(identifier, appId),
      file: safeFileName(identifier, "appstore.provisionprofile"),
      mobileFile: safeFileName(identifier, "appstore.mobileprovision"),
      capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
    }));
    for (const target of targets) {
      await syncTarget(jwt, cert, "IOS_APP_STORE", target, outDir, options);
    }
  }

  if (platform === "macos" || platform === "all") {
    const cert = await findCertificate(jwt, ["DEVELOPER_ID_APPLICATION"], macosCertificateId);
    const targets = [appId, networkId].map((identifier) => ({
      identifier,
      name: `${identifier}.developer-id`,
      bundleName: bundleDisplayName(identifier, appId),
      file: safeFileName(identifier, "developerid.provisionprofile"),
      capabilities: identifier === appId ? appCapabilities : extensionCapabilities,
    }));
    for (const target of targets) {
      await syncTarget(jwt, cert, "MAC_APP_DIRECT", target, outDir, options);
    }
  }
}

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