// Copyright 2018-2025 the Deno authors. MIT license.
import { type LevelName, LogLevels } from "./levels.ts";
import type { LogRecord } from "./logger.ts";
import { BaseHandler, type BaseHandlerOptions } from "./base_handler.ts";
import { writeAllSync } from "@std/io/write-all";
import {
  bufSymbol,
  encoderSymbol,
  filenameSymbol,
  fileSymbol,
  modeSymbol,
  openOptionsSymbol,
  pointerSymbol,
} from "./_file_handler_symbols.ts";

/** Supported log modes for FileHandlerOptions {@linkcode FileHandlerOptions.mode}. */
export type LogMode = "a" | "w" | "x";

/** Options for {@linkcode FileHandler}. */
export interface FileHandlerOptions extends BaseHandlerOptions {
  /**
   * The filename to output to.
   */
  filename: string;
  /**
   * Log mode for the handler. Behavior of the log modes is as follows:
   *
   * - `'a'` - Default mode. Appends new log messages to the end of an existing log
   *   file, or create a new log file if none exists.
   * - `'w'` - Upon creation of the handler, any existing log file will be removed
   *   and a new one created.
   * - `'x'` - This will create a new log file and throw an error if one already
   *   exists.
   *
   * @default {"a"}
   */
  mode?: LogMode;
  /**
   * Buffer size for writing log messages to file, in bytes.
   *
   * @default {4096}
   */
  bufferSize?: number;
}

/**
 * A file-based log handler that writes log messages to a specified file with buffering and optional modes.
 * The logs are buffered for optimized performance, writing to the file only
 * when the buffer is full, on manual .flush() call, during logging of a critical message or when process ends.
 * It is important to note that the file can grow indefinitely.
 *
 * This handler requires `--allow-write` permission on the log file.
 *
 * @example Usage
 * ```ts no-assert
 * import { FileHandler } from "@std/log/file-handler";
 *
 * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
 * handler.setup();
 * handler.log('Hello, world!'); // Buffers the message, or writes it to the file depending on buffer state
 * handler.flush(); // Manually flushes the buffer
 * handler.destroy(); // Closes the file and removes listeners
 * ```
 */
