import { Octokit, App } from "octokit";

import fetch from "node-fetch";
import fs from "fs";

import {
  getGitContentJsonDec,
  upsertGitContent,
} from "@browsercapturesalt/config/server/utils";

export { fetch };

import { startServer } from "./server";
import { setTransitionHooks, toHandlers } from "vue";

const argv = require("minimist")(process.argv.slice(2));

const API_BASE_URL = "https://api.heroku.com";
const DEFAULT_ACCEPT = "application/vnd.heroku+json; version=3";

const MAX_APPS = 5;

const APP_CONF = JSON.parse(fs.readFileSync("appconf.json").toString());

const DEFAULT_APP_NAME = APP_CONF.defaultApp || "appmandummyapp";

const LOCAL_CONFIG = {};

if (APP_CONF.config) {
  for (const key of APP_CONF.config) LOCAL_CONFIG[key] = process.env[key];
}

function getAppConf(name) {
  return APP_CONF.apps[name];
}

let VERBOSE = false;

function log(...args) {
  if (VERBOSE) syslog(...args);
}

let syslog = (...args) => {
  console.log(...args);
};

export function setSyslog(syslogFunc: any) {
  syslog = syslogFunc;
}

export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const MONTH = 30 * DAY;
export const WEEK = 7 * DAY;
export const YEAR = 365 * DAY;

export function formatDurationMs(dur: number) {
  if (dur < SECOND) return `${dur} millisecond(s)`;
  if (dur < MINUTE) return `${Math.floor((dur / SECOND) * 10) / 10} second(s)`;
  if (dur < HOUR) return `${Math.floor((dur / MINUTE) * 10) / 10} minute(s)`;
  if (dur < DAY) return `${Math.floor((dur / HOUR) * 10) / 10} hour(s)`;
  if (dur < WEEK) return `${Math.floor((dur / DAY) * 10) / 10} day(s)`;
  if (dur < MONTH) return `${Math.floor((dur / WEEK) * 10) / 10} week(s)`;
  if (dur < YEAR) return `${Math.floor((dur / MONTH) * 10) / 10} month(s)`;
  return `${Math.floor((dur / YEAR) * 10) / 10} year(s)`;
}

export function formatDuration(sec: number) {
  return formatDurationMs(sec * SECOND);
}

export function getConfig() {
  return getGitContentJsonDec("config");
}

//////////////////////////////////////////////////////////////////////
// API primitives

export function api(endpoint, method, payload, token, accept?) {
  const url = `${API_BASE_URL}/${endpoint}`;

  const headers = {
    Authorization: `Bearer ${token}`,
    Accept: accept || DEFAULT_ACCEPT,
    "Content-Type": "application/json",
  };

  const body = payload ? JSON.stringify(payload) : undefined;

  if (require.main === module) {
    log({ endpoint, method, url, headers, payload, token, body });
  }

  return new Promise((resolve, reject) => {
    fetch(url, {
      method,
      headers,
      body,
    }).then(
      (resp) =>
        resp.json().then(
          (json) => {
            if (VERBOSE)
              fs.writeFileSync(
                `responsesamples/${endpoint.replace(/\//g, "_")}.json`,
                JSON.stringify(json, null, 2)
              );
            resolve(json);
          },
          (err) => {
            syslog("ERROR", err);
            reject(err);
          }
        ),
      (err) => {
        syslog("ERROR", err);
        reject(err);
      }
    );
  });
}

function get(endpoint, payload, token, accept?) {
  return api(endpoint, "GET", payload, token, accept);
}

function post(endpoint, payload, token, accept?) {
  return api(endpoint, "POST", payload, token, accept);
}

function del(endpoint, payload, token, accept?) {
  return api(endpoint, "DELETE", payload, token, accept);
}

function patch(endpoint, payload, token, accept?) {
  return api(endpoint, "PATCH", payload, token, accept);
}

//////////////////////////////////////////////////////////////////////

