// @ts-check
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { exec as execCb, execFile as execFileCb } from "node:child_process";
import { env } from "node:process";
import { promisify } from "node:util";

const exec = promisify(execCb);
const execFile = promisify(execFileCb);

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const __root = path.resolve(path.join(__dirname, ".."));
const targetDir = path.join(__root, "target");

const arch =
  process.env.RUST_TARGET_TRIPLE?.split("-")[0] ??
  (process.arch === "arm64" ? "aarch64" : "x86_64");

const BASE_CARGO_TOML = `[env]
FFMPEG_DIR = { relative = true, force = true, value = "target/native-deps" }
`;

async function main() {
  await fs.mkdir(targetDir, { recursive: true });

  let cargoConfigContents = BASE_CARGO_TOML;

  if (process.platform === "darwin") {
    const NATIVE_DEPS_VERSION = "v0.25";
    const NATIVE_DEPS_URL = `https://github.com/spacedriveapp/native-deps/releases/download/${NATIVE_DEPS_VERSION}`;

    const NATIVE_DEPS_ASSETS = {
      x86_64: "native-deps-x86_64-darwin-apple.tar.xz",
      aarch64: "native-deps-aarch64-darwin-apple.tar.xz",
    };

    const nativeDepsTar = NATIVE_DEPS_ASSETS[arch];
    const nativeDepsTarPath = path.join(targetDir, nativeDepsTar);
    let downloadedNativeDeps = false;

    if (!(await fileExists(nativeDepsTarPath))) {
      console.log(`Downloading ${nativeDepsTar}`);
      const nativeDepsBytes = await fetch(`${NATIVE_DEPS_URL}/${nativeDepsTar}`)
        .then((r) => r.blob())
        .then((b) => b.arrayBuffer());
      await fs.writeFile(nativeDepsTarPath, Buffer.from(nativeDepsBytes));
      console.log("Downloaded native deps");
      downloadedNativeDeps = true;
    } else console.log(`Using cached ${nativeDepsTar}`);

    const nativeDepsFolder = `native-deps`;
    const nativeDepsDir = path.join(targetDir, nativeDepsFolder);
    const frameworkDir = path.join(nativeDepsDir, "Spacedrive.framework");
    if (downloadedNativeDeps || !(await fileExists(nativeDepsDir))) {
      await fs.mkdir(nativeDepsDir, { recursive: true });
      await execFile("tar", ["xf", nativeDepsTarPath, "-C", nativeDepsDir]);
      console.log(`Extracted ${nativeDepsFolder}`);
    } else console.log(`Using cached ${nativeDepsFolder}`);

    await trimMacOSFramework(frameworkDir);
    console.log("Trimmed .framework");

    console.log("Signing .framework libraries");
    await signMacOSFrameworkLibs(frameworkDir);
    console.log("Signed .framework libraries");

    const frameworkTargetDir = path.join(
      targetDir,
      "Frameworks",
      "Spacedrive.framework"
    );
    await fs.rm(frameworkTargetDir, { recursive: true }).catch(() => {});
    await fs.cp(
      frameworkDir,
      path.join(targetDir, "Frameworks", "Spacedrive.framework"),
      { recursive: true }
    );

    // alternative to specifying dylibs as linker args
    await fs.mkdir(path.join(targetDir, "/debug"), { recursive: true });
    for (const name of await fs.readdir(path.join(nativeDepsDir, "lib"))) {
      await fs.copyFile(
        path.join(nativeDepsDir, "lib", name),
        path.join(targetDir, "debug", name)
      );
    }
    console.log("Copied ffmpeg dylibs to target/debug");
  } else if (process.platform === "win32") {
    const FFMPEG_VERSION = "7.1";
    const FFMPEG_ZIP_NAME = `ffmpeg-${FFMPEG_VERSION}-full_build-shared`;
    const FFMPEG_ZIP_URL = `https://github.com/GyanD/codexffmpeg/releases/download/${FFMPEG_VERSION}/${FFMPEG_ZIP_NAME}.zip`;

    await fs.mkdir(targetDir, { recursive: true });

    let downloadedFfmpeg = false;
    const ffmpegZip = `ffmpeg-${FFMPEG_VERSION}.zip`;
    const ffmpegZipPath = path.join(targetDir, ffmpegZip);
    if (!(await fileExists(ffmpegZipPath))) {
      const ffmpegZipBytes = await fetch(FFMPEG_ZIP_URL)
        .then((r) => r.blob())
        .then((b) => b.arrayBuffer());
      await fs.writeFile(ffmpegZipPath, Buffer.from(ffmpegZipBytes));
      console.log(`Downloaded ${ffmpegZip}`);
      downloadedFfmpeg = true;
    } else console.log(`Using cached ${ffmpegZip}`);

    const ffmpegDir = path.join(targetDir, "ffmpeg");
    if (!(await fileExists(ffmpegDir)) || downloadedFfmpeg) {
      await execFile("tar", ["xf", ffmpegZipPath, "-C", targetDir]);
      await fs.rm(ffmpegDir, { recursive: true, force: true }).catch(() => {});
      await fs.rename(path.join(targetDir, FFMPEG_ZIP_NAME), ffmpegDir);
      console.log("Extracted ffmpeg");
    } else console.log("Using cached ffmpeg");

    // alternative to adding ffmpeg/bin to PATH
    await fs.mkdir(path.join(targetDir, "debug"), { recursive: true });
    for (const name of await fs.readdir(path.join(ffmpegDir, "bin"))) {
      await fs.copyFile(
        path.join(ffmpegDir, "bin", name),
        path.join(targetDir, "debug", name)
      );
    }
    console.log("Copied ffmpeg dylibs to target/debug");

    if (!(await fileExists(path.join(targetDir, "native-deps"))))
      await fs.mkdir(path.join(targetDir, "native-deps"), { recursive: true });

    await fs.cp(
      path.join(ffmpegDir, "lib"),
      path.join(targetDir, "native-deps", "lib"),
      {
        recursive: true,
        force: true,
      }
    );
    await fs.cp(
      path.join(ffmpegDir, "include"),
      path.join(targetDir, "native-deps", "include"),
      {
        recursive: true,
        force: true,
      }
    );
    console.log("Copied ffmpeg/lib and ffmpeg/include to target/native-deps");

    const { stdout: vcInstallDir } = await exec(
      '$(& "${env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" -latest -property installationPath)',
      { shell: "powershell.exe" }
    );

    const libclangPath = path.join(
      vcInstallDir.trim(),
      "VC/Tools/LLVM/x64/bin/libclang.dll"
    );

    cargoConfigContents += `LIBCLANG_PATH = "${libclangPath.replaceAll(
      "\\",
      "/"
    )}"\n`;
  }

  await fs.mkdir(path.join(__root, ".cargo"), { recursive: true });
  await fs.writeFile(
    path.join(__root, ".cargo/config.toml"),
    cargoConfigContents
  );
}

