import pm2 from "pm2";
import inquirer from "inquirer";
import util from "util";
import jetpack from "fs-jetpack";
import dotenv from "dotenv";
import { exec as exe, spawn } from "child_process";
import ora from "ora";
import chalk from "chalk";
import chalkAnimation from "chalk-animation";
import { killPortProcess } from "kill-port-process";
const exec = util.promisify(exe);
const pjson = jetpack.read("package.json", "json");
const spinner = ora();
spinner.spinner = "dots3";
spinner.color = "green";
//////////////////////////////////////////////////////////////////
// ---------------------- FLOW COMMANDS --------------------------
//////////////////////////////////////////////////////////////////
const EMULATOR_DEPLOYMENT =
  "flow project deploy -o json --network=emulator -f flow.json --update";
const TESTNET_DEPLOYMENT =
  "flow project deploy -o json --network=testnet -f flow.json -f flow.testnet.json --update";
function initializeStorefront(network) {
  if (!network) return envErr();
  return `flow transactions send -o json --network=${network} --signer ${network}-account ./cadence/transactions/nftStorefront/setup_account.cdc -f flow.json ${
    network !== "emulator" ? `-f flow.${network}.json` : ""
  }`;
}
function initializeKittyItems(network) {
  if (!network) return envErr();
  return `flow transactions send -o json --network=${network} --signer ${network}-account ./cadence/transactions/kittyItems/setup_account.cdc -f flow.json ${
    network !== "emulator" ? `-f flow.${network}.json` : ""
  }`;
}
//////////////////////////////////////////////////////////////////
// ------------------- HELPER FUNCTIONS -----------------------
//////////////////////////////////////////////////////////////////
function convertToEnv(object) {
  let envFile = "";
  for (const key of Object.keys(object)) {
    envFile += `${key}=${object[key]}\n`;
  }
  return envFile;
}
function envErr() {
  throw new Error(
    `Unknown or missing CHAIN_ENV environment variable.
         Please provide one of the following: "emulator", "testnet"`
  );
}
function adminError() {
  throw new Error(
    `Unknown or missing ADMIN_ADRESS environment variable.
      Please create a testnet account and add your credentials to .env.tenstnet`
  );
}
function deploy(chainEnv) {
  switch (chainEnv) {
    case "emulator":
      return EMULATOR_DEPLOYMENT;
    case "testnet":
      return TESTNET_DEPLOYMENT;
    default:
      envErr();
  }
}
async function generateKeys() {
  const { stdout: out, stderr: err } = await exec(
    `flow keys generate -o json`,
    { cwd: process.cwd() }
  );
  if (err) {
    console.log(err);
  }
  return JSON.parse(out);
}
function requireEnv(chainEnv) {
  switch (chainEnv) {
    case "emulator":
      return ".env.emulator";
    case "testnet":
      if (!jetpack.exists(".env.testnet")) {
        throw new Error(
          "Testnet deployment config not created. See README.md for instructions."
        );
      }
      return ".env.testnet";
    default:
      envErr();
  }
}
function runProcess(config, cb = () => {}) {
  return new Promise((resolve, reject) => {
    pm2.start(config, function (err, result) {
      if (err) {
        console.log(err);
        reject(err);
      }
      resolve(result);
    });
  });
}
function stopProcess(name, port) {
  return new Promise((resolve, reject) => {
    pm2.stop(name, function (err, result) {
      pm2.delete(name, async function () {
        await killPortProcess(port);
        resolve();
      });
    });
  });
}
async function cleanupTestnetConfig() {
  spinner.warn("Cleaning up old testnet config and database...");
  const dbExists = jetpack.exists("./api/kitty-items-db-testnet.sqlite");
  if (dbExists) {
    spinner.info(
      `We found an old testnet database. If you're starting a new testnet deployment, you can delete it.`
    );
    let removeDb = await inquirer.prompt({
      type: "confirm",
      name: "confirm",
      message: `Are you sure you want to remove the testnet database?`,
      default: true
    });
    if (removeDb.confirm) {
      spinner.info(`Removing testnet database...`);
      jetpack.remove("./api/kitty-items-db-testnet.sqlite");
      spinner.info(`Removing .env.testnet file...`);
      jetpack.remove(".env.testnet");
    } else {
      throw new Error(
        "You must remove the testnet database before starting a new deployment."
      );
    }
  }
  jetpack.remove("./.env.testnet");
  jetpack.remove("./api/kitty-items-db-testnet.sqlite");
}
//////////////////////////////////////////////////////////////////
// ------------- PROCESS MANAGEMENT ENTRYPOINT -------------------
//////////////////////////////////////////////////////////////////
pm2.connect(false, async function (err) {
  if (err) {
    console.error(err);
    pm2.disconnect();
    process.exit(2);
  }
  pm2.flush();
  let env = {};
  spinner.info(`Stopping previously launched processes...${"\n"}`);
  await stopProcess("api", [3000]);
  await stopProcess("web", [3001]);
  await stopProcess("dev-wallet", [8701]);
  await stopProcess("emulator", [8080, 3569]);
  // ------------------------------------------------------------
  // ------------- CHECK FOR CORRECT NODE VERSION ---------------
  if (
    !process.version.split(".")[0].includes(pjson.engines.node.split(".")[0])
  ) {
    spinner.warn(
      `This project requires Node version ${chalk.yellow(
        "16.x"
      )} or higher. Please install Node.js and try again.${"\n"}`
    );
    pm2.disconnect();
    return;
  }
  // ------------------------------------------------------------
  // ------------- TESTNET ACCOUNT CREATION ---------------------
  async function bootstrapNewTestnetAccount() {
    const result = await generateKeys();
    console.log(`
      ${chalk.greenBright("Next steps:")}
      1. Create a new account using the testnet faucet by visiting this URL:
      ${chalk.cyanBright(
        `https://testnet-faucet.onflow.org/?key=${result.public}&source=ki`
      )}
      2. Copy the new account address from the faucet, and paste it below π
      ${chalk.yellowBright("β οΈ  Don't exit this terminal.")}
      `);
    const testnet = await inquirer.prompt([
      {
        type: "input",
        name: "account",
        message: "Paste your new testnet account address here:"
      }
    ]);
    result.account = testnet.account;
    jetpack.file(`testnet-credentials-${testnet.account}.json`, {
      content: JSON.stringify(result)
    });
    const testnetEnvFile = jetpack.read(".env.testnet.template");
    const buf = Buffer.from(testnetEnvFile);
    const parsed = dotenv.parse(buf);
    env = {
      ADMIN_ADDRESS: testnet.account,
      FLOW_PRIVATE_KEY: result.private,
      FLOW_PUBLIC_KEY: result.public
    };
    jetpack.file(".env.testnet", {
      content: `${convertToEnv({ ...parsed, ...env })}`
    });
    spinner.info(
      `Testnet envronment config was written to: .env.testnet${"\n"}`
    );
    dotenv.config({
      path: requireEnv(process.env.CHAIN_ENV)
    });
  }
  // ------------------------------------------------------------
  // ---------------- CONTRACT DEPLOYMENT -----------------------
  async function deployAndInitialize() {
    spinner.start(
      `Deploying contracts to:  ${process.env.ADMIN_ADDRESS} (${process.env.CHAIN_ENV})`
    );
    const { stdout: out1, stderr: err1 } = await exec(
      `${deploy(process.env.CHAIN_ENV)}`,
      { cwd: process.cwd() }
    );
    if (err1) {
      throw new Error(err1);
    }
    spinner.succeed(chalk.greenBright("Contracts deployed"));
    spinner.info(
      `Contracts were deployed to: ${process.env.ADMIN_ADDRESS} (${
        process.env.CHAIN_ENV
      })${"\n"}`
    );
    spinner.start(
      `Initializing admin account: ${process.env.ADMIN_ADDRESS} (${process.env.CHAIN_ENV})`
    );
    // -------------- Initialize Kitty Items  --------------------------
    const { stderr: err2 } = await exec(
      initializeKittyItems(process.env.CHAIN_ENV),
      { cwd: process.cwd() }
    );
    if (err2) {
      throw new Error(err2);
    }
    // -------------- Initialize NFTStorefront --------------------------
    const { stderr: err3 } = await exec(
      initializeStorefront(process.env.CHAIN_ENV),
      { cwd: process.cwd() }
    );
    if (err3) {
      throw new Error(err3);
    }
    spinner.succeed(chalk.greenBright("Admin account initialized"));
    spinner.info(
      `${chalk.cyanBright(
        "./cadence/transactions/nftStorefront/setup_account.cdc"
      )} was executed successfully.`
    );
    spinner.info(
      `${chalk.cyanBright(
        "./cadence/transactions/kittyItems/setup_account.cdc"
      )} was executed successfully.${"\n"}`
    );
  }
  // ------------------------------------------------------------
  // ------------- EMULATOR ENVIRONMENT STARTUP -----------------
  if (process.env.CHAIN_ENV === "emulator") {
    spinner.start("Emulating Flow Network");
    await runProcess({
      name: "emulator",
      script: "flow",
      args: "emulator",
      wait_ready: true
    });
    spinner.succeed(chalk.greenBright("Emulator started"));
    spinner.info(
      `Flow Emulator is running at: ${chalk.yellow("http://localhost:8080")}`
    );
    spinner.info(
      `View log output: ${chalk.cyanBright("npx pm2 logs emulator")}${"\n"}`
    );
    spinner.start("Starting FCL Developer Wallet");
    await runProcess({
      name: "dev-wallet",
      script: "flow",
      args: "dev-wallet",
      wait_ready: true
    });
    spinner.succeed(chalk.greenBright("Developer Wallet started"));
    spinner.info(
      `FCL Dev Wallet running at: ${chalk.yellow("http://localhost:8701")}`
    );
    spinner.info(
      `View log output: ${chalk.cyanBright("npx pm2 logs dev-wallet")}${"\n"}`
    );
    // NOTE: Emulator development does not persist chain state by default.
    // If you add support for emulator persistence, you will need to remove this
    // because now your emulator will maintain all events from past runs,
    // emitted by the Kitty Items contract, and the sale offers will match
    // with what is represented on-chain (what NFTs are for sale in which accounts).
    jetpack.remove("./api/kitty-items-db-emulator.sqlite");
    dotenv.config({
      path: requireEnv(process.env.CHAIN_ENV)
    });
    await deployAndInitialize();
  }
  // ------------------------------------------------------------
  // ------------- TESTNET ENVIRONMENT STARTUP ------------------
  if (process.env.CHAIN_ENV === "testnet") {
    const USE_EXISTING = jetpack.exists(".env.testnet");
    if (!USE_EXISTING) {
      await cleanupTestnetConfig();
      await bootstrapNewTestnetAccount();
      await deployAndInitialize();
    } else {
      let useExisting = await inquirer.prompt({
        type: "confirm",
        name: "confirm",
        message: `Use existing tesnet credentials in ${chalk.greenBright(
          "env.testnet"
        )} ?`,
        default: true
      });
      if (!useExisting.confirm) {
        spinner.warn("Creating new testnet account credentials...");
        await cleanupTestnetConfig();
        await bootstrapNewTestnetAccount();
        await deployAndInitialize();
      } else {
        dotenv.config({
          path: requireEnv(process.env.CHAIN_ENV)
        });
      }
    }
  }
  // ------------------------------------------------------------
  // --------------------- SERVICES STARTUP ---------------------
  if (!process.env.ADMIN_ADDRESS) adminError();
  spinner.start("Starting API server");
  await runProcess({
    name: `api`,
    cwd: "./api",
    script: "npm",
    args: "run dev",
    watch: false,
    wait_ready: true
  });
  spinner.succeed(chalk.greenBright("API server started"));
  spinner.info(
    `Kitty Items API is running at: ${chalk.yellow("http://localhost:3000")}`
  );
  spinner.info(
    `View log output: ${chalk.cyanBright(`npx pm2 logs api`)}${"\n"}`
  );
  spinner.start("Starting storefront web app");
  await runProcess({
    name: `web`,
    cwd: "./web",
    script: "npm",
    args: "run dev",
    watch: false,
    wait_ready: true,
    autorestart: false
  });
  spinner.succeed(chalk.greenBright("Storefront web app started"));
  spinner.info(
    `Kitty Items Web App is running at: ${chalk.yellow(
      "http://localhost:3001"
    )}`
  );
  spinner.info(
    `View log output: ${chalk.cyanBright(`npx pm2 logs web`)}${"\n"}`
  );
  // ------------------------------------------------------------
  // --------------------- DONE -------------------------------
  const rainbow = chalkAnimation.rainbow("KITTY ITEMS HAS STARTED");
  setTimeout(async () => {
    rainbow.stop();
    console.log("\n");
    console.log(`${chalk.cyanBright("Visit")}: http://localhost:3001`);
    console.log("\n");
    if (process.env.CHAIN_ENV !== "emulator") {
      console.log(
        `${chalk.cyanBright(
          `View your account and transactions here:${"\n"}`
        )}https://${
          process.env.CHAIN_ENV === "testnet" ? "testnet." : ""
        }flowscan.org/account/${process.env.ADMIN_ADDRESS}\n`
      );
      console.log(
        `${chalk.cyanBright(
          `Explore your account here:${"\n"}`
        )}https://flow-view-source.com/${process.env.CHAIN_ENV}/account/${
          process.env.ADMIN_ADDRESS
        }\n`
      );
    }
    let logs = await inquirer.prompt({
      type: "confirm",
      name: "confirm",
      message: `Would you like to view the logs for all processes?`,
      default: true
    });
    if (logs.confirm) {
      console.log("\n");
      const ps = spawn("npx", ["pm2", "logs", "--no-daemon"], {
        shell: true,
        stdio: "inherit"
      });
      ps.stdout?.on("data", (data) => {
        console.log(data.toString().trim());
      });
      process.on("SIGINT", () => {
        process.exit(0);
      }); // CTRL+C
    } else {
      spinner.info(
        `View log output for all processes: ${chalk.cyanBright(
          `npx pm2 logs`
        )}${"\n"}`
      );
    }
    pm2.disconnect();
    spinner.stop();
  }, 3000);
  //////////////////////////////////////////////////////////////////
  // ------------- END PROCESS MANAGEMENT ENTRYPOINT ---------------
  //////////////////////////////////////////////////////////////////
});