function awaitCompletion(resolve, func, triesLeft, stepOpt?) {
  const step = stepOpt || 0;
  if (triesLeft <= 0) {
    resolve({ error: "timed out" });
  } else {
    syslog("awaiting step", step, "tries left", triesLeft);
    func().then((result: any) => {
      if (result.done) {
        resolve(result);
      } else {
        setTimeout(
          () => awaitCompletion(resolve, func, triesLeft - 1, step + 1),
          10000
        );
      }
    });
  }
}

//////////////////////////////////////////////////////////////////////

export type MigrationStrategy = "external" | "internal" | "disabled";
const DEFAULT_MIGRATION_STRATEGY = "external";
export type SelectionStrategy = "preferred" | "best" | "manual";
const DEFAULT_SELECTION_STRATEGY = "preferred";
export type SetConfigStrategy = "remote" | "fallback" | "local";
const DEFAULT_SET_CONFIG_STRATEGY = "fallback";
export type DeployStrategy = {
  migrationStrategy?: MigrationStrategy;
  selectionStrategy?: SelectionStrategy;
  setConfigStrategy?: SetConfigStrategy;
  deployTo?: string;
};

export type LogParams = {
  lines?: number;
  tail?: boolean;
};

export class HerokuApp {
  id: string = "";
  name: string = "";
  stack: string = "";
  region: string = "";
  quotaUsed: number = 0;
  parentAccount: HerokuAccount = new HerokuAccount("");
  serialize() {
    return {
      id: this.id,
      name: this.name,
      stack: this.stack,
      region: this.region,
      quotaUsed: this.quotaUsed,
    };
  }
  constructor(parentAccount: HerokuAccount, blob: any) {
    this.id = blob.id;
    this.name = blob.name;
    this.stack = blob.stack.name;
    this.region = blob.region.name;
    this.parentAccount = parentAccount;
  }
  getLogs(lpOpt?: LogParams) {
    const lp: LogParams = lpOpt || {
      lines: 500,
      tail: true,
    };
    return new Promise((resolve) => {
      post(`apps/${this.name}/log-sessions`, lp, this.parentAccount.token).then(
        (result: any) => {
          resolve(result);
        }
      );
    });
  }
  build(url) {
    syslog(
      "starting build",
      this.name,
      "at",
      this.parentAccount.name,
      "targz",
      url
    );
    return new Promise((resolve) => {
      post(
        `apps/${this.name}/builds`,
        {
          source_blob: {
            checksum: null,
            url,
            version: null,
          },
        },
        this.parentAccount.token
      ).then((result: any) => {
        resolve(result);
      });
    });
  }
  getBuild(id: string) {
    return new Promise((resolve) => {
      get(
        `apps/${this.name}/builds/${id}`,
        undefined,
        this.parentAccount.token
      ).then((result: any) => {
        resolve(result);
      });
    });
  }
  awaitBuild(url) {
    return new Promise((resolve) => {
      this.build(url).then((result: any) => {
        const id = result.id;

        awaitCompletion(
          resolve,
          () => {
            return new Promise((resolve) => {
              this.getBuild(id).then((result: any) => {
                if (result.status === "pending") {
                  resolve({ done: false });
                } else {
                  resolve({ done: result });
                }
              });
            });
          },
          30
        );
      });
    });
  }
  getBuilds() {
    return new Promise((resolve) => {
      get(`apps/${this.name}/builds`, undefined, this.parentAccount.token).then(
        (json) => {
          resolve(json);
        }
      );
    });
  }
  setConfig(configOpt?: any) {
    const config = configOpt || {};
    const numKeys = Object.keys(config).length;
    syslog("setting config", this.name, numKeys, "keys");
    return new Promise((resolve) => {
      patch(
        `apps/${this.name}/config-vars`,
        config,
        this.parentAccount.token
      ).then((result: any) => {
        if (typeof result === "object") {
          const setNumKeys = Object.keys(result).length;
          syslog(
            "set config result",
            this.name,
            setNumKeys,
            "keys",
            setNumKeys >= numKeys ? "ok" : "failed"
          );
        } else {
          syslog("ERROR", "set config result is not object", this.name, result);
        }
        resolve(result);
      });
    });
  }
  getConfig() {
    return new Promise((resolve) => {
      get(
        `apps/${this.name}/config-vars`,
        undefined,
        this.parentAccount.token
      ).then((result: any) => {
        resolve(result);
      });
    });
  }
  toString(pref: string) {
    return `${pref}HerokuApp < ${this.name} [ ${this.id} ] ${
      this.region
    } used: ${formatDuration(this.quotaUsed)} , remaining: ${formatDuration(
      this.parentAccount.quotaRemaining()
    )} >`;
  }
}

