/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.plugins.iis;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.domain.measurement.calltime.CallTimeData;
import org.rhq.core.domain.measurement.calltime.CallTimeDataValue;
import org.rhq.core.pluginapi.util.ResponseTimeConfiguration;
import org.rhq.core.pluginapi.util.ResponseTimeLogParser;

public class IISResponseTimeDelegate {

    // date time c-ip cs-method cs-uri-stem sc-status time-taken
    private enum LogFormatToken {
        DATE("date"), //
        TIME("time"), //
        C_IP("c-ip"), //
        CS_URI_STEM("cs-uri-stem"), //
        SC_STATUS("sc-status"), //
        TIME_TAKEN("time-taken");

        private String tokenLiteral;

        private LogFormatToken(String tokenLiteral) {
            this.tokenLiteral = tokenLiteral;
        }

        public static LogFormatToken getViaTokenLiteral(String literal) {
            for (LogFormatToken logToken : values()) {
                // case-sensitive comparison
                if (logToken.tokenLiteral.equals(literal)) {
                    return logToken;
                }
            }
            return null; // this is OK, user can be using extra tokens
        }

        public static String getRequiredTokenString() {
            StringBuilder builder = new StringBuilder();
            for (LogFormatToken nextToken : values()) {
                if (builder.length() > 0) {
                    builder.append(", ");
                }
                builder.append("'").append(nextToken.tokenLiteral).append("'");
            }
            return builder.toString();
        }

    }

    private File logDirectory;
    private File previousFile;
    private long previousOffset;
    private ResponseTimeLogParser logParser;
    private boolean isAbsoluteTime;
    private ResponseTimeConfiguration responseTimeConfiguration;
    private Map<LogFormatToken, Integer> logTokenPositions;

    private Log log = LogFactory.getLog(IISResponseTimeDelegate.class);

    private class IISResponseTimeLogFileFilter implements FileFilter {
        public boolean accept(File f) {
            String fileName = f.getName().toLowerCase();
            return fileName.startsWith("ex") && fileName.endsWith(".log");
        }
    }

    public IISResponseTimeDelegate(String logDirectory, String logFormat,
        ResponseTimeConfiguration responseTimeConfiguration/*, boolean isAbsoluteTime*/) {
        if (logDirectory == null) {
            throw new IllegalArgumentException("logDirectory can not be null");
        }
        this.logDirectory = new File(logDirectory);

        // IIS always logs in UTC, even if the "Use local time for file naming and rollover" option is selected
        this.isAbsoluteTime = true;

        this.responseTimeConfiguration = responseTimeConfiguration;

        logTokenPositions = new HashMap<LogFormatToken, Integer>();
        String[] logFormatTokens = logFormat.split(" ");
        EnumSet<LogFormatToken> foundTokens = EnumSet.noneOf(LogFormatToken.class);
        for (int i = 0; i < logFormatTokens.length; i++) {
            String nextLiteral = logFormatTokens[i];
            LogFormatToken nextToken = LogFormatToken.getViaTokenLiteral(nextLiteral);
            if (nextToken != null) {
                if (foundTokens.contains(nextToken)) {
                    // weird, but I suppose it's possible possible
                    log.warn("Token '" + nextLiteral + "' was specified more than once");
                } else {
                    log.debug("Required token found '" + nextLiteral + "' at position " + i);
                    foundTokens.add(nextToken);
                    logTokenPositions.put(nextToken, i);
                }
            } else {
                log.debug("Extra token found '" + nextLiteral + "' at position " + i);
            }
        }
        if (!foundTokens.containsAll(EnumSet.allOf(LogFormatToken.class))) {
            log.error("Log format '" + logFormat + "' needs to include: " + LogFormatToken.getRequiredTokenString());
        }
    }

    public void parseLogs(CallTimeData data) {
        File lastAccessedFile = getLastAccessedFile();
        if (lastAccessedFile == null) {
            // no files have been written to the logDirectory yet
            log.debug("No log files exist yet");
            return;
        }
        log.debug("Last accessed file = " + lastAccessedFile);

        if (previousFile == null) {
            // first time we found a file in the logDirectory
            log.debug("This is the first time we found a log file");
            previousFile = lastAccessedFile;
            previousOffset = previousFile.length();
            logParser = new IISResponseTimeLogParser(previousFile);
        } else {
            // logs have been rotated
            if (!previousFile.equals(lastAccessedFile)) {
                log.debug("Log files have been rotated");
                // so reset the offset to the beginnnig of the file
                previousOffset = previousFile.length();
                logParser = new IISResponseTimeLogParser(previousFile);
            }
        }

        if (logParser == null) {
            log.error("Unexpected error, logParser was null");
            return;
        }

        try {
            logParser.parseLog(data);
        } catch (IOException ioe) {
            log.error("Error parsing log data: " + ioe.getMessage(), ioe);
        }
    }

    private File getLastAccessedFile() {
        File[] logs = this.logDirectory.listFiles(new IISResponseTimeLogFileFilter());
        File lastModifiedFile = null;
        long lastModifiedTime = 0;
        for (File log : logs) {
            if (log.lastModified() > lastModifiedTime) {
                lastModifiedFile = log;
                lastModifiedTime = log.lastModified();
            }
        }
        return lastModifiedFile;
    }

