import winston, { createLogger, transports, format } from "winston";
import { TransformableInfo, FormatWrap } from "logform";
import Transport from "winston-transport";
import { v4 } from "uuid";

type LogFunction<T> = (event: T) => void;

export interface Logger<T> {
  debug: LogFunction<T>;
  info: LogFunction<T>;
  warn: LogFunction<T>;
  error: LogFunction<T>;
  profile: LogFunction<string>;
  extend: (extraMeta: object) => Logger<T>;
}

export interface HasLogger<T> {
  logger: Logger<T>;
}

export interface LoggerMetaData {
  application: string;
  environment: string;
  correlationRequestId?: string;
}

export const createNoopLogger = <T>(): Logger<T> => {
  const logger: Logger<T> = {
    debug() {},
    info() {},
    warn() {},
    error() {},
    profile() {},
    extend() {
      return logger;
    }
  };
  return logger;
};

const withMetaData: FormatWrap = format(
  (info: TransformableInfo, meta: LoggerMetaData) => {
    return {
      ...meta,
      ...info
    };
  }
);

const loggerFromWinstonLogger = <T extends object>(
  logger: winston.Logger
): Logger<T> => {
  return {
    debug(event: T) {
      logger.debug(event);
    },
    info(event: T) {
      logger.info(event);
    },
    warn(event: T) {
      logger.warn(event);
    },
    error(event: T) {
      logger.error(event);
    },
    profile(id: string) {
      logger.profile(id);
    },
    extend(extraMeta: object) {
      return loggerFromWinstonLogger(logger.child(extraMeta));
    }
  };
};

const createWinstonLogger = <T extends object>(
  meta: LoggerMetaData,
  level: string = "info",
  transport: Transport = new transports.Console()
): Logger<T> => {
  const logger = createLogger({
    level,
    transports: [transport],
    format: format.combine(
      withMetaData(meta),
      format.timestamp(),
      format.ms(),
      format.json()
    )
  });

  return loggerFromWinstonLogger(logger);
};

export const createDefaultLogger = createWinstonLogger;

export const createLoggerFromContext = <T extends object>(
  application: string,
  environment: string,
  level: string,
  creator: (
    meta: LoggerMetaData,
    level: string
  ) => Logger<T> = createDefaultLogger
) => (correlationRequestId?: string) =>
  creator(
    {
      application,
      environment,
      correlationRequestId: correlationRequestId || `UNKNOWN/${v4()}`
    },
    level
  );