export type CreateAppParams = {
  name: string;
  region?: string;
  stack?: string;
};

export class HerokuAccount {
  name: string = "";
  envTokenFullName: string = "";
  token: string = "";
  id: string = "";
  quotaTotal: number = 0;
  quotaUsed: number = 0;
  apps: HerokuApp[] = [];
  serialize() {
    return {
      name: this.name,
      id: this.id,
      quotaTotal: this.quotaTotal,
      quotaUsed: this.quotaUsed,
      apps: this.apps.map((app) => app.serialize()),
    };
  }
  constructor(name: string) {
    this.name = name;
    this.envTokenFullName = "HEROKU_TOKEN_" + this.name;
    this.token = process.env[this.envTokenFullName];
  }
  createApp(cap: CreateAppParams) {
    syslog("creating app", cap, "at", this.name);
    return new Promise((resolve) => {
      post("apps", cap, this.token).then((result: any) => {
        if (result.id === "invalid_params") {
          syslog("could not create app", cap, result.message);
          resolve({
            error: "invalid params",
            message: result.message,
          });
        } else {
          syslog("created app", cap, "at", this.name, "id", result.id);
          resolve(result);
        }
      });
    });
  }
  deleteApp(name: string) {
    syslog("deleting app", name, "at", this.name);
    return new Promise((resolve) => {
      del(`apps/${name}`, undefined, this.token).then((result: any) => {
        if (result.id === "not_found") {
          syslog("ERROR", result.message);
          resolve({
            error: "not found",
            message: result.message,
          });
        } else {
          syslog("deleted", name, "at", this.name, "id", result.id);
          resolve(result);
        }
      });
    });
  }
  quotaRemaining() {
    return this.quotaTotal - this.quotaUsed;
  }
  toString(pref: string) {
    const apps = this.apps.length
      ? `\n${pref}  apps:\n${this.apps
          .map((app) => "    " + app.toString(pref))
          .join("\n")}`
      : "";
    return `${pref}HerokuAccount < ${this.name} [ ${this.id} , ${
      this.token
    } ]\n${pref}  used: ${formatDuration(
      this.quotaUsed
    )} , remaining: ${formatDuration(this.quotaRemaining())}${apps}\n${pref}>`;
  }
  getAccount() {
    return new Promise((resolve) => {
      get(`account`, undefined, this.token).then((json: any) => {
        this.id = json.id;
        resolve(json);
      });
    });
  }
  getQuota() {
    return new Promise((resolve) => {
      get(
        `accounts/${this.id}/actions/get-quota`,
        undefined,
        this.token,
        "application/vnd.heroku+json; version=3.account-quotas"
      ).then((json: any) => {
        resolve(json);
      });
    });
  }
  getAppById(id: string) {
    return this.apps.find((app) => app.id === id);
  }
  getApps() {
    return new Promise((resolve) => {
      get("apps", undefined, this.token).then((json: any) => {
        this.apps = json.map((app) => new HerokuApp(this, app));
        resolve(json);
      });
    });
  }
  init() {
    return new Promise(async (resolve) => {
      const account = await this.getAccount();
      const quota: any = await this.getQuota();
      const apps = await this.getApps();
      for (const qApp of quota.apps) {
        try {
          this.getAppById(qApp.app_uuid).quotaUsed = qApp.quota_used;
        } catch (err) {
          /*syslog(
            `quota app not among account apps`,
            qApp,
            this.apps.map((app) => app.id)
          );*/
        }
      }
      this.quotaTotal = quota.account_quota;
      this.quotaUsed = quota.quota_used;
      resolve({ account, quota, apps });
    });
  }
}

