import {
  readdir as fsReaddir,
  access as fsAccess,
  readFile as fsReadFile,
  writeFile as fsWriteFile,
  rename as fsRename
} from 'fs';
import { readdir as fspReaddir } from 'fs/promises';
import { promisify } from 'util';
import { isAbsolute, resolve, join } from 'path';

import * as tarJs from 'tar';
import * as mkdirpJs from 'mkdirp';
import { rimraf as rimrafJs } from 'rimraf';

import { Spawner, Cmd } from './types';
import { SpawnerBase } from './spawner-base';
import { GnuSpawner } from './gnu-spawner';
import pump = require('pump');
import { Readable } from 'node:stream';

export function JsSpawner(context: { path?: string } = {}): Spawner {
  const gnuSpawner = GnuSpawner({ resolvePath, ...SpawnerBase(translate) });
  return {
    ...gnuSpawner,
    resolvePath,
    readdir,
    async readFile(path) {
      return await promisify(fsReadFile)(resolvePath(path), {
        encoding: 'utf8'
      });
    },
    async writeFile(path, data) {
      await promisify(fsWriteFile)(resolvePath(path), data, {
        encoding: 'utf8'
      });
    },
    mkdirp,
    rimraf,
    tar,
    walk,
    async rename(oldPath, newPath) {
      await promisify(fsRename)(resolvePath(oldPath), resolvePath(newPath));
    },
    async untar(input, cwd) {
      await new Promise<void>((resolve, reject) => {
        const writable = tarJs.extract({ cwd: resolvePath(cwd) });
        pump(input, writable, (err) => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        });
      });
    },
    exists
  };

  function resolvePath(...paths: (string | undefined)[]) {
    return resolve(context.path ?? '', ...paths.map((path) => path ?? ''));
  }

  function translate(cmd: Cmd) {
    if (cmd.superuser) {
      throw new Error(
        `${JsSpawner.name} does not support cmd option "superuser"`
      );
    }
    const translated: Cmd = {
      ...cmd
    };

    if (cmd.cwd) {
      translated.cwd = resolvePath(cmd.cwd);
    }

    return translated;
  }

  async function mkdirp(path = '') {
    await mkdirpJs(resolvePath(path));
  }

  async function rimraf(path = '') {
    await rimrafJs(resolvePath(path));
  }

  function readdir(path = '') {
    return promisify(fsReaddir)(resolvePath(path));
  }

  async function tar(...paths: string[]) {
    for (const path in paths) {
      if (isAbsolute(path)) {
        throw new Error('Paths passed to spawner.tar must not be absolute');
      }
    }
    const packageStream = tarJs.create(
      { sync: true, gzip: true, cwd: resolvePath() },
      paths
    ) as unknown as Readable;
    // ^^ The @types for tar.create are not correct
    return packageStream;
  }

  async function exists(path: string) {
    if (!path) {
      throw new Error('path is mandatory');
    }
    try {
      await promisify(fsAccess)(resolvePath(path));
      return true;
    } catch (ex) {
      return false;
    }
  }

  async function walk(dir?: string): Promise<string[]> {
    const relPath = dir ?? '';
    const path = resolvePath(relPath);
    const dirPromises: Promise<string[]>[] = [];
    const fileResults = await new Promise(
      (
        _resolve: (results: string[]) => void,
        _reject: (err: Error) => void
      ) => {
        fspReaddir(path, { withFileTypes: true })
          .then((files) => {
            const nonDirFiles: string[] = [];
            files.forEach((file) => {
              const filePath = join(relPath, file.name);
              if (file.isDirectory()) {
                dirPromises.push(walk(filePath));
              } else {
                nonDirFiles.push(filePath);
              }
            });
            _resolve(nonDirFiles);
          })
          .catch((e: Error) => _reject(e));
      }
    );
    const dirResults = await Promise.all(dirPromises);
    return dirResults.flat().concat(fileResults);
  }
}
