import path from 'path';
import { Readable } from 'stream';
import * as fs from 'fs-extra';
import { injectable } from 'inversify';
import { appPath } from '../../core/app-path';
import { isDev } from '../../core/env';
import { ILogger } from './i-logger';
import { LogLevel } from './log-level';

const loggingDir = path.resolve(appPath, 'logs');
const logFile = path.resolve(loggingDir, 'default.log');
const exceptionLogFile = path.resolve(loggingDir, 'exception.log');
const maxLogSize = 50000;

/**
 * A logger not using winston to log to files in the appPath.
 */
@injectable()
export class Logger implements ILogger {
  public constructor() {
    fs.createFileSync(logFile);
    fs.createFileSync(exceptionLogFile);
  }

  private static writeStream(stream: NodeJS.ReadableStream, filePath: string): Promise<void> {
    return new Promise((resolve) => {
      let buffer = fs.readFileSync(filePath, {
        flag: 'r',
      });

      stream.on('data', (chunk) => {
        buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
      });

      stream.on('end', () => {
        const diff = buffer.byteLength - maxLogSize;
        if (diff > 0) {
          buffer = buffer.slice(diff);
          const firstLineBreakIndex = buffer.findIndex((value) => String.fromCharCode(value) === '\n');
          buffer = buffer.slice(firstLineBreakIndex + 1);
        }
        fs.writeFileSync(filePath, buffer);
        resolve();
      });
    });
  }

  private static writeLine(line: string): Promise<void> {
    return Logger.writeStream(Readable.from(`${line}\n`), logFile);
  }

  private static formatLine(level: LogLevel, message: string): string {
    return `[${new Date().toISOString()}] ${LogLevel[level]}: ${message}`;
  }

  public getLogFile(): string {
    return logFile;
  }

  public getExceptionsLogFile(): string {
    return exceptionLogFile;
  }

  public exception(error: Error): Promise<void> {
    return Logger.writeStream(
      Readable.from(
        `[${new Date().toISOString()}] ${(error as NodeJS.ErrnoException).code ?? 'Error'}: ${error.message}
        ${error.stack ?? 'no stack trace'}\n`
      ),
      exceptionLogFile
    );
  }

  public log(level: LogLevel, message: string): Promise<void> {
    if (!isDev() && level === LogLevel.debug) {
      return Promise.resolve();
    }
    return Logger.writeLine(Logger.formatLine(level, message)).catch((error) => {
      // eslint-disable-next-line no-console -- I don't want logging to be a source of a fatal error, but i want to log it somewhere
      console.error(error);
    });
  }

  public fatal(message: string): Promise<void> {
    return this.log(LogLevel.fatal, message);
  }

  public error(message: string): Promise<void> {
    return this.log(LogLevel.error, message);
  }

  public warning(message: string): Promise<void> {
    return this.log(LogLevel.warning, message);
  }

  public notice(message: string): Promise<void> {
    return this.log(LogLevel.notice, message);
  }

  public info(message: string): Promise<void> {
    return this.log(LogLevel.info, message);
  }

  public debug(message: string): Promise<void> {
    return this.log(LogLevel.debug, message);
  }
}