function getAllEnvTokens() {
  const envTokenKeys = Object.keys(process.env).filter((key) =>
    key.match(/^HEROKU_TOKEN_/)
  );
  const envTokens = envTokenKeys.map((key) => ({
    key,
    name: key.split("_")[2],
    token: process.env[key],
  }));
  return envTokens;
}

function getAllGitHubFullTokens() {
  const envTokenKeys = Object.keys(process.env).filter((key) =>
    key.match(/_GITHUB_TOKEN_FULL$/)
  );
  const envTokens = envTokenKeys.map((key) => ({
    key,
    envTokenName: key.split("_")[0],
    token: process.env[key],
  }));
  return envTokens;
}

class HerokuAppManager {
  accounts: HerokuAccount[] = [];
  serialize() {
    return {
      accounts: this.accounts.map((account) => account.serialize()),
    };
  }
  constructor() {}
  getAccountByName(name: string) {
    const acc = this.accounts.find((acc) => acc.name === name);
    return acc;
  }
  createApp(accountName: string, cap: CreateAppParams) {
    const acc = this.getAccountByName(accountName);
    return new Promise((resolve) => {
      if (acc) {
        acc.createApp(cap).then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such account",
        });
      }
    });
  }
  allApps() {
    const apps = this.accounts.map((acc) => acc.apps).flat();
    return apps;
  }
  getAppByName(name: string) {
    const app = this.allApps().find((app) => app.name === name);
    return app;
  }
  setConfig(nameOpt?: string, configOpt?: any) {
    const name = nameOpt || DEFAULT_APP_NAME;
    const app = this.getAppByName(name);
    return new Promise((resolve) => {
      if (app) {
        app.setConfig(configOpt).then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such app",
        });
      }
    });
  }
  getConfig(nameOpt?: string) {
    const name = nameOpt || DEFAULT_APP_NAME;
    const app = this.getAppByName(name);
    return new Promise((resolve) => {
      if (app) {
        app.getConfig().then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such app",
        });
      }
    });
  }
  getLogs(nameOpt?: string, lpOpt?: LogParams) {
    const name = nameOpt || DEFAULT_APP_NAME;
    const app = this.getAppByName(name);
    return new Promise((resolve) => {
      if (app) {
        app.getLogs(lpOpt).then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such app",
        });
      }
    });
  }
  getBuilds(nameOpt?: string) {
    const name = nameOpt || DEFAULT_APP_NAME;
    const app = this.getAppByName(name);
    return new Promise((resolve) => {
      if (app) {
        app.getBuilds().then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such app",
        });
      }
    });
  }
  getAccountByAppName(name: string) {
    const app = this.allApps().find((app) => app.name === name);
    if (app) {
      return app.parentAccount;
    } else {
      return undefined;
    }
  }
  deleteApp(name: string) {
    const acc = this.getAccountByAppName(name);
    return new Promise((resolve) => {
      if (acc) {
        acc.deleteApp(name).then((result: any) => {
          resolve(result);
        });
      } else {
        resolve({
          error: "no such app",
        });
      }
    });
  }
  toString() {
    return `HerokuAppManager <\n${this.accounts
      .map((acc) => acc.toString("  "))
      .join("\n")}\n>`;
  }
  init() {
    syslog("initial,bizing app manager");
    return new Promise(async (resolve) => {
      this.accounts = getAllEnvTokens().map(
        (token) => new HerokuAccount(token.name)
      );

      const initResult = await Promise.all(
        this.accounts.map((acc) => acc.init())
      );

      this.accounts.sort((a, b) => a.name.localeCompare(b.name));

      syslog(
        "initialized",
        initResult.length,
        "account(s)",
        this.allApps().length,
        "app(s)"
      );

      resolve(initResult);
    });
  }
  deployApp(nameOpt?: string, strategyOpt?: DeployStrategy) {
    const name = nameOpt || DEFAULT_APP_NAME;
    const strategy = strategyOpt || {};
    const migrationStrategy =
      strategy.migrationStrategy || DEFAULT_MIGRATION_STRATEGY;
    const selectionStrategy =
      strategy.selectionStrategy || DEFAULT_SELECTION_STRATEGY;
    const setConfigStrategy =
      strategy.setConfigStrategy || DEFAULT_SET_CONFIG_STRATEGY;
    syslog(
      "deploying app",
      name,
      migrationStrategy,
      selectionStrategy,
      setConfigStrategy
    );
    return new Promise(async (resolve) => {
      syslog("getting app conf", name);

      const appConf = getAppConf(name);

      if (!appConf) {
        resolve({ error: "no conf for app" });
        return;
      }

      syslog("getting targz url", name);

      const targzUrl = appConf.targzUrl;

      if (!targzUrl) {
        resolve({ error: "no targz url for app" });
        return;
      }

      syslog("getting deploy account", name);

      let account = strategy.deployTo;

      if (selectionStrategy === "preferred") {
        const preferredAccount = appConf.preferredAccount;

        if (!preferredAccount) {
          resolve({ error: "no preferred account for app" });
          return;
        }

        account = preferredAccount;
      } else if (selectionStrategy === "best") {
        const allowedAccountNames =
          appConf.allowedAccounts || this.accounts.map((acc) => acc.name);
        const allowedAccounts = this.accounts
          .filter((acc) =>
            allowedAccountNames.find((allowed) => acc.name === allowed)
          )
          .filter((acc) => acc.apps.length < MAX_APPS);
        const sortedAllowedAccounts = allowedAccounts.sort(
          (a, b) => b.quotaRemaining() - a.quotaRemaining()
        );
        if (!sortedAllowedAccounts.length) {
          resolve({ error: "no available account" });
          return;
        }
        account = sortedAllowedAccounts[0].name;
      }

      if (!account) {
        resolve({ error: "could not obtain deploy account" });
        return;
      }

      syslog("getting config", name);

      let config = LOCAL_CONFIG;

      if (setConfigStrategy !== "local") {
        const getConfigResult = await getConfig();

        const remoteConfig = getConfigResult.content;

        if (remoteConfig) {
          config = remoteConfig;
        } else {
          if (setConfigStrategy === "remote") {
            resolve({ error: "could not obtain remote config" });
            return;
          }
        }
      }

      syslog("migrating", name);

      const existingApp = this.allApps().find((app) => app.name === name);

      if (existingApp) {
        syslog("already exists", name);

        const existingAccountName = existingApp.parentAccount.name;

        if (existingAccountName !== account) {
          if (migrationStrategy === "disabled") {
            resolve({ error: "app exists on different account" });
            return;
          }

          if (migrationStrategy === "external") {
            const deleteAppResult: any = await this.deleteApp(name);
            syslog("delete", name, "result", deleteAppResult.id);
          } else {
            resolve({ error: "internal migration not implemented" });
            return;
          }
        }
      }

      syslog("creating", name);

      const createAppResult = await this.createApp(account, {
        name,
      });

      const initResult = await this.init();

      const app = this.getAppByName(name);

      if (!app) {
        resolve({ error: "could not create app" });
        return;
      }

      syslog("setting config", name);

      const setConfigResult = await this.setConfig(name, config);

      syslog("building", name);

      const awaitBuildResult: any = await app.awaitBuild(targzUrl);

      if (awaitBuildResult.done) {
        syslog("deployed", name, "status", awaitBuildResult.done.status);
      } else {
        syslog("ERROR", "deploy", name, "failed", awaitBuildResult.status);
      }

      resolve(awaitBuildResult);
    });
  }
}

