#!/usr/bin/env node
import { base64ToString, base64ToUint8Array } from 'uint8array-extras';
import { readFile, writeFile, mkdir } from 'fs/promises';
import { join, dirname } from 'path';
import { z } from 'zod';
import { execSync } from 'child_process';
import { Command } from 'commander';
import { randomBytes } from 'crypto';
import { fileURLToPath } from 'url';

const decryptEnvVar = async ({
  encrypted,
  encryptedPrivateKey: privateKeyData,
  passphrase
}) => {
  try {
    const { iv, encryptedPrivateKey } = JSON.parse(
      base64ToString(privateKeyData)
    );
    const passphraseKey = await globalThis.crypto.subtle.importKey(
      "raw",
      base64ToUint8Array(passphrase),
      {
        name: "AES-GCM",
        length: 256
      },
      false,
      ["unwrapKey"]
    );
    const privateKey = await globalThis.crypto.subtle.unwrapKey(
      "pkcs8",
      base64ToUint8Array(encryptedPrivateKey),
      passphraseKey,
      {
        name: "AES-GCM",
        iv: base64ToUint8Array(iv)
      },
      {
        name: "RSA-OAEP",
        hash: { name: "SHA-256" }
      },
      false,
      ["unwrapKey"]
    );
    const secretData = JSON.parse(base64ToString(encrypted));
    const symmetricKey = await globalThis.crypto.subtle.unwrapKey(
      "raw",
      base64ToUint8Array(secretData.encryptedSymmetricKey),
      privateKey,
      {
        name: "RSA-OAEP"
      },
      {
        name: "AES-GCM",
        length: 256
      },
      false,
      ["decrypt"]
    );
    const decrypted = await globalThis.crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: base64ToUint8Array(secretData.iv)
      },
      symmetricKey,
      base64ToUint8Array(secretData.encryptedValue)
    );
    const plaintext = new TextDecoder().decode(decrypted);
    return plaintext;
  } catch (error) {
    console.log("error", error);
  }
};

const getPassphraseEnvName = ({ stage }) => `GITENVS_PASSPHRASE_${stage?.toUpperCase()}`;
const GITENVS_STAGE_ENV_NAME = "GITENVS_STAGE";
const GITENVS_DIR_ENV_NAME = "GITENVS_DIR";

const getCwd = () => {
  const cwd = process.env[GITENVS_DIR_ENV_NAME] ?? process.cwd();
  return cwd;
};

const EnvVarValue = z.object({
  value: z.string(),
  encrypted: z.boolean()
});
const EnvVar = z.object({
  id: z.string(),
  fileId: z.string(),
  key: z.string(),
  values: z.record(z.string(), EnvVarValue)
});
const EnvFileType = z.enum(["dotenv"]);
const EnvFile = z.object({
  id: z.string(),
  name: z.string(),
  filePath: z.string(),
  type: EnvFileType
});
const EnvStage = z.object({
  name: z.string(),
  publicKey: z.string(),
  encryptedPrivateKey: z.string()
});
const Gitenvs = z.object({
  version: z.string(),
  envStages: z.array(EnvStage),
  envFiles: z.array(EnvFile),
  envVars: z.array(EnvVar)
});
z.object({
  stageName: z.string(),
  passphrase: z.string()
});

const PASSPHRASE_FILE_NAME = "gitenvs.passphrases.json";
const getPassphrase = async ({
  stage,
  passphrase,
  passphrasePath = join(getCwd(), PASSPHRASE_FILE_NAME)
}) => {
  const getFromEnvVars = () => {
    const envName = getPassphraseEnvName({ stage });
    return process.env[envName];
  };
  const getFromFile = async () => {
    const currentFileContent = await readFile(passphrasePath, "utf-8").then((res) => JSON.parse(res)).catch(() => []);
    const requestedPassphrase = currentFileContent.find(
      (p) => p.stageName === stage
    )?.passphrase;
    return requestedPassphrase;
  };
  return passphrase ?? getFromEnvVars() ?? await getFromFile();
};

