/* eslint-disable no-bitwise */
import { format as formatDate } from 'date-fns'
import colorizer from 'json-colorizer'
import winston from 'winston'
import chalk from 'chalk'

const { timestamp, splat, json, errors, combine } = winston.format

/**
 * @see https://github.com/winstonjs/winston#logging
 */
export enum LogLevel {
  Error = 'error',
  Warn = 'warn',
  Info = 'info',
  Http = 'http',
  Verbose = 'verbose',
  Debug = 'debug',
  Silly = 'silly',
}

export enum LogOutput {
  Summary = 'summary',
  Details = 'details',
  Silent = 'silent',
  Raw = 'raw',
}

export const logIcons = new Map<LogLevel, string>([
  [LogLevel.Error, '💥'],
  [LogLevel.Warn, '🛑'],
  [LogLevel.Info, '🔔'],
  [LogLevel.Http, '🚛'],
  [LogLevel.Verbose, '💬'],
  [LogLevel.Debug, '🐛'],
  [LogLevel.Silly, '🤪'],
])

export interface LogReporter {
  readonly version: string
  readonly id: string
  readonly ip: string
}

export interface LogData {
  // Default info
  readonly reporter: LogReporter
  readonly timestamp: string

  // Optional error stack
  stack?: string

  // Optional additional info
  [prop: string]: any
}

const colors = {
  STRING_LITERAL: 'white',
  BOOLEAN_LITERAL: 'blue',
  NUMBER_LITERAL: 'cyan',
  NULL_LITERAL: 'magenta',
  STRING_KEY: 'white.dim',
}

const getLogOutputType = (value: LogOutput = LogOutput.Raw) => {
  if (!Object.values(LogOutput).includes(value)) {
    // eslint-disable-next-line no-console
    console.warn(
      `The provided LOG_OUTPUT "${value}" is not supported. Defaulting to "${LogOutput.Raw}".`,
    )

    return LogOutput.Raw
  }

  return value
}

const getLogLevel = (value: LogLevel = LogLevel.Info) => {
  if (!Object.values(LogLevel).includes(value)) {
    // eslint-disable-next-line no-console
    console.warn(
      `The provided LOG_LEVEL "${value}" is not supported. Defaulting to "${LogLevel.Info}".`,
    )

    return LogLevel.Info
  }

  return value
}

const getReadableFormatter = (details = false) =>
  winston.format.printf((info) => {
    const { level, message, timestamp: ts } = info
    const label = logIcons.get(level as LogLevel) || '❔'
    const time = chalk.yellow(formatDate(new Date(ts), '[HH:mm:ss.SSS]'))
    const header = chalk.bold(`${time} ${label} ${message}`)
    const parts = [header]

    if (details) {
      parts.push(
        colorizer(JSON.stringify(info), {
          pretty: true,
          colors,
        }).replace(/\\n/g, '\n'), // To avoid having "\n" in the stack log
      )
    }

    return parts.join('\n')
  })

/**
 * SpotLogger Class
 */
export const createLogger = (id: string, ip: string, version: string) => {
  const { LOG_OUTPUT, LOG_LEVEL, RUNTIME_ENV, NODE_ENV } = process.env
  const outputType = getLogOutputType(LOG_OUTPUT as LogOutput)
  const logLevel = getLogLevel(LOG_LEVEL as LogLevel)
  const formats = [errors({ stack: true }), timestamp(), splat(), json()]

  // If any of the formatted output flags are set we pretty print.
  if ([LogOutput.Details, LogOutput.Summary].includes(outputType)) {
    formats.push(getReadableFormatter(outputType === LogOutput.Details))
  }

  return winston.createLogger({
    format: combine(...formats),
    level: logLevel,
    transports: [
      new winston.transports.Console({
        silent: outputType === LogOutput.Silent,
      }),
    ],
    defaultMeta: {
      environment: RUNTIME_ENV || NODE_ENV,
      type: 'log',
      reporter: {
        version,
        id,
        ip,
      },
    },
  })
}