class GitHubRepo {
  name: string = "";
  id: number = 0;
  fullName: string = "";
  private: boolean = false;
  repoUrl: string = "";
  stars: number = 0;
  forks: number = 0;
  createdAt: string = "";
  updatedAt: string = "";
  pushedAt: string = "";
  blob: any = {};

  constructor(blob: any) {
    this.name = blob.name;
    this.id = blob.id;
    this.fullName = blob.fullName;
    this.private = blob.private;
    this.repoUrl = blob.html_url;
    this.stars = blob.stargazers_count;
    this.forks = blob.forks;
    this.createdAt = blob.created_at;
    this.updatedAt = blob.updated_at;
    this.pushedAt = blob.pushed_at;
    this.blob = blob;
  }

  serialize() {
    return {
      name: this.name,
      id: this.id,
      fullName: this.fullName,
      private: this.private,
      repoUrl: this.repoUrl,
      stars: this.stars,
      forks: this.forks,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      pushedAt: this.pushedAt,
      //blob: this.blob,
    };
  }
}

type CreateRepoParams = {
  name: string;
  description?: string;
  init?: boolean;
  license?: string;
};

class GitHubAccount {
  envTokenName: string = "";
  envTokenFullName: string = "";
  token: string = "";
  gitUserName: string = "";
  id: string = "";
  avatarUrl: string = "";
  gitUrl: string = "";
  octokit: any = undefined;
  repos: GitHubRepo[] = [];

