package com.newrelic.metrics.publish.util;

import java.io.File;

import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;

/**
 * Logger class which supports logging at debug, info, warn, error, and fatal levels. 
 * By default the log level is info. 
 * 
 * Logging is directed to the console and to a log file. The log file name and location is configurable.
 * 
 * <p>
 * For usage see, {@link #getLogger(Class)}
 * 
 * <p>
 * Note: {@link #init(String, String, String, Integer)} must be called before getting a logger via {@link #getLogger(Class)}.
 * Logger initialization is managed by the SDK.
 */
public final class Logger {

    /**
     * Supported log levels:
     * Debug, Info, Warn, Error, Fatal
     */
    public enum Level {
        Debug, Info, Warn, Error, Fatal;
        
        public static Level fromString(String value) {
            for (Level level : Level.values()) {
                if (level.toString().equalsIgnoreCase(value)) {
                    return level;
                }
            }
            return null;
        }
    }
    
    // default configuration
    private static Level level = Level.Info;
    private static String filePath = "logs";
    private static String fileName = "newrelic_plugin.log";
    private static Integer fileLimitInKilobytes = 25600; // 25 MB
    
    // logback configuration
    private static final String LogPattern  = "[%date] %-5level %logger - %msg%n";
    private static final ConsoleAppender<ILoggingEvent> ConsoleAppender = new ConsoleAppender<ILoggingEvent>();
    private static final RollingFileAppender<ILoggingEvent> FileAppender = new RollingFileAppender<ILoggingEvent>();
    
    private final ch.qos.logback.classic.Logger logger;
    
    private Logger(ch.qos.logback.classic.Logger logger, Level level) {
        this.logger = logger;
        this.logger.addAppender(ConsoleAppender);
        this.logger.addAppender(FileAppender);
        this.logger.setLevel(translateLevel(level));
    }
    