main();

async function trimMacOSFramework(frameworkDir) {
  const headersDir = path.join(frameworkDir, "Headers");
  const librariesDir = path.join(frameworkDir, "Libraries");

  const libraries = await fs.readdir(librariesDir);

  const unnecessaryLibraries = libraries.filter(
    (v) =>
      !(
        v.startsWith("libav") ||
        v.startsWith("libsw") ||
        v.startsWith("libpostproc")
      )
  );

  for (const lib of unnecessaryLibraries) {
    await fs.rm(path.join(librariesDir, lib), { recursive: true });
  }

  const headers = await fs.readdir(headersDir);

  const unnecessaryHeaders = headers.filter(
    (v) =>
      !(
        v.startsWith("libav") ||
        v.startsWith("libsw") ||
        v.startsWith("libpostproc")
      )
  );

  for (const header of unnecessaryHeaders) {
    await fs.rm(path.join(headersDir, header), { recursive: true });
  }

  const modelsPath = path.join(frameworkDir, "Resources", "Models");
  if (await fileExists(modelsPath))
    await fs.rm(modelsPath, {
      recursive: true,
    });
}

async function signMacOSFrameworkLibs(frameworkDir) {
  const signId = env.APPLE_SIGNING_IDENTITY || "-";
  const keychain = env.APPLE_KEYCHAIN ? `--keychain ${env.APPLE_KEYCHAIN}` : "";

  // Sign dylibs (Required for them to work on macOS 13+)
  await fs
    .readdir(path.join(frameworkDir, "Libraries"), {
      recursive: true,
      withFileTypes: true,
    })
    .then((files) =>
      Promise.all(
        files
          .filter((entry) => entry.isFile() && entry.name.endsWith(".dylib"))
          .map((entry) =>
            exec(
              `codesign ${keychain} -s "${signId}" -f "${path.join(
                entry.path,
                entry.name
              )}"`
            )
          )
      )
    );
}

async function fileExists(path) {
  return await fs
    .access(path)
    .then(() => true)
    .catch(() => false);
}