  constructor(envTokenName) {
    this.envTokenName = envTokenName;
    this.envTokenFullName = this.envTokenName + "_GITHUB_TOKEN_FULL";
    this.token = process.env[this.envTokenFullName];
  }

  createRepo(params: CreateRepoParams) {
    const name = params.name;
    const description = params.description || params.name;
    const auto_init = !!params.init;
    const license_template = params.init ? params.license || "MIT" : undefined;

    return new Promise((resolve) => {
      // https://octokit.github.io/rest.js/v18#repos-create-for-authenticated-user
      this.octokit.rest.repos
        .createForAuthenticatedUser({
          name,
          description,
          auto_init,
          license_template,
        })
        .then((result) => {
          resolve(result);
        })
        .catch((err) => {
          resolve({ error: err });
        });
    });
  }

  deleteRepo(name: string) {
    return new Promise((resolve) => {
      // https://octokit.github.io/rest.js/v18#repos-delete
      this.octokit.rest.repos
        .delete({
          owner: this.gitUserName,
          repo: name,
        })
        .then((result) => {
          resolve(result);
        })
        .catch((err) => {
          resolve({ error: err });
        });
    });
  }

  forkRepo(gitUserName: string, name: string) {
    return new Promise((resolve) => {
      // https://octokit.github.io/rest.js/v18#repos-create-fork
      this.octokit.rest.repos
        .createFork({
          owner: gitUserName,
          repo: name,
        })
        .then((result) => {
          resolve(result);
        })
        .catch((err) => {
          resolve({ error: err });
        });
    });
  }

  getRepos() {
    return new Promise((resolve) => {
      // https://octokit.github.io/rest.js/v18#repos-list-for-authenticated-user
      this.octokit.rest.repos
        .listForAuthenticatedUser({ per_page: 100 })
        .then((result) => {
          if (result.data) {
            this.repos = result.data.map((repo) => new GitHubRepo(repo));

            this.repos.sort((a, b) => a.name.localeCompare(b.name));

            resolve(this.repos.map((repo) => repo.serialize()));
          } else {
            syslog("ERROR", "could not get repos for", this.gitUserName);

            resolve([]);
          }
        })
        .catch((err) => {
          resolve([]);
        });
    });
  }

  init() {
    return new Promise((resolve) => {
      this.octokit = new Octokit({ auth: this.token });

      this.octokit.rest.users
        .getAuthenticated()
        .then((result) => {
          if (result.status === 200) {
            const acc = result.data;
            this.gitUserName = acc.login;
            this.id = acc.id;
            this.avatarUrl = acc.avatar_url;
            this.gitUrl = acc.html_url;
            this.getRepos().then((result) => {
              resolve(this.serialize());
            });
          } else {
            resolve({ error: result.status });
          }
        })
        .catch((err) => {
          resolve({ error: err });
        });
    });
  }

  serialize() {
    return {
      envTokenName: this.envTokenName,
      gitUserName: this.gitUserName,
      id: this.id,
      avatarUrl: this.avatarUrl,
      gitUrl: this.gitUrl,
      repos: this.repos.map((repo) => repo.serialize()),
    };
  }
}

