import type pino from "pino";

import { mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";

import type { LogLevel, ResolvedLoggingConfig } from "./checker/types";

export interface Logger {
  child(bindings: Record<string, unknown>): Logger;
  debug(obj: Record<string, unknown>, msg?: string): void;
  debug(msg: string): void;
  error(obj: Record<string, unknown>, msg?: string): void;
  error(msg: string): void;
  fatal(obj: Record<string, unknown>, msg?: string): void;
  fatal(msg: string): void;
  flush(): void;
  info(obj: Record<string, unknown>, msg?: string): void;
  info(msg: string): void;
  trace(obj: Record<string, unknown>, msg?: string): void;
  trace(msg: string): void;
  warn(obj: Record<string, unknown>, msg?: string): void;
  warn(msg: string): void;
}

export const REDACT_PATHS = [
  "authorization",
  "cookie",
  "set-cookie",
  "*.set-cookie",
  "authToken",
  "key",
  "password",
  "token",
  "apiKey",
  "*.authorization",
  "*.cookie",
  "*.authToken",
  "*.key",
  "*.password",
  "*.token",
  "*.apiKey",
];

const LOG_LEVEL_MAP: Record<LogLevel, string> = {
  debug: "debug",
  error: "error",
  fatal: "fatal",
  info: "info",
  trace: "trace",
  warn: "warn",
};

type LogFn = (objOrMsg: Record<string, unknown> | string, msg?: string) => void;

const voidLog: LogFn = () => undefined;

class ConsoleFallbackLogger implements Logger {
  child(_bindings: Record<string, unknown>): Logger {
    return this;
  }

  debug(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.log(formatMsg(objOrMsg, msg));
  }

  error(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.error(formatMsg(objOrMsg, msg));
  }

  fatal(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.error(formatMsg(objOrMsg, msg));
  }

  flush: () => void = () => undefined;

  info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.log(formatMsg(objOrMsg, msg));
  }

  trace(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.log(formatMsg(objOrMsg, msg));
  }

  warn(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    console.warn(formatMsg(objOrMsg, msg));
  }
}

class NoopLogger implements Logger {
  debug: LogFn = voidLog;
  error: LogFn = voidLog;
  fatal: LogFn = voidLog;
  info: LogFn = voidLog;
  trace: LogFn = voidLog;
  warn: LogFn = voidLog;
  child(_bindings: Record<string, unknown>): Logger {
    return this;
  }
  flush: () => void = () => undefined;
}

class PinoLoggerWrapper implements Logger {
  private pino: pino.Logger;

  constructor(pinoLogger: pino.Logger) {
    this.pino = pinoLogger;
  }

  child(bindings: Record<string, unknown>): Logger {
    return new PinoLoggerWrapper(this.pino.child(bindings));
  }

  debug(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.debug(objOrMsg);
    else this.pino.debug(objOrMsg, msg);
  }

  error(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.error(objOrMsg);
    else this.pino.error(objOrMsg, msg);
  }

  fatal(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.fatal(objOrMsg);
    else this.pino.fatal(objOrMsg, msg);
  }

  flush(): void {
    this.pino.flush();
  }

  info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.info(objOrMsg);
    else this.pino.info(objOrMsg, msg);
  }

  trace(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.trace(objOrMsg);
    else this.pino.trace(objOrMsg, msg);
  }

  warn(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") this.pino.warn(objOrMsg);
    else this.pino.warn(objOrMsg, msg);
  }
}

export class MemoryLogger implements Logger {
  entries: Array<{ level: string; msg: string; obj?: Record<string, unknown> }> = [];

  child(_bindings: Record<string, unknown>): Logger {
    return this;
  }

  debug(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("debug", objOrMsg, msg);
  }

  error(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("error", objOrMsg, msg);
  }

  fatal(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("fatal", objOrMsg, msg);
  }

  flush: () => void = () => undefined;

  info(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("info", objOrMsg, msg);
  }

  trace(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("trace", objOrMsg, msg);
  }

  warn(objOrMsg: Record<string, unknown> | string, msg?: string): void {
    this.capture("warn", objOrMsg, msg);
  }

  private capture(level: string, objOrMsg: Record<string, unknown> | string, msg?: string): void {
    if (typeof objOrMsg === "string") {
      this.entries.push({ level, msg: objOrMsg });
    } else {
      this.entries.push({ level, msg: msg ?? "", obj: objOrMsg });
    }
  }
}

export function createConsoleFallback(): Logger {
  return new ConsoleFallbackLogger();
}

export function createMemoryLogger(): MemoryLogger {
  return new MemoryLogger();
}

export function createNoopLogger(): Logger {
  return new NoopLogger();
}

export async function createRuntimeLogger(
  config: ResolvedLoggingConfig,
  mode: string,
  version: string,
): Promise<Logger> {
  const pinoLib = await import("pino");
  const pinoPretty = await import("pino-pretty");

  mkdirSync(dirname(config.filePath), { recursive: true });

  const rootLevel = resolveRootLevel(config.consoleLevel, config.fileLevel);

  const prettyStream = pinoPretty.default({
    colorize: true,
    ignore: "pid,hostname",
    singleLine: true,
    translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
  });

  const fileStream = await createRollingFileStream(config);

  const streams: pino.StreamEntry[] = [
    { level: toPinoLevel(config.consoleLevel) as pino.Level, stream: prettyStream },
    { level: toPinoLevel(config.fileLevel) as pino.Level, stream: fileStream },
  ];

  const logger = pinoLib.default(
    {
      base: { mode, service: "dial-server", version },
      level: rootLevel,
      redact: { censor: "[Redacted]", paths: REDACT_PATHS },
      timestamp: pinoLib.stdTimeFunctions.isoTime,
    },
    pinoLib.multistream(streams),
  );

  return new PinoLoggerWrapper(logger);
}

async function createRollingFileStream(config: ResolvedLoggingConfig): Promise<NodeJS.WritableStream> {
  const dir = dirname(config.filePath);
  const base = resolve(dir, config.filePath.replace(/^.*[\\/]/, "").replace(/\.log$/, ""));

  try {
    const buildPinoRoll = (await import("pino-roll")).default;
    return await buildPinoRoll({
      file: base,
      frequency: config.rotationFrequency,
      limit: { count: config.rotationMaxFiles },
      mkdir: true,
      size: config.rotationSizeRaw,
    });
  } catch {
    const fs = await import("node:fs");
    return fs.createWriteStream(config.filePath, { flags: "a" });
  }
}

function formatMsg(objOrMsg: Record<string, unknown> | string, msg?: string): string {
  if (typeof objOrMsg === "string") return objOrMsg;
  return msg ? `${msg} ${JSON.stringify(objOrMsg)}` : JSON.stringify(objOrMsg);
}

function resolveRootLevel(consoleLevel: LogLevel, fileLevel: LogLevel): string {
  const order: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
  const ci = order.indexOf(consoleLevel);
  const fi = order.indexOf(fileLevel);
  return LOG_LEVEL_MAP[order[Math.min(ci, fi)]!] ?? "info";
}

function toPinoLevel(level: LogLevel): string {
  return LOG_LEVEL_MAP[level];
}
