// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import type { LevelName } from "./levels.ts";
import { existsSync } from "jsr:@std/fs@^1.0.3/exists";
import { FileHandler, type FileHandlerOptions } from "./file_handler.ts";
import {
  encoderSymbol,
  filenameSymbol,
  fileSymbol,
  modeSymbol,
  openOptionsSymbol,
} from "./_file_handler_symbols.ts";

interface RotatingFileHandlerOptions extends FileHandlerOptions {
  maxBytes: number;
  maxBackupCount: number;
}

/**
 * This handler extends the functionality of the {@linkcode FileHandler} by
 * "rotating" the log file when it reaches a certain size. `maxBytes` specifies
 * the maximum size in bytes that the log file can grow to before rolling over
 * to a new one. If the size of the new log message plus the current log file
 * size exceeds `maxBytes` then a roll-over is triggered. When a roll-over
 * occurs, before the log message is written, the log file is renamed and
 * appended with `.1`. If a `.1` version already existed, it would have been
 * renamed `.2` first and so on. The maximum number of log files to keep is
 * specified by `maxBackupCount`. After the renames are complete the log message
 * is written to the original, now blank, file.
 *
 * Example: Given `log.txt`, `log.txt.1`, `log.txt.2` and `log.txt.3`, a
 * `maxBackupCount` of 3 and a new log message which would cause `log.txt` to
 * exceed `maxBytes`, then `log.txt.2` would be renamed to `log.txt.3` (thereby
 * discarding the original contents of `log.txt.3` since 3 is the maximum number
 * of backups to keep), `log.txt.1` would be renamed to `log.txt.2`, `log.txt`
 * would be renamed to `log.txt.1` and finally `log.txt` would be created from
 * scratch where the new log message would be written.
 *
 * This handler uses a buffer for writing log messages to file. Logs can be
 * manually flushed with `fileHandler.flush()`. Log messages with a log level
 * greater than ERROR are immediately flushed. Logs are also flushed on process
 * completion.
 *
 * Additional notes on `mode` as described above:
 *
 * - `'a'` Default mode. As above, this will pick up where the logs left off in
 *   rotation, or create a new log file if it doesn't exist.
 * - `'w'` in addition to starting with a clean `filename`, this mode will also
 *   cause any existing backups (up to `maxBackupCount`) to be deleted on setup
 *   giving a fully clean slate.
 * - `'x'` requires that neither `filename`, nor any backups (up to
 *   `maxBackupCount`), exist before setup.
 *
 * This handler requires both `--allow-read` and `--allow-write` permissions on
 * the log files.
 */
export class RotatingFileHandler extends FileHandler {
  #maxBytes: number;
  #maxBackupCount: number;
  #currentFileSize = 0;

  constructor(levelName: LevelName, options: RotatingFileHandlerOptions) {
    super(levelName, options);
    this.#maxBytes = options.maxBytes;
    this.#maxBackupCount = options.maxBackupCount;
  }

  override setup() {
    if (this.#maxBytes < 1) {
      this.destroy();
      throw new Error(`"maxBytes" must be >= 1: received ${this.#maxBytes}`);
    }
    if (this.#maxBackupCount < 1) {
      this.destroy();
      throw new Error(
        `"maxBackupCount" must be >= 1: received ${this.#maxBackupCount}`,
      );
    }
    super.setup();

    if (this[modeSymbol] === "w") {
      // Remove old backups too as it doesn't make sense to start with a clean
      // log file, but old backups
      for (let i = 1; i <= this.#maxBackupCount; i++) {
        try {
          Deno.removeSync(this[filenameSymbol] + "." + i);
        } catch (error) {
          if (!(error instanceof Deno.errors.NotFound)) {
            throw error;
          }
        }
      }
    } else if (this[modeSymbol] === "x") {
      // Throw if any backups also exist
      for (let i = 1; i <= this.#maxBackupCount; i++) {
        if (existsSync(this[filenameSymbol] + "." + i)) {
          this.destroy();
          throw new Deno.errors.AlreadyExists(
            "Backup log file " + this[filenameSymbol] + "." + i +
              " already exists",
          );
        }
      }
    } else {
      this.#currentFileSize = (Deno.statSync(this[filenameSymbol])).size;
    }
  }

  override log(msg: string) {
    const msgByteLength = this[encoderSymbol].encode(msg).byteLength + 1;

    if (this.#currentFileSize + msgByteLength > this.#maxBytes) {
      this.rotateLogFiles();
      this.#currentFileSize = 0;
    }

    super.log(msg);

    this.#currentFileSize += msgByteLength;
  }

  rotateLogFiles() {
    this.flush();
    this[fileSymbol]!.close();

    for (let i = this.#maxBackupCount - 1; i >= 0; i--) {
      const source = this[filenameSymbol] + (i === 0 ? "" : "." + i);
      const dest = this[filenameSymbol] + "." + (i + 1);

      if (existsSync(source)) {
        Deno.renameSync(source, dest);
      }
    }

    this[fileSymbol] = Deno.openSync(
      this[filenameSymbol],
      this[openOptionsSymbol],
    );
  }
}

// denoCacheMetadata={"headers":{"access-control-allow-origin":"*","report-to":"{\"group\":\"cf-nel\",\"max_age\":604800,\"endpoints\":[{\"url\":\"https://a.nel.cloudflare.com/report/v4?s=QOHsoLej0IcAS69sjAvSgxpLosqlS7FUGLyX21xLxm%2FwxTdFHrY8CPC46xD7h7mVsUJuJAA5HifbWAkZuIuKkoXcaTevBdBIn7J1vyPSB9%2BKteQqXCYE4Lc%3D\"}]}","server":"cloudflare","content-type":"text/typescript","vary":"Origin","date":"Tue, 28 Jul 2026 12:31:15 GMT","set-cookie":"__cf_bm=PWvao88KXGCq6yGtRNtyh6EF3jZWTGmihYeBXi3o7Is-1785241875.10191-1.0.1.1-4RbirAkQg_5Cadu95oW065.amOvlYwDgRvIrB13SkLKfjY63zeIZJIRV0PiIpB1WN5T5pTCCnXPJpbAPC84MNaD8vcHuiLlDKQ8.65aVb9otaz31Pt3o25Aor4_qpzXx; HttpOnly; SameSite=None; Secure; Path=/; Domain=jsr.io; Expires=Tue, 28 Jul 2026 13:01:15 GMT","nel":"{\"report_to\":\"cf-nel\",\"success_fraction\":0.0,\"max_age\":604800}","alt-svc":"h3=\":443\"; ma=86400","cross-origin-resource-policy":"cross-origin","access-control-expose-headers":"*","etag":"\"19eb6dcd5fe93d2d2591028033870e1a\"","x-content-type-options":"nosniff","content-security-policy":"default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'; font-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; frame-ancestors 'none'; sandbox; form-action 'none';","cf-ray":"a223e65769797db6-LAX","content-length":"4989","x-jsr-backend":"modules","cache-control":"public, max-age=31536000, immutable","x-robots-tag":"noindex"},"url":"https://jsr.io/@std/log/0.224.7/rotating_file_handler.ts","time":1785241875}