// @ts-check

/**
 * Call deploy endpoint
 *
 * Use http to avoid issues on the runner such as this error encountered when
 * requesting `https` URLs:
 * `Failed to connect to techlab.acanto.net port 443: Connection refused`
 * We also make sure to add http authentication for `acanto.net` protected
 * endpoints
 *
 * @param {string} path
 * @param {string} action
 */
function callDeployEndpoint(path, action = "") {
  const log = require("fancy-log");
  const { normaliseUrl } = require("../utils");
  const { username, password, auth } = checkOptionsDefintions();
  const {} = process.env;
  let url = normaliseUrl(process.env.APP_URL + "/");
  // enforce http
  url = url.replace("https", "http");

  // add http authentication, values from the ci .yml file
  const [protocol, domain] = url.split("//");
  let authPrefix = "";

  if (auth === "http") {
    if (username && password) {
      authPrefix = `${username}:${password}@`;
    }
  }

  url = protocol + "//" + authPrefix + domain + path;

  // alternative with curl:
  const { execSync } = require("child_process");
  log(`Calling ${action} script at url: ${url}`);
  execSync(`curl ${url}`);
  log(`Done calling ${action} script`);
}

/**
 * Check that all deployment options are correctly set, in some occations we can
 * even guess a fallback value, in which case we log it, in others we just throw
 * a descriptive error and exit.
 */
function checkOptionsDefintions() {
  const log = require("fancy-log");
  const chalk = require("chalk");
  const { getCliOptionsMap } = require("../utils/cli");

  let {
    env,
    mode,
    folder,
    host,
    username,
    password,
    auth,
  } = getCliOptionsMap();

  if (!env) {
    env = guessEnvironment();

    if (env) {
      log.info(
        chalk.yellowBright.bold(
          `Missing '--env' option, assuming value '${env}' based on current branch.`
        )
      );
    } else {
      throw Error("Missing '--env myenv' option. Exiting...");
    }
  } else {
    // TODO: check that the end defined matches an `.env.{envName}` file?
  }

  if (!mode) {
    // throw Error("Missing '--mode ftp|ssh' option. Exiting...");
    mode = "ftp";

    log.info(
      chalk.yellowBright.bold(
        `Missing '--mode' option, assuming default value '${mode}'.`
      )
    );
  }

  if (mode === "ftp") {
    if (!host || !username || !password) {
      throw Error(
        "Missing one option between '--host', '--username' and '--password'. Exiting..."
      );
    }
  }

  if (!folder) {
    folder = process.env.APP_NAME;

    log.info(
      chalk.yellowBright.bold(
        `Missing '--folder' option, guessed fallback is '${folder}' based on APP_NAME.`
      )
    );
  }

  return { env, mode, folder, host, username, password, auth };
}

/**
 * Guess deploy environment based on standard branching model
 *
 * @return {"dev" | "staging" | "production" | false}
 */
function guessEnvironment() {
  const branch = process.env.CI_COMMIT_BRANCH;
  if (!branch) {
    return false;
  }

  const branchesMap = {
    master: "dev",
    stage: "staging",
    staging: "staging",
    prod: "production",
    production: "production",
  };

  return branchesMap[branch] || false;
}

/**
 * Check that we actually are in a CI environment, exit otherwise
 */
function ciCheck(callback) {
  const ci = require("ci-info");
  const log = require("fancy-log");

  if (ci.isCI) {
    log.info(`Running on CI server ${ci.name}`);
    checkOptionsDefintions();
    callback();
  } else {
    throw Error("This script is not running on a CI server. Exiting...");
    // process.exit();
  }
}

/**
 * Ci env and htaccess manager
 */
function ciEnvHtaccess(callback) {
  const fs = require("fs");
  const paths = require("../paths");
  const options = checkOptionsDefintions();
  const pathEnv = paths.join(paths.root, `.env.${options.env}`);
  const pathHtaccess = paths.join(
    paths.frontend.dest.public,
    `.htaccess.${options.env}`
  );

  // use the right .env.{env} file and copy it as .env
  if (fs.existsSync(pathEnv)) {
    fs.copyFileSync(pathEnv, paths.join(paths.root, ".env"));
  }

  // use the right .htaccess.{env} file and copy it as .htaccess
  if (fs.existsSync(pathHtaccess)) {
    fs.copyFileSync(
      pathHtaccess,
      paths.join(paths.frontend.dest.public, ".htaccess")
    );
  }

  // reload current env to be sure...
  require("dotenv").config();

  callback();
}

/**
 * Run composer to install vendor files
 */
function ciComposer(callback) {
  const { execSync } = require("child_process");

  execSync(
    "composer install --no-interaction --no-progress --no-dev --prefer-dist --ignore-platform-reqs"
  );

  // execSync("composer dump-autoload"); // done by laravel already

  callback();
}

/**
 * Zip the vendor folder
 */
function ciComposerZip(callback) {
  // require modules
  const fs = require("fs");
  const archiver = require("archiver");
  const log = require("fancy-log");
  const del = require("del");
  const paths = require("../paths");

  // create a file to stream archive data to.
  const output = fs.createWriteStream(paths.join(paths.root, "vendor.zip"));
  const archive = archiver("zip", { zlib: { level: 9 } });

  output.on("close", function () {
    // now delete the vendor folder so that it does not get synced to the server
    del(paths.join(paths.root, "vendor"));

    // succesfull message
    log("Created vendor.zip and deleted vendor folder");
    callback();
  });

  output.on("end", function () {
    log("vendor.zip: data has been drained");
  });

  archive.on("warning", function (err) {
    if (err.code === "ENOENT") {
    } else {
      throw err;
    }
  });

  archive.on("error", function (err) {
    throw err;
  });

  archive.directory(paths.join(paths.root, "vendor/"), false);

  archive.pipe(output);
  archive.finalize();
}

