/**
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.audit.spi;

import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.audit.AuditManager;
import org.jivesoftware.openfire.audit.Auditor;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketInterceptor;
import org.jivesoftware.openfire.session.Session;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;

import java.io.File;
import java.util.*;

/**
 * Implementation of the AuditManager interface.
 */
public class AuditManagerImpl extends BasicModule implements AuditManager, PropertyEventListener {

    private boolean enabled;
    private boolean auditMessage;
    private boolean auditPresence;
    private boolean auditIQ;
    private boolean auditXPath;
    private List<String> xpath = new LinkedList<>();
    private AuditorImpl auditor = null;
    /**
     * Max size in bytes that all audit log files may have. When the limit is reached
     * oldest audit log files will be removed until total size is under the limit.
     */
    private int maxTotalSize;
    /**
     * Max size in bytes that each audit log file may have. Once the limit has been
     * reached a new audit file will be created.
     */
    private int maxFileSize;
    /**
     * Max number of days to keep audit information. Once the limit has been reached
     * audit files that contain information that exceed the limit will be deleted.
     */
    private int maxDays;
    private int logTimeout;
    private String logDir;
    private Collection<String> ignoreList = new ArrayList<>();
    private static final int MAX_TOTAL_SIZE = 1000;
    private static final int MAX_FILE_SIZE = 10;
    private static final int MAX_DAYS = -1;
    private static final int DEFAULT_LOG_TIMEOUT = 120000;
    private AuditorInterceptor interceptor;

    public AuditManagerImpl() {
        super("Audit Manager");
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        JiveGlobals.setProperty("xmpp.audit.active", enabled ? "true" : "false");
        processEnabled(enabled);
    }

    @Override
    public Auditor getAuditor() {
        if (auditor == null) {
            throw new IllegalStateException("Must initialize audit manager first");
        }
        return auditor;
    }

    @Override
    public int getMaxTotalSize() {
        return maxTotalSize;
    }

    @Override
    public void setMaxTotalSize(int size) {
        maxTotalSize = size;
        auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
        JiveGlobals.setProperty("xmpp.audit.totalsize", Integer.toString(size));
    }

    @Override
    public int getMaxFileSize() {
        return maxFileSize;
    }

    @Override
    public void setMaxFileSize(int size) {
        maxFileSize = size;
        auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
        JiveGlobals.setProperty("xmpp.audit.filesize", Integer.toString(size));
    }

    @Override
    public int getMaxDays() {
        return maxDays;
    }

    @Override
    public void setMaxDays(int count) {
        maxDays = validateMaxDays(count);
        auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
        JiveGlobals.setProperty("xmpp.audit.days", Integer.toString(count));
    }

    @Override
    public int getLogTimeout() {
        return logTimeout;
    }

    @Override
    public void setLogTimeout(int logTimeout) {
        this.logTimeout = logTimeout;
        auditor.setLogTimeout(logTimeout);
        JiveGlobals.setProperty("xmpp.audit.logtimeout", Integer.toString(logTimeout));
    }

    @Override
    public String getLogDir() {
        return logDir;
    }

    @Override
    public void setLogDir(String logDir) {
        this.logDir = logDir;
        auditor.setLogDir(logDir);
        JiveGlobals.setProperty("xmpp.audit.logdir", logDir);
    }

    @Override
    public boolean isAuditMessage() {
        return auditMessage;
    }

    @Override
    public void setAuditMessage(boolean auditMessage) {
        this.auditMessage = auditMessage;
        JiveGlobals.setProperty("xmpp.audit.message", auditMessage ? "true" : "false");
    }

    @Override
    public boolean isAuditPresence() {
        return auditPresence;
    }

    @Override
    public void setAuditPresence(boolean auditPresence) {
        this.auditPresence = auditPresence;
        JiveGlobals.setProperty("xmpp.audit.presence", auditPresence ? "true" : "false");
    }

    @Override
    public boolean isAuditIQ() {
        return auditIQ;
    }

    @Override
    public void setAuditIQ(boolean auditIQ) {
        this.auditIQ = auditIQ;
        JiveGlobals.setProperty("xmpp.audit.iq", Boolean.toString(auditIQ));
    }

    @Override
    public boolean isAuditXPath() {
        return auditXPath;
    }

    @Override
    public void setAuditXPath(boolean auditXPath) {
        this.auditXPath = auditXPath;
        JiveGlobals.setProperty("xmpp.audit.xpath", Boolean.toString(auditXPath));
    }

    @Override
    public void addXPath(String xpathExpression) {
        xpath.add(xpathExpression);
        saveXPath();
    }

    @Override
    public void removeXPath(String xpathExpression) {
        xpath.remove(xpathExpression);
        saveXPath();
    }

    private void saveXPath() {
        // TODO: save XPath values!
        //String[] filters = new String[xpath.size()];
        //filters = (String[]) xpath.toArray(filters); 
    }

    @Override
    public Iterator getXPathFilters() {
        return xpath.iterator();
    }

    @Override
    public void setIgnoreList(Collection<String> usernames) {
        if (ignoreList.equals(usernames)) {
            return;
        }
        ignoreList = usernames;
        // Encode the collection
        StringBuilder ignoreString = new StringBuilder();
        for (String username : ignoreList) {
            if (ignoreString.length() == 0) {
                ignoreString.append(username);
            }
            else {
                ignoreString.append(',').append(username);
            }
        }
        JiveGlobals.setProperty("xmpp.audit.ignore", ignoreString.toString());
    }

    @Override
    public Collection<String> getIgnoreList() {
        return Collections.unmodifiableCollection(ignoreList);
    }

