/*
 * Copyright 2011 Future Systems
 * 
 * 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.araqne.logstorage.engine;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.araqne.confdb.ConfigService;
import org.araqne.cron.PeriodicJob;
import org.araqne.logstorage.DiskLackAction;
import org.araqne.logstorage.DiskLackCallback;
import org.araqne.logstorage.DiskSpaceType;
import org.araqne.logstorage.LogRetentionPolicy;
import org.araqne.logstorage.LogStorage;
import org.araqne.logstorage.LogStorageMonitor;
import org.araqne.logstorage.LogStorageStatus;
import org.araqne.logstorage.LogTableRegistry;
import org.araqne.logstorage.TableLockedException;
import org.araqne.storage.api.FilePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@PeriodicJob("* * * * *")
@Component(name = "logstorage-monitor")
@Provides
public class LogStorageMonitorEngine implements LogStorageMonitor {
	private static final String DEFAULT_MIN_FREE_SPACE_TYPE = DiskSpaceType.Percentage.toString();
	private static final int DEFAULT_MIN_FREE_SPACE_VALUE = 5;
	private static final String DEFAULT_DISK_LACK_ACTION = DiskLackAction.StopLogging.toString();

	private final Logger logger = LoggerFactory.getLogger(LogStorageMonitorEngine.class.getName());

	@Requires
	private LogTableRegistry tableRegistry;

	@Requires
	private LogStorage storage;

	@Requires
	private ConfigService conf;

	// last check time, check retention policy and purge files for every hour
	private long lastPurgeCheck;

	private DiskSpaceType minFreeSpaceType;
	private int minFreeSpaceValue;
	private DiskLackAction diskLackAction;
	private Set<DiskLackCallback> diskLackCallbacks = new HashSet<DiskLackCallback>();

	private boolean stopByLowDisk;

	public LogStorageMonitorEngine() {
		reload();
	}

	private void reload() {
		minFreeSpaceType = DiskSpaceType.valueOf(getStringParameter(Constants.MinFreeDiskSpaceType, DEFAULT_MIN_FREE_SPACE_TYPE));
		minFreeSpaceValue = getIntParameter(Constants.MinFreeDiskSpaceValue, DEFAULT_MIN_FREE_SPACE_VALUE);
		diskLackAction = DiskLackAction.valueOf(getStringParameter(Constants.DiskLackAction, DEFAULT_DISK_LACK_ACTION));
	}

	@Override
	public int getMinFreeSpaceValue() {
		return minFreeSpaceValue;
	}

	@Override
	public DiskSpaceType getMinFreeSpaceType() {
		return minFreeSpaceType;
	}

	@Override
	public void setMinFreeSpace(int value, DiskSpaceType type) {
		if (type == DiskSpaceType.Percentage) {
			if (value <= 0 || value >= 100)
				throw new IllegalArgumentException("invalid value");
		} else if (type == DiskSpaceType.Megabyte) {
			if (value <= 0)
				throw new IllegalArgumentException("invalid value");
		} else if (type == null)
			throw new IllegalArgumentException("type cannot be null");

		this.minFreeSpaceType = type;
		this.minFreeSpaceValue = value;

		ConfigUtil.set(conf, Constants.MinFreeDiskSpaceType, type.toString());
		ConfigUtil.set(conf, Constants.MinFreeDiskSpaceValue, Integer.toString(value));
	}

	@Override
	public DiskLackAction getDiskLackAction() {
		return diskLackAction;
	}

	@Override
	public void setDiskLackAction(DiskLackAction action) {
		if (action == null)
			throw new IllegalArgumentException("action cannot be null");

		this.diskLackAction = action;

		ConfigUtil.set(conf, Constants.DiskLackAction, action.toString());
	}

	@Override
	public void registerDiskLackCallback(DiskLackCallback callback) {
		diskLackCallbacks.add(callback);
	}

	@Override
	public void unregisterDiskLackCallback(DiskLackCallback callback) {
		diskLackCallbacks.remove(callback);
	}

	@Override
	public void forceRetentionCheck() {
		checkRetentions(true);
	}

	private boolean isDiskLack(FilePath dir) {
		if (!dir.exists())
			return false;

		long usable = dir.getUsableSpace();
		long total = dir.getTotalSpace();

		logger.trace("araqne logstorage: check {} {} free space of partition [{}], current [{}] total [{}]", new Object[] {
				minFreeSpaceValue, minFreeSpaceType.toString().toLowerCase(), dir.getAbsolutePath(), usable, total });

		if (total == 0) {
			logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}]", new Object[] {
					dir.getAbsoluteFilePath(), usable, total });
			return true;
		}

		String unit = (minFreeSpaceType == DiskSpaceType.Percentage ? "%" : "MB");

		if (minFreeSpaceType == DiskSpaceType.Percentage) {
			int percent = (int) (usable * 100 / total);
			if (percent < minFreeSpaceValue) {
				logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}] percent [{}] threshold [{} {}]",
						new Object[] { dir.getAbsoluteFilePath(), usable, total, percent, minFreeSpaceValue, unit });

				return true;
			}
		} else if (minFreeSpaceType == DiskSpaceType.Megabyte) {
			int mega = (int) (usable / 1048576);
			if (mega < minFreeSpaceValue) {
				logger.error("araqne logstorage: low disk, dir [{}] usable [{}] total [{}] percent [{}] threshold [{} {}]",
						new Object[] { dir.getAbsoluteFilePath(), usable, total, mega, minFreeSpaceValue, unit });

				return true;
			}
		}

		return false;
	}

	@Override
	public void run() {
		try {
			runOnce();
		} catch (Exception e) {
			logger.error("araqne logstorage: storage monitor error", e);
		}
	}

	private void runOnce() {
		reload();
		checkRetentions(false);
		checkDiskLack();
	}

	private void checkRetentions(boolean force) {
		long now = System.currentTimeMillis();
		if (!force && now - lastPurgeCheck < 3600 * 1000)
			return;

		for (String tableName : tableRegistry.getTableNames()) {
			checkAndPurgeFiles(tableName);
		}

		lastPurgeCheck = now;
	}

	private void checkAndPurgeFiles(String tableName) {
		LogRetentionPolicy p = storage.getRetentionPolicy(tableName);
		if (p == null || p.getRetentionDays() == 0) {
			logger.debug("araqne logstorage: no retention policy for table [{}]", tableName);
			return;
		}

		// purge tables
		Date logBaseline = storage.getPurgeBaseline(tableName);
		if (logBaseline != null) {
			try {
				storage.purge(tableName, null, logBaseline);
			} catch (TableLockedException e) {
				logger.debug("purge skipped: table is locked by {}", e.getMessage());
			}
		}
	}

	private void checkDiskLack() {
		// categorize by partition path
		Map<FilePath, List<String>> partitionTables = new HashMap<FilePath, List<String>>();

		for (String tableName : tableRegistry.getTableNames()) {
			FilePath tableDir = storage.getTableDirectory(tableName);
			if (tableDir == null)
				continue;

			FilePath dir = tableDir.getAbsoluteFilePath().getParentFilePath();

			List<String> tables = partitionTables.get(dir);
			if (tables == null) {
				tables = new ArrayList<String>();
				partitionTables.put(dir, tables);
			}

			tables.add(tableName);
		}

		boolean lowDisk = false;
		for (FilePath dir : partitionTables.keySet()) {
			lowDisk |= checkDiskPartitions(dir, partitionTables.get(dir));
		}

		if (lowDisk) {
			for (DiskLackCallback callback : diskLackCallbacks) {
				try {
					callback.callback();
				} catch (Throwable t) {
					logger.warn("araqne logstorage: disk lack callback should not throw any exception", t);
				}
			}
		} else {
			// open log storage if low disk problem is resolved
			if (stopByLowDisk)
				startStorage();
		}
	}

	private boolean checkDiskPartitions(FilePath partitionPath, List<String> tableNames) {
		if (isDiskLack(partitionPath)) {
			logger.error("araqne logstorage: not enough disk space, current minimum free space config [{}] {}",
					minFreeSpaceValue, (minFreeSpaceType == DiskSpaceType.Percentage ? "%" : "MB"));

			if (diskLackAction == DiskLackAction.StopLogging) {
				if (storage.getStatus() == LogStorageStatus.Open) {
					stopStorage(partitionPath);
				}
			} else if (diskLackAction == DiskLackAction.RemoveOldLog) {
				List<LogFile> files = new ArrayList<LogFile>();
				for (String tableName : tableNames) {
					for (Date date : storage.getLogDates(tableName))
						files.add(new LogFile(tableName, date));
				}
				Collections.sort(files, new LogFileComparator());
				int index = 0;
				SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

				do {
					if (index >= files.size()) {
						if (storage.getStatus() == LogStorageStatus.Open) {
							stopStorage(partitionPath);
						}
						break;
					}

					LogFile lf = files.get(index++);
					logger.info("araqne logstorage: removing old log, table [{}], date [{}]", lf.tableName, sdf.format(lf.date));
					storage.purge(lf.tableName, lf.date);
				} while (isDiskLack(partitionPath));
			}

			return true;
		}

		return false;
	}

	private void startStorage() {
		logger.info("araqne logstorage: low disk problem is resolved, restart logstorage");
		storage.start();
		stopByLowDisk = false;
	}

	private void stopStorage(FilePath partitionPath) {
		logger.error("araqne logstorage: [{}] not enough space, stop logging", partitionPath.getAbsolutePath());
		storage.stop();
		stopByLowDisk = true;
	}

	private String getStringParameter(Constants key, String defaultValue) {
		String value = ConfigUtil.get(conf, key);
		if (value != null)
			return value;
		return defaultValue;
	}

	private int getIntParameter(Constants key, int defaultValue) {
		String value = ConfigUtil.get(conf, key);
		if (value != null)
			return Integer.valueOf(value);
		return defaultValue;
	}

	private class LogFile {
		private String tableName;
		private Date date;

		private LogFile(String tableName, Date date) {
			this.tableName = tableName;
			this.date = date;
		}
	}

	private class LogFileComparator implements Comparator<LogFile> {
		@Override
		public int compare(LogFile o1, LogFile o2) {
			return o1.date.compareTo(o2.date);
		}
	}
}
