import * as plugins from './meta.plugins.js';
import * as paths from '../paths.js';
import * as interfaces from './meta.interfaces.js';

import { logger } from '../gitzone.logging.js';

export class Meta {
  public cwd: string;
  public dirName: string;
  public filePaths: {
    metaJson: string;
    gitIgnore: string;
    packageJson: string;
  };

  constructor(cwdArg: string) {
    this.cwd = cwdArg;
    this.dirName = plugins.path.basename(this.cwd);
    this.filePaths = {
      metaJson: plugins.path.join(this.cwd, './.meta.json'),
      gitIgnore: plugins.path.join(this.cwd, './.gitignore'),
      packageJson: plugins.path.join(this.cwd, './package.json'),
    };
  }

  /**
   * the meta repo data
   */
  public metaRepoData: interfaces.IMetaRepoData;
  public smartshellInstance = new plugins.smartshell.Smartshell({
    executor: 'bash',
  });

  /**
   * sorts the metaRepoData
   */
  public async sortMetaRepoData() {
    const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []);
    this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
  }

  /**
   * reads the meta file from disk
   */
  public async readDirectory() {
    await this.syncToRemote(true);
    logger.log('info', `reading directory`);
    const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson);
    if (!metaFileExists) {
      throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
    }
    this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson);
  }

  /**
   * generates the gitignore file and stores it on disk
   */
  public async generateGitignore(): Promise<string> {
    await this.sortMetaRepoData();
    let gitignoreString = `# ignored repo directories\n`;
    gitignoreString += `.nogit/\n`;
    gitignoreString += `.pnpm-store/\n`;
    for (const key of Object.keys(this.metaRepoData.projects)) {
      gitignoreString = `${gitignoreString}${key}\n`;
    }
    return gitignoreString;
  }

  /**
   * write to disk
   */
  public async writeToDisk() {
    // write .meta.json to disk
    plugins.smartfile.memory.toFsSync(
      JSON.stringify(this.metaRepoData, null, 2),
      this.filePaths.metaJson,
    );
    // write .gitignore to disk
    plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore);
  }

  /**
   * push to remote
   */
  public async syncToRemote(gitCleanArg = false) {
    logger.log('info', `syncing from origin master`);
    await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`);
    if (gitCleanArg) {
      logger.log('info', `cleaning the repository from old directories`);
      await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
    }
    logger.log('info', `syncing  to remote origin master`);
    await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`);
  }

  /**
   * update the locally cloned repositories
   */
  public async updateLocalRepos() {
    await this.syncToRemote();
    const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects;
    const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd);
    for (const preExistingFolderArg of preExistingFolders) {
      if (
        preExistingFolderArg !== '.git' &&
        !Object.keys(projects).find((projectFolder) =>
          projectFolder.startsWith(preExistingFolderArg),
        )
      ) {
        const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation(
          `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
          true,
        );
        if (response) {
          logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
        } else {
          logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`);
        }
      }
    }

    await this.readDirectory();
    await this.sortMetaRepoData();
    const missingRepos: string[] = [];
    for (const key of Object.keys(this.metaRepoData.projects)) {
      plugins.smartfile.fs.isDirectory(key)
        ? logger.log('ok', `${key} -> is already cloned`)
        : missingRepos.push(key);
    }

    logger.log('info', `found ${missingRepos.length} missing repos`);
    for (const missingRepo of missingRepos) {
      await this.smartshellInstance.exec(
        `cd ${this.cwd} && git clone ${this.metaRepoData.projects[missingRepo]} ${missingRepo}`,
      );
    }

    logger.log('info', `write changes to disk`);
    await this.writeToDisk();

    logger.log('info', `persist changes with a git commit`);
    await this.smartshellInstance.exec(
      `cd ${this.cwd} && git add -A && git commit -m "updated structure"`,
    );
    await this.syncToRemote();

    // go recursive
    const folders = await plugins.smartfile.fs.listFolders(this.cwd);
    const childMetaRepositories: string[] = [];
    for (const folder of folders) {
      logger.log('info', folder);
    }
    console.log('Recursion still needs to be implemented');
  }

  // project manipulation

  /**
   * init a new meta project
   */
  public async initProject() {
    await this.syncToRemote(true);
    const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson);
    if (!fileExists) {
      await plugins.smartfile.memory.toFs(
        JSON.stringify({
          projects: {},
        }),
        this.filePaths.metaJson,
      );
      logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`);
      await plugins.smartfile.memory.toFs(
        JSON.stringify({
          name: this.dirName,
          version: '1.0.0',
        }),
        this.filePaths.packageJson,
      );
      logger.log(`success`, `created a new package.json in directory ${this.cwd}`);
    } else {
      logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`);
    }
    await this.smartshellInstance.exec(
      `cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
    );
    await this.updateLocalRepos();
  }

  /**
   * adds a project
   */
  public async addProject(projectNameArg: string, gitUrlArg) {
    await this.readDirectory();

    const existingProject = this.metaRepoData.projects[projectNameArg];

    if (existingProject) {
      throw new Error('Project already exists! Please remove it first before adding it again.');
    }

    this.metaRepoData.projects[projectNameArg] = gitUrlArg;

    await this.sortMetaRepoData();
    await this.writeToDisk();
    await this.smartshellInstance.exec(
      `cd ${this.cwd} && git add -A && git commit -m "feat(project): add ${projectNameArg}"`,
    );
    await this.updateLocalRepos();
  }

  /**
   * removes a project
   */
  public async removeProject(projectNameArg: string) {
    await this.readDirectory();

    const existingProject = this.metaRepoData.projects[projectNameArg];

    if (!existingProject) {
      logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`);
      return;
    }

    delete this.metaRepoData.projects[projectNameArg];

    logger.log('info', 'removing project from .meta.json');
    await this.sortMetaRepoData();
    await this.writeToDisk();

    logger.log('info', 'removing directory from cwd');
    await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg));
    await this.updateLocalRepos();
  }
}