import { createWriteStream, mkdirSync } from "fs";
import { PassThrough, Transform } from "stream";
import { stripVTControlCharacters } from "util";
import chalk from "chalk";
import logUpdate from "log-update";
import { dateToStr } from "./dateToStr";
import { prependDate } from "./prependDate";
import { stripPassword } from "./stripPassword";

/** The directory to write the data and log files to. */
export const TMP_DIR = `/tmp/pg-microsharding.${process.getuid!()}`;
mkdirSync(TMP_DIR, { recursive: true, mode: 0o700 });

/**
 * A helper class to easier mock output in unit tests.
 */
class StdWritable extends PassThrough {
  constructor(private std: NodeJS.WriteStream) {
    super();
    this.pipe(this.std);

    if (!this.columns && parseInt(process.env["COLUMNS"] ?? "")) {
      this.columns = parseInt(process.env["COLUMNS"]!);
    }

    if (!this.rows && parseInt(process.env["ROWS"] ?? "")) {
      this.rows = parseInt(process.env["ROWS"]!);
    }
  }

  get rows(): number {
    return this.std.rows;
  }

  get columns(): number {
    return this.std.columns;
  }

  set rows(rows: number) {
    this.std.rows = rows;
  }

  set columns(columns: number) {
    this.std.columns = columns;
  }
}

/**
 * A helper class to write messages to a log file.
 */
class FileLogWritable extends Transform {
  constructor() {
    super({
      transform: (chunk, _encoding, callback) =>
        callback(null, stripVTControlCharacters(String(chunk))),
    });
    // Swallow the data, until pipeToNewLogFile() is called.
    this.pipe(
      new FileLogWritable.Writable({ write: (_, __, callback) => callback() }),
    );
  }

  pipeToNewLogFile(key: string): () => Promise<void> {
    const fileName = `${TMP_DIR}/${dateToStr(new Date())}.${key}.log`;
    const file = createWriteStream(fileName, { flags: "a" });
    this.pipe(file);
    return async () =>
      new Promise((resolve) => {
        this.unpipe(file);
        file.end(resolve);
      });
  }
}

/** Use instead of process.stdout. */
export const stdout = new StdWritable(process.stdout);

/** Use instead of process.stderr. */
export const stderr = new StdWritable(process.stderr);

/** The messages are also logged to this stream. By default, they are swallowed.
 * The caller may pipe it to a file if needed.*/
export const stdlog = new FileLogWritable();

// A log-update engine. We use stderr, to not pollute the output when --json
// flag is used for instance.
const progressObj = logUpdate.create(stderr, { showCursor: true });

// Last unique lines written with progress().
const progressLoggedLines: Set<string> = new Set();

/**
 * Logs erasable (or persistent) progress message prepended with the timestamp.
 * The unique lines of the message are also written to the file log.
 */
export function progress(...lines: string[]): void {
  progress.unlogged(...lines);

  const linesToLog = lines.filter(
    (line) => !progressLoggedLines.has(line) && line.trim(),
  );
  if (linesToLog.length > 0) {
    stdlog.write(stripPassword(prependDate(linesToLog)) + "\n");
  }

  progressLoggedLines.clear();
  for (const line of lines) {
    progressLoggedLines.add(line);
  }
}

/**
 * Same as progress(), but does not write to the file log.
 */
progress.unlogged = (...lines: string[]): void => {
  progressObj(stripPassword(prependDate(lines)));
};

/**
 * Clears the progress.
 */
progress.clear = () => {
  progressObj.clear();
  progressLoggedLines.clear();
};

/**
 * Persists the progress on screen.
 */
progress.done = () => {
  progressObj.done();
  progressLoggedLines.clear();
};

/**
 * Clears the progress and prints the raw message to console and to log file.
 */
export function print(msg: string): void {
  progress.done();
  const str = stripPassword(msg) + "\n";
  stdout.write(str);
  stdlog.write(str);
}

/**
 * Same as print(), but uses bold font.
 */
print.section = (msg: string): void => {
  print(chalk.bold(msg));
};

/**
 * Same as print(), but prepends the current date and time. The message is also
 * written to the log file.
 */
export function log(...lines: string[]): void {
  print(prependDate(lines));
}

/**
 * Same as log() (i.e. with date/time prepended), but in red.
 */
log.warning = (...lines: string[]): void => {
  print(chalk.yellow(prependDate(lines)));
};

/**
 * Same as log() (i.e. with date/time prepended), but in red.
 */
log.error = (...lines: string[]): void => {
  print(chalk.red(prependDate(lines)));
};

/**
 * Logs details about the upcoming runShell() execution.
 */
log.shellCmd = ({
  comment,
  cmd,
  input,
  isError = false,
}: {
  comment: string;
  cmd: string;
  input: string | null;
  isError?: boolean;
}): void => {
  if (!comment.match(/[!?.:]$/)) {
    comment += "...";
  }

  log(chalk[isError ? "red" : "greenBright"](chalk.bold(comment)));
  if (cmd) {
    log(chalk.gray(`$ ${cmd}`));
  }

  input = input?.trim() ?? null;
  if (input) {
    log(chalk.gray(input.trim()));
  }
};