/**
 * Clean files that should not end up on the server
 */
function ciClean() {
  const del = require("del");
  const paths = require("../paths");

  return del(
    [
      paths.join(paths.root, ".env.*"),
      paths.join(paths.frontend.dest.public, ".htaccess.*"),
      paths.join(paths.root, "package.json"),
      paths.join(paths.root, "package-lock.json"),
      paths.join(paths.root, "composer.lock"),
      paths.join(paths.root, "tsconfig.json"),
      paths.join(paths.root, "*.md"),
      paths.join(paths.root, ".vscode"),
      paths.join(paths.frontend.src.base),
    ],
    { force: true, dot: true }
  );
}

/**
 * Touch files in order to change their timestamps and force lftp to update them
 * despite having the same byte length.
 */
function ciTouch() {
  const paths = require("../paths");
  const gulp = require("gulp");
  const touchCmd = require("gulp-touch-fd");

  return (
    gulp
      .src(paths.join(paths.frontend.dest.base, "/**/*"), {
        base: paths.frontend.dest.base,
        dot: true, // htaccess too
      })
      .pipe(gulp.dest(paths.frontend.dest.base))
      // @ts-ignore
      .pipe(touchCmd())
  );
}

/**
 * Sync files, either via ftp or ssh (TODO: when we'll need it)
 *
 * all commands are synchronous here, just run them one by one
 */
function ciSync(callback) {
  const { mode } = checkOptionsDefintions();

  if (mode === "ftp") {
    ciSyncFtp(callback);
  } else {
    throw new Error("deploy script supports only `ftp` at the moment");
  }
}

/**
 * Sync files, either via ftp or ssh (TODO: when we'll need it)
 *
 * all commands are synchronous here, just run them one by one
 */
function ciSyncFtp(callback) {
  const { execSync } = require("child_process");
  const log = require("fancy-log");
  const { removeTrailingSlashes } = require("../utils");
  let { username, password, host, folder } = checkOptionsDefintions();
  const cmdPrefx = `lftp -c "set ftp:ssl-allow no; open -u`;
  const cmdDefaults = `mirror --reverse --parallel=50`;
  const cmdCommon = `${cmdPrefx} ${username},${password} ${host}; ${cmdDefaults}`;
  folder = removeTrailingSlashes(folder);

  // selective incremental mirror (without delete, for long cached files)
  log("Syncing public assets folder...");
  execSync(
    `${cmdCommon} --only-newer ./public/assets ${folder}/public/assets" || true`
  );

  log("Syncing storage folder...");
  execSync(`${cmdCommon} ./storage ${folder}/storage" || true`);

  // selective excluding mirror (with delete)
  log("Syncing all the rest...");
  execSync(
    `${cmdCommon} --ignore-time --delete --exclude-glob=.git* --exclude=.git/ --exclude=.npm/ --exclude=node_modules/ --exclude=vendor/ --exclude=public/assets/ --exclude=storage/ ./ ${folder}/" || true`
  );

  callback();
}

/**
 * Copy deployer script in public folder so that it will be uploaded
 */
function ciCopyScripts() {
  const gulp = require("gulp");
  const paths = require("../paths");

  return gulp
    .src([
      paths.join(paths.self.scripts, "deploy/deployer.php"),
    ])
    .pipe(gulp.dest(paths.frontend.dest.public));
}

/**
 * Call deployer script temporarily put on server
 */
function ciDeployer(callback) {
  callDeployEndpoint("deployer.php", "deployer");
  callback();
}

/**
 * Call laravel-frontend exposed hook
 */
function ciHooks(callback) {
  callDeployEndpoint("_/hooks/deploy/end/", "deploy end hook");
  callback();
}

/**
 * The full command should be something like:
 *
 * ./fetchurls.sh --non-interactive --domain http://example.com
 *
 * @see https://github.com/adamdehaven/fetchurls
 */
function ciCrawler(callback) {
  const fs = require("fs");
  const spawn = require("cross-spawn");
  const log = require("fancy-log");
  const paths = require("../paths");
  const { removeTrailingSlashes } = require("../utils");
  const { username, password, auth } = checkOptionsDefintions();
  const logDestination = paths.join(paths.root, "fetchurls-log.txt");
  const domain = removeTrailingSlashes(process.env.APP_URL);
  let args = ["--domain", domain, "--non-interactive"];

  if (auth === "http") {
    if (username && password) {
      args = args.concat(["-u", username, "-p", password]);
    }
  }

  const fetchurls = spawn(
    paths.join(paths.self.nodeModule, "scripts/fetchurls.sh"),
    args,
    {
      stdio: "inherit",
    }
  );

  fetchurls.on("close", (code) => {
    if (fs.existsSync(logDestination))
      log.info(
        fs.readFileSync(logDestination, {
          encoding: "utf8",
        })
      );
    callback();
  });
}

/**
 * Collect subtasks in a single task that respect the gulp async lifecycle by
 * calling the callback on completion.
 */
function ciPre(callback) {
  const gulp = require("gulp");

  return gulp.series(ciCheck, ciEnvHtaccess)(callback);
}

/**
 * Collect subtasks in a single task that respect the gulp async lifecycle by
 * calling the callback on completion.
 */
function ciPost(callback) {
  const gulp = require("gulp");

  return gulp.series(
    ciComposer,
    ciComposerZip,
    ciCopyScripts,
    ciClean,
    ciTouch,
    ciSync,
    ciDeployer,
    ciHooks,
    ciCrawler
  )(callback);
}

exports.ciPre = ciPre;
exports.ciPost = ciPost;