export class FileHandler extends BaseHandler {
  /** Opened file to append logs to.
   *
   * @private
   */
  [fileSymbol]: Deno.FsFile | undefined;
  /** Buffer used to write to file.
   *
   * @private
   */
  [bufSymbol]: Uint8Array;
  /**
   * Current position for pointer.
   *
   * @private
   */
  [pointerSymbol] = 0;
  /**
   * Filename associated with the file being logged.
   *
   * @private
   */
  [filenameSymbol]: string;
  /**
   * Current log mode.
   *
   * @private
   */
  [modeSymbol]: LogMode;
  /**
   * File open options.
   *
   * @private
   */
  [openOptionsSymbol]: Deno.OpenOptions;
  /**
   * Text encoder.
   *
   * @private
   */
  [encoderSymbol]: TextEncoder = new TextEncoder();
  #unloadCallback = (() => {
    this.destroy();
  }).bind(this);

  /**
   * Constructs a new FileHandler instance.
   *
   * @param levelName The level name to log messages at.
   * @param options Options for the handler.
   */
  constructor(levelName: LevelName, options: FileHandlerOptions) {
    super(levelName, options);
    this[filenameSymbol] = options.filename;
    // default to append mode, write only
    this[modeSymbol] = options.mode ?? "a";
    this[openOptionsSymbol] = {
      createNew: this[modeSymbol] === "x",
      create: this[modeSymbol] !== "x",
      append: this[modeSymbol] === "a",
      truncate: this[modeSymbol] !== "a",
      write: true,
    };
    this[bufSymbol] = new Uint8Array(options.bufferSize ?? 4096);
  }

  /**
   * Sets up the file handler by opening the specified file and initializing resources.
   *
   * @example Usage
   * ```ts no-assert
   * import { FileHandler } from "@std/log/file-handler";
   *
   * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
   * handler.setup(); // Opens the file and prepares the handler for logging.
   * handler.destroy();
   * ```
   */
  override setup() {
    this[fileSymbol] = Deno.openSync(
      this[filenameSymbol],
      this[openOptionsSymbol],
    );
    this.#resetBuffer();

    addEventListener("unload", this.#unloadCallback);
  }

  /**
   * Handles a log record and flushes the buffer if the log level is higher than error.
   *
   * @param logRecord Log record to handle.
   *
   * @example Usage
   * ```ts
   * import { FileHandler } from "@std/log/file-handler";
   * import { assertInstanceOf } from "@std/assert/instance-of";
   * import { LogLevels } from "./levels.ts";
   * import { LogRecord } from "./logger.ts";
   *
   * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
   * handler.setup();
   *
   * // Flushes the buffer immediately and logs "CRITICAL This log is very critical indeed." into the file.
   * handler.handle(
   *   new LogRecord({
   *     msg: "This log is very critical indeed.",
   *     args: [],
   *     level: LogLevels.CRITICAL,
   *     loggerName: "INFO",
   *   }),
   * );
   * handler.destroy();
   *
   * assertInstanceOf(handler, FileHandler);
   * ```
   */
  override handle(logRecord: LogRecord) {
    super.handle(logRecord);

    // Immediately flush if log level is higher than ERROR
    if (logRecord.level > LogLevels.ERROR) {
      this.flush();
    }
  }

  /**
   * Logs a message by adding it to the buffer, with flushing as needed.
   *
   * @param msg The message to log.
   *
   * @example Usage
   * ```ts
   * import { FileHandler } from "@std/log/file-handler";
   * import { assertInstanceOf } from "@std/assert/instance-of";
   *
   * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
   * handler.setup();
   * handler.log('Hello, world!');
   * handler.flush();
   * handler.destroy();
   *
   * assertInstanceOf(handler, FileHandler);
   * ```
   */
  log(msg: string) {
    const bytes = this[encoderSymbol].encode(msg + "\n");
    if (bytes.byteLength > this[bufSymbol].byteLength - this[pointerSymbol]) {
      this.flush();
    }
    if (bytes.byteLength > this[bufSymbol].byteLength) {
      writeAllSync(this[fileSymbol]!, bytes);
    } else {
      this[bufSymbol].set(bytes, this[pointerSymbol]);
      this[pointerSymbol] += bytes.byteLength;
    }
  }

  /**
   * Immediately writes the contents of the buffer to the previously opened file.
   *
   * @example Usage
   * ```ts
   * import { FileHandler } from "@std/log/file-handler";
   * import { assertInstanceOf } from "@std/assert/instance-of";
   *
   * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
   * handler.setup();
   * handler.log('Hello, world!');
   * handler.flush(); // Writes buffered log messages to the file immediately.
   * handler.destroy();
   *
   * assertInstanceOf(handler, FileHandler);
   * ```
   */
  flush() {
    if (this[pointerSymbol] > 0 && this[fileSymbol]) {
      let written = 0;
      while (written < this[pointerSymbol]) {
        written += this[fileSymbol].writeSync(
          this[bufSymbol].subarray(written, this[pointerSymbol]),
        );
      }
      this.#resetBuffer();
    }
  }

  #resetBuffer() {
    this[pointerSymbol] = 0;
  }

  /**
   * Destroys the handler, performing any cleanup that is required and closes the file handler.
   *
   * @example Usage
   * ```ts
   * import { FileHandler } from "@std/log/file-handler";
   * import { assertInstanceOf } from "@std/assert/instance-of";
   *
   * const handler = new FileHandler("INFO", { filename: "./_tmp/logs.txt" });
   * handler.setup();
   * handler.destroy();
   *
   * assertInstanceOf(handler, FileHandler);
   * ```
   */
  override destroy() {
    this.flush();
    this[fileSymbol]?.close();
    this[fileSymbol] = undefined;
    removeEventListener("unload", this.#unloadCallback);
  }
}
