/**
 * This Updatable entity is meant to handle the updating of Vizality's plugins and themes
 * and even Vizality itself.
 * @todo Finish writing this.
 * @module Updatable
 * @memberof Entities
 * @namespace Entities.Updatable
 */

import { getConfig, statusMatrix } from 'isomorphic-git';
import { getCaller } from '@vizality/util/file';
import http from 'isomorphic-git/http/node';
import { promisify } from 'util';
import { existsSync } from 'fs';
import cp from 'child_process';
import Events from 'events';
import { join } from 'path';
import fs from 'fs';

const exec = promisify(cp.exec);

/**
 * @extends Events
 */
export default class Updatable extends Events {
  /**
   * @property {string} dir Directory path of entity to update
   * @property {string} addonId Addon ID
   * @property {string} updateIdentifier Entity update identifier
   */
  constructor (dir, addonId, updateIdentifier) {
    super();
    this.dir = dir;
    /**
     * The addonId might be pre-defined by the plugin manager.
     */
    if (!this.addonId) {
    // It might be pre-defined by plugin manager
      this.addonId = addonId;
    }
    this.path = join(this.dir, this.addonId);
    if (!updateIdentifier) {
      updateIdentifier = `${getCaller(this.path)?.type}_${this.addonId}`;
    }
    this._updateIdentifier = updateIdentifier;
  }

  /**
   * 
   */
  get _cwd () {
    return { cwd: this.path };
  }

  /**
   * @returns {boolean} Whether this can be updated or not
   */
  isUpdatable () {
    return existsSync(join(this.dir, this.addonId, '.git'));
  }

  /**
   * 
   * @private
   * @returns {Promise<boolean>}
   */
  async _checkForUpdates () {
    try {
      await exec('git fetch', this._cwd);
      const gitStatus = await exec('git status -uno', this._cwd).then(({ stdout }) => stdout.toString());
      return gitStatus.includes('git pull');
    } catch (err) {
      return false;
    }
  }

  /**
   * 
   * @private
   * @returns {Promise<Array>}
   */
  async _getUpdateCommits () {
    const branch = await this.getBranch();
    const commits = [];
    const gitLog = await exec(`git log --format="%H -- %an -- %s" ..origin/${branch}`, this._cwd)
      .then(({ stdout }) => stdout.toString());
    const lines = gitLog.split('\n');
    lines.pop();
    lines.forEach(line => {
      const data = line.split(' -- ');
      commits.push({
        id: data.shift(),
        author: data.shift(),
        message: data.shift()
      });
    });
    return commits;
  }

  /**
   * 
   * @private
   * @param {boolean} [force=false] 
   * @returns {Promise<boolean>}
   */
  async _update (force = false) {
    try {
      let command = 'git pull --ff-only';
      if (force) {
        const branch = await this.getBranch();
        command = `git reset --hard origin/${branch}`;
      }
      await exec(command, this._cwd).then(({ stdout }) => stdout.toString());
      return true;
    } catch (err) {
      return false;
    }
  }

  /**
   * Fetches the git repository URL for an entity.
   * @returns {Promise<string|void>}
   */
  async getGitRepo () {
    try {
      if (!this.path) {
        return;
      }
      const repoUrl = await getConfig({
        fs,
        dir: this.path,
        path: 'remote.origin.url'
      });
      return repoUrl;
    } catch (err) {
      if (this.path) {
        return this.warn('Failed to fetch git origin URL. Remote updates will be unavailable!');
      }
      return this.error(err);
    }
  }

  /**
   * Fetches the current git branch for an entity.
   * @returns {Promise<string|null>}
   */
  getBranch () {
    return exec('git branch', this._cwd)
      .then(({ stdout }) =>
        stdout.toString().split('\n').find(l => l.startsWith('*')).slice(2).trim()
      );
  }
}