    // #########################################################################
    // Basic module methods
    // #########################################################################

    @Override
	public void initialize(XMPPServer server) {
        super.initialize(server);
        enabled = JiveGlobals.getBooleanProperty("xmpp.audit.active");
        auditMessage = JiveGlobals.getBooleanProperty("xmpp.audit.message");
        auditPresence = JiveGlobals.getBooleanProperty("xmpp.audit.presence");
        auditIQ = JiveGlobals.getBooleanProperty("xmpp.audit.iq");
        auditXPath = JiveGlobals.getBooleanProperty("xmpp.audit.xpath");
        // TODO: load xpath values!
//        String[] filters = context.getProperties("xmpp.audit.filter.xpath");
//        for (int i = 0; i < filters.length; i++) {
//            xpath.add(filters[i]);
//        }
        maxTotalSize = JiveGlobals.getIntProperty("xmpp.audit.totalsize", MAX_TOTAL_SIZE);
        maxFileSize = JiveGlobals.getIntProperty("xmpp.audit.filesize", MAX_FILE_SIZE);
        maxDays = JiveGlobals.getIntProperty("xmpp.audit.days", MAX_DAYS);
        logTimeout = JiveGlobals.getIntProperty("xmpp.audit.logtimeout", DEFAULT_LOG_TIMEOUT);
        logDir = JiveGlobals.getProperty("xmpp.audit.logdir", JiveGlobals.getHomeDirectory() +
                File.separator + "logs");
		processIgnoreString(JiveGlobals.getProperty("xmpp.audit.ignore", ""));

        auditor = new AuditorImpl(this);
        auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
        auditor.setLogDir(logDir);
        auditor.setLogTimeout(logTimeout);

        interceptor = new AuditorInterceptor();
		processEnabled(enabled);
		PropertyEventDispatcher.addListener(this);
    }

	private void processIgnoreString(String ignoreString) {
		ignoreList.clear();
		// Decode the ignore list
		StringTokenizer tokenizer = new StringTokenizer(ignoreString, ",");
		while (tokenizer.hasMoreTokens()) {
			String username = tokenizer.nextToken().trim();
			ignoreList.add(username);
		}
	}
	
	private void processEnabled(boolean enabled) {
		// Add or remove the auditor interceptor depending on the enabled status
		if (enabled) {
			InterceptorManager.getInstance().addInterceptor(interceptor);
		} else {
			InterceptorManager.getInstance().removeInterceptor(interceptor);
		}
	}

	private int validateMaxDays(int count) {
		if (count < -1) {
			count = -1;
		}
		if (count == 0) {
			count = 1;
		}
		return count;
	}
    
    @Override
	public void stop() {
        if (auditor != null) {
            auditor.stop();
        }
    }

	@Override
	public void propertySet(String property, Map<String, Object> params) {
		String value = (String) params.get("value");
		switch (property) {
			case "xmpp.audit.active":
				enabled = Boolean.parseBoolean(value);
				processEnabled(enabled);
				break;
			case "xmpp.audit.message":
				auditMessage = Boolean.parseBoolean(value);
				break;
			case "xmpp.audit.presence":
				auditPresence = Boolean.parseBoolean(value);
				break;
			case "xmpp.audit.iq":
				auditIQ = Boolean.parseBoolean(value);
				break;
			case "xmpp.audit.xpath":
				auditXPath = Boolean.parseBoolean(value);
				break;
			case "xmpp.audit.totalsize":
				maxTotalSize = parseIntegerOrDefault(value, MAX_TOTAL_SIZE);
				auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
				break;
			case "xmpp.audit.filesize":
				maxFileSize = parseIntegerOrDefault(value, MAX_FILE_SIZE);
				auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
				break;
			case "xmpp.audit.days":
				maxDays = validateMaxDays(parseIntegerOrDefault(value, MAX_DAYS));
				auditor.setMaxValues(maxTotalSize, maxFileSize, maxDays);
				break;
			case "xmpp.audit.logtimeout":
				logTimeout = parseIntegerOrDefault(value, DEFAULT_LOG_TIMEOUT);
				auditor.setLogTimeout(logTimeout);
				break;
			case "xmpp.audit.logdir":
				File d = null;
				if (value != null && !"".equals(value.trim())) {
					d = new File(value);
				}
				logDir = (d == null || !d.exists() || !d.canRead() || !d.canWrite() || !d
						.isDirectory()) ? JiveGlobals.getHomeDirectory()
						+ File.separator + "logs" : value;
				auditor.setLogDir(logDir);
				break;
			case "xmpp.audit.ignore":
				processIgnoreString(value);
				break;
		}
	}

	private int parseIntegerOrDefault(String intValue, int defaultValue) {
		try {
			return Integer.parseInt(intValue);
		} catch (NumberFormatException nfe) {
			return defaultValue;
		}
	}
	
	@Override
	public void propertyDeleted(String property, Map<String, Object> params) {
		propertySet(property, Collections.<String, Object>emptyMap());
	}

	@Override
	public void xmlPropertySet(String property, Map<String, Object> params) {
	}

	@Override
	public void xmlPropertyDeleted(String property, Map<String, Object> params) {
	}    
    
    private class AuditorInterceptor implements PacketInterceptor {

        @Override
        public void interceptPacket(Packet packet, Session session, boolean read, boolean processed) {
            if (!processed) {
                // Ignore packets sent or received by users that are present in the ignore list
                JID from = packet.getFrom();
                JID to = packet.getTo();
                if ((from == null || !ignoreList.contains(from.getNode())) &&
                        (to == null || !ignoreList.contains(to.getNode()))) {
                    auditor.audit(packet, session);
                }
            }
        }
    }
}