class GitHubAccountManager {
  accounts: GitHubAccount[] = [];
  constructor() {}
  getAccountByGitUserName(gitUserName) {
    return this.accounts.find((acc) => acc.gitUserName === gitUserName);
  }
  serialize() {
    return {
      accounts: this.accounts.map((acc) => acc.serialize()),
    };
  }
  init() {
    syslog("initializing git manager");
    return new Promise(async (resolve) => {
      this.accounts = getAllGitHubFullTokens().map(
        (token) => new GitHubAccount(token.envTokenName)
      );

      const initResult = await Promise.all(
        this.accounts.map((acc) => acc.init())
      );

      this.accounts.sort((a, b) => a.gitUserName.localeCompare(b.gitUserName));

      syslog("initialized", initResult.length, "account(s)");

      resolve(initResult);
    });
  }
}

export const appMan = new HerokuAppManager();
export const gitMan = new GitHubAccountManager();

export function uploadTargz() {
  const targz = fs.readFileSync("repo.tar.gz");

  return new Promise((resolve) => {
    upsertGitContent("apptargz/herokuappman.tar.gz", targz).then((result) => {
      syslog("upload tar.gz status", result.status);

      resolve(result);
    });
  });
}

export function interpreter(argv) {
  const command = argv._[0];

  return new Promise(async (resolve) => {
    if (command === "config") {
      getConfig().then((result) => {
        const config = result.content;
        if (config) {
          const keys = Object.keys(config);
          APP_CONF["config"] = keys;
          fs.writeFileSync("appconf.json", JSON.stringify(APP_CONF, null, 2));
          syslog(keys);
          syslog("written", keys.length, "keys");
          resolve({ done: config });
        } else {
          syslog("ERROR", "could not obtain config", result);
          resolve({ error: result });
        }
      });

      return;
    }

    if (command === "serve") {
      startServer().then((result) => resolve(result));

      return;
    }

    const initResultAppMan = await appMan.init();
    const initResultGitMan = await gitMan.init();

    if (command === "deploy") {
      const name = argv.name;

      const strategy: DeployStrategy = {
        migrationStrategy: argv.migrate,
        selectionStrategy: argv.select,
        setConfigStrategy: argv.setconfig,
        deployTo: argv.deployto || "",
      };

      const deployResult = await appMan.deployApp(name, strategy);

      resolve(deployResult);

      return;
    }

    if (command === "delete") {
      const deleteResult = await appMan.deleteApp(argv.name);

      resolve(deleteResult);

      return;
    }

    if (command === "uploadtargz") {
      uploadTargz().then((result) => resolve(result));

      return;
    }

    if (command == "repos") {
      const acc = gitMan.getAccountByGitUserName(argv.user);

      if (acc) {
        const repos = await acc.getRepos();

        resolve(repos);
      } else {
        resolve({ error: "no such account", gitUserName: argv.user });
      }
    }

    if (command == "createrepo") {
      const acc = gitMan.getAccountByGitUserName(argv.user);

      if (acc) {
        const result = acc.createRepo({ name: argv.name, init: !!argv.init });

        resolve(result);
      } else {
        resolve({ error: "no such account", gitUserName: argv.user });
      }
    }

    if (command == "fork") {
      const acc = gitMan.getAccountByGitUserName(argv.user);

      if (acc) {
        const result = acc.forkRepo(argv.owner, argv.name);

        resolve(result);
      } else {
        resolve({ error: "no such account", gitUserName: argv.user });
      }
    }

    if (command == "deleterepo") {
      const acc = gitMan.getAccountByGitUserName(argv.user);

      if (acc) {
        const result = acc.deleteRepo(argv.name);

        resolve(result);
      } else {
        resolve({ error: "no such account", gitUserName: argv.user });
      }
    }

    resolve({ error: "unknown command", command });
  });
}

if (require.main === module) {
  interpreter(argv).then((result) => {
    console.log(result);
  });
}