    /**
     * Get a logger for a specific class.
     * <p>
     * For better visibility into where log messages are reported from, 
     * it is recommended to create a static logger once per class.
     * 
     * <p>
     * Examples:
     * <p>
     * <pre>
     * {@code
     * private static final Logger logger = Logger.getLogger(ExampleAgent.class);
     * logger.debug("debug log message");
     * logger.error(new RuntimeError(), "error log message", "\tsecond message");
     * }
     * <p>
     * 
     * @param klass the class name will be used for the name of the logger
     * @return Logger
     */
    public static Logger getLogger(Class<?> klass) {
        return new Logger((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(klass), getLevel());
    }
    
    /**
     * The Logger class must be initialized before any loggers can be created.
     * 
     * <p>
     * If logFileLimitInKilobytes is 0, log file max size and file rolling will be disabled. 
     * The log file size would then be unlimited.
     * 
     * @param logLevel the string log level for all loggers
     * @param logFilePath the path to the log file for all loggers
     * @param logFileName the log file name for all loggers
     * @param logFileLimitInKilobytes the max size of the log file in kilobytes
     */
    public static void init(String logLevel, String logFilePath, String logFileName, Integer logFileLimitInKilobytes) {
        validateArgs(logLevel, logFilePath, logFileName, logFileLimitInKilobytes);
        
        Logger.level = Level.fromString(logLevel);
        Logger.filePath = logFilePath;
        Logger.fileName = logFileName;
        Logger.fileLimitInKilobytes = logFileLimitInKilobytes;
        
        initLogback();
    }
    
    private static void validateArgs(String logLevel, String logFilePath, String logFileName, Integer logFileLimitInKilobytes) {
        if (isNullOrEmptyString(logLevel)) {
            throw new IllegalArgumentException("'logLevel' must not be null or empty");
        }
        if (isNullOrEmptyString(logFilePath)) {
            throw new IllegalArgumentException("'logFilePath' must not be null or empty");
        }
        if (isNullOrEmptyString(logFileName)) {
            throw new IllegalArgumentException("'logFileName' must not be null or empty");
        }
        if (logFileLimitInKilobytes == null || logFileLimitInKilobytes < 0) {
            throw new IllegalArgumentException("'logFileLimitInKilobytes' must not be null or negative");
        }
    }
    
    private static boolean isNullOrEmptyString(String value) {
        return value == null || value.length() == 0;
    }
    
    /**
     * Initialize Logback
     */
    private static void initLogback() {
        // reset logger context
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        context.reset();
        
        // shared console appender
        ConsoleAppender.setContext(context);
        ConsoleAppender.setTarget("System.out");
        
        PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
        consoleEncoder.setContext(context);
        consoleEncoder.setPattern(LogPattern);
        consoleEncoder.start();
        ConsoleAppender.setEncoder(consoleEncoder);
        ConsoleAppender.start();
        
        // rolling file
        String logFile = getFilePath() + File.separatorChar + getFileName();
        FileAppender.setContext(context);
        FileAppender.setFile(logFile);
        
        // log pattern
        PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
        fileEncoder.setContext(context);
        fileEncoder.setPattern(LogPattern);
        fileEncoder.start();
        FileAppender.setEncoder(fileEncoder);
        
        // rolling policy
        FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
        rollingPolicy.setContext(context);
        rollingPolicy.setParent(FileAppender);
        rollingPolicy.setFileNamePattern(logFile + "%i.zip");
        rollingPolicy.setMinIndex(1);
        rollingPolicy.setMaxIndex(1);
        rollingPolicy.start();
        
        // file max size - if fileLimit is 0, set max file size to maximum allowed
        long fileLimit = getFileLimitInKBytes() != 0 ? getFileLimitInKBytes() * 1024 : Long.MAX_VALUE;
        SizeBasedTriggeringPolicy<ILoggingEvent> triggeringPolicy = new SizeBasedTriggeringPolicy<ILoggingEvent>(String.valueOf(fileLimit));
        triggeringPolicy.start();
        
        FileAppender.setRollingPolicy(rollingPolicy);
        FileAppender.setTriggeringPolicy(triggeringPolicy);
        FileAppender.start();
    }
    
    /**
     * Log a message with a variable number of arguments at the debug level.
     * Only logs if the debug level is enabled.
     * 
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void debug(Object... messages) {
        if (logger.isDebugEnabled()) {
            logger.debug(buildMessage(messages));
        }
    }
    
    /**
     * Log a throwable and a message with a variable number of arguments at the debug level.
     * Only logs if the debug level is enabled.
     * 
     * @param throwable
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void debug(Throwable throwable, Object... messages) {
        if (logger.isDebugEnabled()) {
            logger.debug(buildMessage(messages), throwable);
        }
    }
    
    /**
     * Log a message with a variable number of arguments at the info level.
     * Only logs if the info level is enabled.
     * 
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void info(Object... messages) {
        if (logger.isInfoEnabled()) {
            logger.info(buildMessage(messages));
        }
    }
    
    /**
     * Log a throwable and a message with a variable number of arguments at the info level.
     * Only logs if the info level is enabled.
     * 
     * @param throwable
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void info(Throwable throwable, Object... messages) {
        if (logger.isInfoEnabled()) {
            logger.info(buildMessage(messages), throwable);
        }
    }
    
    /**
     * Log a message with a variable number of arguments at the warn level.
     * Only logs if the warn level is enabled.
     * 
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void warn(Object... messages) {
        if (logger.isWarnEnabled()) {
            logger.warn(buildMessage(messages));
        }
    }
    
    /**
     * Log a throwable and a message with a variable number of arguments at the warn level.
     * Only logs if the warn level is enabled.
     * 
     * @param throwable
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void warn(Throwable throwable, Object... messages) {
        if (logger.isWarnEnabled()) {
            logger.warn(buildMessage(messages), throwable);
        }
    }
    
    /**
     * Log a message with a variable number of arguments at the error level.
     * Only logs if the error level is enabled.
     * 
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void error(Object... messages) {
        if (logger.isErrorEnabled()) {
            logger.error(buildMessage(messages));
        }
    }
    
    /**
     * Log a throwable and a message with a variable number of arguments at the error level.
     * Only logs if the error level is enabled.
     * 
     * @param throwable
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void error(Throwable throwable, Object... messages) {
        if (logger.isErrorEnabled()) {
            logger.error(buildMessage(messages), throwable);
        }
    }
    
    /**
     * Log a message with a variable number of arguments at the fatal level.
     * Only logs if the fatal level is enabled.
     * 
     * <p>
     * Note: Currently fatal logs at the same level as error. This may change in a future release.
     * 
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void fatal(Object... messages) {
        error(messages);
    }
    
    /**
     * Log a throwable and a message with a variable number of arguments at the fatal level.
     * Only logs if the fatal level is enabled.
     * 
     * <p>
     * Note: Currently fatal logs at the same level as error. This may change in a future release.
     * 
     * @param throwable
     * @param messages
     * @throws IllegalArgumentException if messages is null
     */
    public void fatal(Throwable throwable, Object... messages) {
        error(throwable, messages);
    }
    
    /**
     * Get the log level for all loggers.
     * Defaults to 'info'.
     * 
     * @return level the log level
     */
    static Level getLevel() {
        return level;
    }
    
    /**
     * Get the log file path for all loggers.
     * Defaults to 'logs'.
     * 
     * @return filePath the log file path
     */
    static String getFilePath() {
        return filePath;
    }
    
    /**
     * Get the log file name for all loggers.
     * Defaults to 'newrelic_plugin.log'.
     * 
     * @return fileName the log file name
     */
    static String getFileName() {
        return fileName;
    }
    
    /**
     * Get the log file limit in kilobytes.
     * Defaults to '25600' or 25 mb.
     * 
     * @return fileLimitInKilobytes the log file limit in kilobytes
     */
    static Integer getFileLimitInKBytes() {
        return fileLimitInKilobytes;
    }
    
    static String buildMessage(Object... messages) {
        if (messages == null) {
            throw new IllegalArgumentException("'messages' cannot be null");
        }
        
        StringBuilder builder = new StringBuilder();
        for (Object message : messages) {
            builder.append(message);
        }
        return builder.toString();
    }
    
    /*
     * Translate supported log level to Logback level
     */
    static ch.qos.logback.classic.Level translateLevel(Level level) {
        if (level == null) {
            throw new IllegalArgumentException("'level' cannot be null");
        }
        
        switch (level) {
        case Debug:
            return ch.qos.logback.classic.Level.DEBUG;
        case Info:
            return ch.qos.logback.classic.Level.INFO;
        case Warn:
            return ch.qos.logback.classic.Level.WARN;
        case Error:
            return ch.qos.logback.classic.Level.ERROR;
        case Fatal:
            return ch.qos.logback.classic.Level.ERROR;
        default:
            return ch.qos.logback.classic.Level.INFO;
        }
    }
}