const getGitenvs = async () => {
  const gitenvsContent = await readFile(join(getCwd(), "gitenvs.json"), "utf-8");
  const gitenvs = Gitenvs.parse(JSON.parse(gitenvsContent));
  return gitenvs;
};

const getGitenvsUiEnvVars = () => ({
  ...process.env,
  GITENVS_DIR: getCwd(),
  GITENVS_ENCRYPTION_TOKEN: randomBytes(32).toString("hex"),
  PORT: "1337"
});
const program = new Command();
program.name("gitenvs").description("Save your env variables in git \u2013 encrypted!");
program.command("create").description("Creates env files").option(
  "--stage <stage>",
  "Example: production, staging, development",
  "development"
).option("--passphrase <passphrase>").option("--passphrasePath <passphrasePath>").action(
  async (options) => {
    const gitenvs = await getGitenvs();
    const stage = process.env[GITENVS_STAGE_ENV_NAME] || options.stage;
    if (!stage) {
      console.error(
        `Stage is required. Set it with --stage <stage> or with env var: ${GITENVS_STAGE_ENV_NAME}`
      );
      process.exit(1);
    }
    const envStage = gitenvs.envStages.find(
      (envStage2) => envStage2.name === stage
    );
    if (!envStage) {
      console.error(`Env stage ${stage} not found`);
      process.exit(1);
    }
    const passphrase = await getPassphrase({
      stage,
      passphrase: options.passphrase,
      passphrasePath: options.passphrasePath
    });
    if (!passphrase) {
      console.error(
        `Requested passphrase for stage ${stage} not found in ${options.passphrasePath ?? join(getCwd(), PASSPHRASE_FILE_NAME)}`
      );
      process.exit(1);
    }
    const promises = [];
    for (const envFile of gitenvs.envFiles) {
      const envVars = gitenvs.envVars.filter(
        (envVar) => envVar.fileId === envFile.id
      );
      const dotenvVars = await Promise.all(
        envVars.map(async (envVar) => {
          const value = envVar.values[stage];
          if (!value) return { key: envVar.key, value: "" };
          if (!value.encrypted) {
            return {
              key: envVar.key,
              value: value.value
            };
          }
          return {
            key: envVar.key,
            value: await decryptEnvVar({
              encrypted: value.value,
              encryptedPrivateKey: envStage.encryptedPrivateKey,
              passphrase
            })
          };
        })
      );
      const dotenvContent = dotenvVars.map((dotenvVar) => {
        const includesDoubleQuote = dotenvVar.value?.includes('"') ?? false;
        const includesBacktick = dotenvVar.value?.includes("`") ?? false;
        let wrapWith = "`";
        if (includesBacktick) {
          if (!includesDoubleQuote) {
            wrapWith = '"';
          } else {
            wrapWith = "";
          }
        }
        return `${dotenvVar.key}=${wrapWith}${dotenvVar.value}${wrapWith}`;
      }).join("\n");
      promises.push(
        writeFile(join(getCwd(), envFile.filePath), dotenvContent, "utf-8")
      );
    }
    await Promise.allSettled(promises);
  }
);
program.command("dev-ui").description("Starts a browser UI to edit env vars").action(() => {
  execSync("pnpm run dev-next", {
    stdio: "inherit",
    env: getGitenvsUiEnvVars()
  });
});
program.command("init-playground").description("Initializes the playground").action(async () => {
  await mkdir("playground", { recursive: true });
  await writeFile(
    "playground/package.json",
    JSON.stringify(
      {
        name: "playground",
        version: "1.0.0",
        private: true
      },
      null,
      2
    ),
    "utf-8"
  );
});
program.command("ui", { isDefault: true }).description("Starts a browser UI to edit env vars").action(() => {
  const nodePath = process.argv0;
  const currentDir = dirname(fileURLToPath(import.meta.url));
  execSync(`${nodePath} ${currentDir}/next/server.js`, {
    stdio: "inherit",
    env: getGitenvsUiEnvVars()
  });
});
program.parse();
