// @ts-check

const HTTP_AUTH_DOMAIN = ".acanto.net";
const HTTP_AUTH_USERNAME = "acanto";
const HTTP_AUTH_PASSWORD = "acanto";

/**
 * Get API client to comunicate with frontend deploy' hooks and scripts
 */
function getApiClient() {
  const https = require("https");
  const axios = require("axios").default;
  
  return axios.create({
    httpsAgent: new https.Agent({
      rejectUnauthorized: false,
    }),
  });
}

/**
 * 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 { normaliseUrl } = require("../utils");
  let url = normaliseUrl(process.env.APP_URL + "/").replace("https", "http");
  url += path;
  
  // with axios:
  // const log = require("fancy-log");
  // const api = getApiClient();
  // const config = {};
  // if (url.indexOf(HTTP_AUTH_DOMAIN) >= 0) {
  //   config.auth = {
  //     username: HTTP_AUTH_USERNAME,
  //     password: HTTP_AUTH_PASSWORD
  //   }
  // }
  // return api.get(url, config)
  //   .then((response) => {
  //     log(`Done calling ${action} script`, response.data);
  //   }, (response) => {
  //     log(`Failed to call ${action}`, response);
  //   });

  // alternative with curl:
  const { execSync } = require("child_process");
  url = url.replace(new RegExp(`\/\/(.+)\(${HTTP_AUTH_DOMAIN})\/`), `//${HTTP_AUTH_USERNAME}:${HTTP_AUTH_PASSWORD}@$1$2/`)
  execSync(`curl ${url}`);

  return Promise.resolve("curl");
}

/**
 * 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 } = 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 };
}

/**
 * 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} ./public/assets ${folder}/public/assets" || true`,
    { stdio: "inherit" }
  );
  
  log("Syncing storage folder...");
  execSync(`${cmdCommon} ./storage ${folder}/storage" || true`, {
    stdio: "inherit",
  });

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

  callback();
}

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

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

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

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

/**
 * The full command should be something like:
 *
 * ./fetchurls.sh --domain http://example.com -u username -p password --non-interactive  --filename crawled.txt
 *
 * @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 domain = removeTrailingSlashes(process.env.APP_URL);
  let args = ["--domain", domain, "--non-interactive"];

  if (domain.endsWith(HTTP_AUTH_DOMAIN)) {
    args = args.concat(["-u", HTTP_AUTH_USERNAME, "-p", HTTP_AUTH_PASSWORD]);
  }

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

  fetchurls.on("close", (code) => {
    log.info(
      fs.readFileSync(paths.join(paths.root, "fetchurls-log.txt"), {
        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,
    ciDeployerCopy,
    ciClean,
    ciTouch,
    ciSync,
    ciDeployerCall,
    ciHooks,
    ciCrawler
  )(callback);
}

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