    private class IISResponseTimeLogParser extends ResponseTimeLogParser {

        private DateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        public IISResponseTimeLogParser(File logFile) {
            super(logFile);
            setExcludes(IISResponseTimeDelegate.this.responseTimeConfiguration.getExcludes());
            setTransforms(IISResponseTimeDelegate.this.responseTimeConfiguration.getTransforms());
        }

        public synchronized void parseLog(CallTimeData callTimeData) throws IOException {
            log.info("Parsing response-time log file " + this.logFile + "...");
            BufferedReader in;
            long newOffset = 0;
            try {
                in = new BufferedReader(new FileReader(this.logFile));

                // record the length now, incase there are more log lines written while
                // we are parsing the ones in the file since the last time we checked it
                newOffset = this.logFile.length();
            } catch (FileNotFoundException e) {
                log.info("Response-time log file '" + this.logFile + "' does not exist.");
                return;
            }

            log.info("Filesize " + newOffset);
            log.info("Skipping " + previousOffset);
            in.skip(IISResponseTimeDelegate.this.previousOffset);
            IISResponseTimeDelegate.this.previousOffset = newOffset;

            String currentLine;
            while ((currentLine = in.readLine()) != null) {
                LogEntry logEntry;
                log.info("Parsing line: " + currentLine);
                try {
                    logEntry = parseLine(currentLine);
                } catch (Exception e) {
                    log.info("Problem parsing line [" + currentLine + "] - cause: " + e);
                    continue;
                }

                String url = logEntry.getUrl();

                // The URL should always begin with a slash. If it doesn't, log an error and skip the entry,
                // so we don't end up with bogus data in the DB.
                if (url.charAt(0) != '/') {
                    String truncatedUrl = url.substring(0, Math.min(url.length(), 120));
                    if (url.length() > 120) {
                        truncatedUrl += "...";
                    }
                    log.info("URL ('" + truncatedUrl
                        + "') parsed from response-time log file does not begin with '/'. " + "Line being parsed is ["
                        + currentLine + "].");
                    continue;
                }

                if (isExcluded(url)) {
                    log.info("URL was excluded");
                    continue;
                }

                // Only collect stats for successful (2xx or 3xx) requests...
                if ((logEntry.getStatusCode() != null)
                    && ((logEntry.getStatusCode() < 200) || (logEntry.getStatusCode() >= 400))) {
                    log.info("Status code was invalid: " + logEntry.getStatusCode());
                    continue;
                }

                String transformedUrl = applyTransforms(url);
                log.info("Original URL: " + url);
                log.info("Transformed: " + transformedUrl);
                try {
                    callTimeData.addCallData(transformedUrl, new Date(logEntry.getStartTime()), logEntry.getDuration());
                } catch (IllegalArgumentException iae) {
                    // if any issue with the data, log them and continue processing the rest of the report
                    log.error(iae);
                }
            }

            log.info("Results...");
            for (Map.Entry<String, CallTimeDataValue> callTime : callTimeData.getValues().entrySet()) {
                String url = callTime.getKey();
                CallTimeDataValue value = callTime.getValue();
                log.info("Calltime URL: " + url);
                log.info("Calltime Data: " + value);
            }

            in.close();
        }

        // E.g. format
        // 2008-04-18 19:56:18 127.0.0.1 GET /favicon.ico 404 0
        protected LogEntry parseLine(String line) throws Exception {
            LogEntry logEntry;
            try {
                String[] logEntryTokens = line.split(" ");

                String date = logEntryTokens[logTokenPositions.get(LogFormatToken.DATE)];
                String time = logEntryTokens[logTokenPositions.get(LogFormatToken.TIME)];
                String ipAddress = logEntryTokens[logTokenPositions.get(LogFormatToken.C_IP)];
                String url = logEntryTokens[logTokenPositions.get(LogFormatToken.CS_URI_STEM)];

                int httpStatus = Integer.parseInt(logEntryTokens[logTokenPositions.get(LogFormatToken.SC_STATUS)]);
                long duration = Long.parseLong(logEntryTokens[logTokenPositions.get(LogFormatToken.TIME_TAKEN)]);

                long startTime = dateParser.parse(date.trim() + " " + time.trim()).getTime();
                if (isAbsoluteTime) {
                    /* if we determine that IIS is recording log files in UTC, we need to 
                     * translate values into agent-local times; get the local start time by 
                     * adding the DST-based local offset from the UTC parsed time
                     */
                    int tzOffset = TimeZone.getDefault().getOffset(startTime);
                    startTime += tzOffset;
                }

                logEntry = new LogEntry(url, startTime, duration, httpStatus, ipAddress);
            } catch (RuntimeException e) {
                //default fields: time c-ip cs-method cs-uri-stem sc-status (also need 'date' and 'time-taken')
                throw new Exception("Failed to parse response time log file line [" + line + "]. "
                    + "Expected field format is 'date time c-ip cs-method cs-uri-stem sc-status'", e);
            }

            return logEntry;
        }
    }
}
