/*
 * Created on Aug 2, 2006
 * 
 * (C) Copyright TANDBERG Television Ltd.
 */

package com.tandbergtv.workflow.sanmanager.internal;

import static com.tandbergtv.workflow.sanmanager.entities.DriveStatus.ERROR;
import static com.tandbergtv.workflow.sanmanager.entities.DriveStatus.OK;
import static com.tandbergtv.workflow.sanmanager.entities.DriveStatus.WARNING;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import com.tandbergtv.workflow.comm.IDestination;
import com.tandbergtv.workflow.comm.routing.IRoutingService;
import com.tandbergtv.workflow.comm.routing.RoutingServiceFactory;
import com.tandbergtv.workflow.core.event.ColleaguePriority;
import com.tandbergtv.workflow.core.event.DefaultMediator;
import com.tandbergtv.workflow.core.event.IColleague;
import com.tandbergtv.workflow.core.event.IMediator;
import com.tandbergtv.workflow.core.event.WorkflowEvent;
import com.tandbergtv.workflow.message.WorkflowMessage;
import com.tandbergtv.workflow.message.WorkflowMessageFactory;
import com.tandbergtv.workflow.message.WorkflowPayload;
import com.tandbergtv.workflow.message.WorkflowMessage.MessageType;
import com.tandbergtv.workflow.sanmanager.SANManagement;
import com.tandbergtv.workflow.sanmanager.SANManagementException;
import com.tandbergtv.workflow.sanmanager.dto.ISANFile;
import com.tandbergtv.workflow.sanmanager.dto.SANFile;
import com.tandbergtv.workflow.sanmanager.dto.SANFolder;
import com.tandbergtv.workflow.sanmanager.entities.DriveStatus;
import com.tandbergtv.workflow.sanmanager.entities.SANDrive;
import com.tandbergtv.workflow.sanmanager.entities.SANDriveHistory;
import com.tandbergtv.workflow.sanmanager.hibernate.DriveHDAO;
import com.tandbergtv.workflow.sanmanager.hibernate.DriveHistoryHDAO;

/**
 * Caches and maintains information about all the SAN Drives
 * 
 * @author Vijay Silva
 */
public class SANManager implements SANManagement, IColleague
{
	private static final String COLLEAGUE_NAME = "SANManager";

	private static final String DRIVE_FREE_SPACE_MESSAGE_UID = "010706";

	private static final String FILE_LIST_MESSAGE_UID = "010709";

	private static final String FOLDER_LIST_MESSAGE_UID = "010710";

	private static final String FOLDER_PATH_PARAM = "Path";

	private static final String DRIVE_FREE_SPACE = "FreeSpace";

	private static final String FOLDER_LIST_RESULT_PARAM = "Result";

	private static final String OUTPUT_ERROR_MESSAGE = "error-message";

	private static final String OUTPUT_ERROR_STACK = "error-stack";

	private static final Logger logger = Logger.getLogger(SANManager.class);

	private Map<Long, SANDrive> driveMap = new HashMap<Long, SANDrive>();

	private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

	private ScheduledFuture future = null;

	private boolean initialized = false;

	private SessionFactory factory;
	
	private IDestination destination;
	
	private long delay;
	
	private static final long INVALID_USED_SPACE = -1L;

	// ========================================================================
	// ====================== CLASS CONSTRUCTOR AND INITIALIZATION
	// ========================================================================

	/*
	 * Constructor
	 */
	public SANManager(SessionFactory factory, IDestination destination, long delay)
	{
		this.factory = factory;
		this.destination = destination;
		this.delay = delay;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	@Override
	public String getServiceName()
	{
		return "IO Manager";
	}

	/**
	 * @return the factory
	 */
	public SessionFactory getSessionFactory()
	{
		return factory;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	@Override
	public void start()
	{
		if (this.initialized)
			return;

		cacheSANDrives();

		Runnable command = new DriveUpdateCommand();
		this.future = this.executorService.scheduleWithFixedDelay(command, 0, this.delay,
				TimeUnit.SECONDS);

		IMediator mediator = DefaultMediator.getInstance();
		mediator.register(this);

		this.initialized = true;

		logger.info("Successfully initialized the SAN Manager.");
	}

	/*
	 * Fetch all SAN drives from database and place them in the cache.
	 */
	private void cacheSANDrives()
	{
		List<SANDrive> driveList = null;
		Session session = getCurrentSession();

		try
		{
			session.beginTransaction();

			DriveHDAO driveHDAO = new DriveHDAO(session);
			driveList = driveHDAO.findAll();

			session.getTransaction().commit();
		}
		catch (RuntimeException ex)
		{
			rollbackTransaction(session.getTransaction());
			throw ex;
		}

		for (SANDrive drive : driveList)
		{
			drive.setUsedSpace(INVALID_USED_SPACE);
			drive.setStatus(DriveStatus.ERROR);
			this.driveMap.put(drive.getId(), drive);
		}
	}

	// ========================================================================
	// ====================== SAN MANAGEMENT METHODS
	// ========================================================================

	/**
	 * @see com.tandbergtv.workflow.sanmanager.SANManagement#getAllSANDrives()
	 */
	public synchronized List<SANDrive> getAllSANDrives()
	{
		List<SANDrive> result = new ArrayList<SANDrive>();

		// Sort the keys by Id
		List<Long> keys = new ArrayList<Long>(this.driveMap.keySet());
		Collections.sort(keys);

		for (long driveId : keys)
		{
			result.add(this.getDriveCopy(driveId));
		}

		return result;
	}

	public synchronized SANDrive getSANDrive(String name) throws SANManagementException
	{
		for (SANDrive drive : driveMap.values())
		{
			if(drive.getName().equals(name))
				return getDriveCopy(drive.getId());
		}

		String msg = "Cannot find drive with Name=" + name + ", no such Drive exists.";
		throw new SANManagementException(msg);
	}

	/**
	 * @see com.tandbergtv.workflow.sanmanager.SANManagement#getSANDrive(long)
	 */
	public synchronized SANDrive getSANDrive(long driveId) throws SANManagementException
	{
		Long key = new Long(driveId);

		if (!this.driveMap.containsKey(key))
		{
			String msg = "Cannot find drive with Id=" + driveId + ", no such Drive exists.";
			throw new SANManagementException(msg);
		}

		return this.getDriveCopy(driveId);
	}

	/**
	 * @see com.tandbergtv.workflow.sanmanager.SANManagement#getSANDriveCount()
	 */
	public synchronized int getSANDriveCount()
	{
		return driveMap.size();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	@Override
	public void stop()
	{
		synchronized (this)
		{
			if (!this.initialized)
				return;

			IMediator mediator = DefaultMediator.getInstance();
			mediator.unregister(this);

			if (future != null)
				this.future.cancel(false);

			this.executorService.shutdownNow();
			this.driveMap.clear();

			this.initialized = false;
		}

		try
		{
			boolean completed = this.executorService.awaitTermination(15L, TimeUnit.SECONDS);
			if (!completed)
				logger.warn("The SAN Manager Executor Service has not terminated.");
		}
		catch (InterruptedException ie)
		{
			logger.warn("Interrupted when waiting for the SAN Executor to terminate.", ie);
		}

		logger.info("Finished shutting down the SAN Manager.");
	}

	/**
	 * @see com.tandbergtv.workflow.sanmanager.SANManagement#getFileListing(java.lang.String)
	 */
	@SuppressWarnings("unchecked")
	public List<SANFile> getFileListing(String sanFolderPath) throws SANManagementException
	{
		return (List<SANFile>) getFileList(sanFolderPath, true);
	}

	/**
	 * @see com.tandbergtv.workflow.sanmanager.SANManagement#getFolderListing(java.lang.String)
	 */
	@SuppressWarnings("unchecked")
	public List<SANFolder> getFolderListing(String sanFolderPath) throws SANManagementException
	{
		return (List<SANFolder>) getFileList(sanFolderPath, false);
	}

	// ========================================================================
	// ====================== COLLEAGUE METHODS
	// ========================================================================

	/**
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleagueName()
	 */
	public String getColleagueName()
	{
		return COLLEAGUE_NAME;
	}

	/**
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleaguePriority()
	 */
	public ColleaguePriority getColleaguePriority()
	{
		return ColleaguePriority.LOW;
	}

	/**
	 * @see com.tandbergtv.workflow.core.event.IColleague#receive(com.tandbergtv.workflow.core.event.WorkflowEvent)
	 */
	public void receive(WorkflowEvent event)
	{ // Currently does not handle any Workflow Events
	}

	// ========================================================================
	// ====================== SAN TRENDING METHODS
	// ========================================================================

	/*
	 * Method to store the Drive Data for trending and update the Drive in the database.
	 */
	private void storeDriveTrendData(long driveId)
	{
		// The Used Space of the Drive will be -1 if there was an error getting the Folder Size
		// In this case, do not store the Drive trend data.
		SANDrive cachedDrive = this.driveMap.get(driveId);
		if (cachedDrive.getUsedSpace() == -1)
			return;

		Session session = getCurrentSession();

		try
		{
			session.beginTransaction();

			SANDriveHistory driveHistory = prepareDriveHistoryDO(driveId);
			DriveHDAO driveDAO = new DriveHDAO(session);
			DriveHistoryHDAO driveHistoryDAO = new DriveHistoryHDAO(session);
			driveDAO.update(cachedDrive);
			driveHistoryDAO.update(driveHistory);

			session.getTransaction().commit();
		}
		catch (RuntimeException ex)
		{ // Transaction failed, rollback and log the error
			rollbackTransaction(session.getTransaction());
			logger.error("Error saving SAN Drive[" + cachedDrive.getId() + " ]: "
					+ cachedDrive.getName() + " and store trend data.", ex);
		}
	}

	/*
	 * Create the SAN Drive History object given the driveId of the SAN Drive after its properties
	 * have been updated.
	 */
	private SANDriveHistory prepareDriveHistoryDO(long driveID)
	{
		SANDrive drive = this.driveMap.get(driveID);

		SANDriveHistory driveHistory = new SANDriveHistory();
		driveHistory.setSanDriveID(driveID);
		driveHistory.setSampleDate(drive.getSampleDate());
		driveHistory.setStatus(drive.getStatus());
		driveHistory.setCapacity(drive.getCapacity());
		driveHistory.setUsedBytes(drive.getUsedSpace());
		driveHistory.setWarningThresholdPercent(drive.getWarningThreshold());
		driveHistory.setErrorThresholdPercent(drive.getErrorThreshold());

		return driveHistory;
	}

	// ========================================================================
	// ====================== FILE / FOLDER LISTING METHODS
	// ========================================================================

	/*
	 * Method to get back the list of either files or folders that exist at the specified path.
	 */
	private List<? extends ISANFile> getFileList(String path, boolean filesOnly)
			throws SANManagementException
	{
		WorkflowMessage message = createFileListMessage(path, filesOnly);
		List<IDestination> destinations = createFileSubsystemDestination();
		List<? extends ISANFile> sanFileList = null;

		try
		{
			IRoutingService service = RoutingServiceFactory.newInstance().createRoutingService();
			WorkflowMessage response = service.send(message, destinations);

			if (MessageType.ack == response.getType())
			{
				WorkflowPayload payload = (WorkflowPayload) response.getPayload();
				String result = payload.getValue(FOLDER_LIST_RESULT_PARAM);
				Object list = XMLObjectSerializer.deserializeObject(result);

				if (filesOnly)
				{
					List<SANFile> fileList = new ArrayList<SANFile>();
					for (Object obj : ((List) list))
					{
						fileList.add((SANFile) obj);
					}

					sanFileList = fileList;
				}
				else
				{
					List<SANFolder> folderList = new ArrayList<SANFolder>();
					for (Object obj : ((List) list))
					{
						folderList.add((SANFolder) obj);
					}

					sanFileList = folderList;
				}
			}
			else
			{
				WorkflowPayload payload = (WorkflowPayload) response.getPayload();
				String errorMessage = payload.getValue(OUTPUT_ERROR_MESSAGE);
				String errorStack = payload.getValue(OUTPUT_ERROR_STACK);

				logger.debug("Failed to get the Folder Listing for Path: " + path + ", "
						+ "Error Stack: " + errorStack);

				String msg = "Failed to get the Folder Listing for Path: " + path + ", Error: "
						+ errorMessage;
				throw new SANManagementException(msg);
			}
		}
		catch (SANManagementException sme)
		{
			throw sme;
		}
		catch (Exception ex)
		{
			String msg = "Failed to get the Folder Listing for Path: " + path;
			throw new SANManagementException(msg, ex);
		}

		return sanFileList;
	}

	/*
	 * Method to create the Workflow Message to make a request to find the Folder size on the SAN.
	 */
	private WorkflowMessage createFileListMessage(String path, boolean filesOnly)
	{
		String uidValue = filesOnly ? FILE_LIST_MESSAGE_UID : FOLDER_LIST_MESSAGE_UID;
		WorkflowMessage message = WorkflowMessageFactory.createControlMessage(uidValue);
		message.putValue(FOLDER_PATH_PARAM, path);

		return message;
	}

	// ========================================================================
	// ====================== SAN DRIVE STATISTICS METHODS
	// ========================================================================

	/*
	 * Method to update the Drive Properties by querying the File Subsystem for used space.
	 */
	private void updateDrive(SANDrive drive)
	{
		long free = this.getFreeSpace(drive);

		synchronized (this)
		{
			this.updateDriveProperties(drive.getId(), free);
			this.storeDriveTrendData(drive.getId());
		}
	}

	/*
	 * We're doing two things here - update the used space of the drive and then determine if we
	 * need to raise some sort of alarm if the usage exceeds some threshold
	 */
	private void updateDriveProperties(long driveId, long free)
	{
		SANDrive cachedDrive = this.driveMap.get(driveId);
		if (cachedDrive == null)
		{
			logger.warn("Cannot update SAN Drive properties for Drive: " + driveId
					+ ", no such drive exists.");
			return;
		}

		DriveStatus previous = cachedDrive.getStatus();
		DriveStatus status = OK;
		
		if (free < 0)
		{ // Error Condition
			status = ERROR;
			
			cachedDrive.setStatus(status);
			cachedDrive.setUsedSpace(INVALID_USED_SPACE);
			
			if (previous != status)
				logger.warn(cachedDrive.getName() + ", drive status " + status);
		}
		else
		{
			long previousused = cachedDrive.getUsedSpace();
			long capacity = cachedDrive.getCapacity();
			long used = (capacity >= free) ? capacity - free : 0;
			cachedDrive.setUsedSpace(used);

			double usedPercentage = (capacity > 0) ? used / (double) capacity * 100D : 100D;
			logger.debug("The Used Percentage for the Drive: " + usedPercentage);
			
			if (usedPercentage >= cachedDrive.getErrorThreshold())
				status = ERROR;
			else if (usedPercentage >= cachedDrive.getWarningThreshold())
				status = WARNING;
			
			cachedDrive.setStatus(status);
			
			if (status.ordinal() >= WARNING.ordinal() && 
				(previousused == INVALID_USED_SPACE || previous != status))
				logger.warn(cachedDrive.getName() + ", drive status " + status);
		}

		cachedDrive.setSampleDate(new Date());
	}

	/*
	 * Method to get the Size of the Folder by querying the File SubSystem via the adaptor
	 */
	private long getFreeSpace(SANDrive drive)
	{
		WorkflowMessage message = createFolderSizeMessage(drive);
		List<IDestination> destinations = createFileSubsystemDestination();

		long free = -1L;

		try
		{
			IRoutingService service = RoutingServiceFactory.newInstance().createRoutingService();
			WorkflowMessage response = service.send(message, destinations);
			WorkflowPayload payload = (WorkflowPayload) response.getPayload();

			if (MessageType.ack == response.getType())
			{
				// Careful - the message parameter key must be synced with the file system webservice
				String result = payload.getValue(DRIVE_FREE_SPACE);
				free = Long.parseLong(result);
			}
			else
			{
				String errorMessage = payload.getValue(OUTPUT_ERROR_MESSAGE);
				String errorStack = payload.getValue(OUTPUT_ERROR_STACK);

				logger.error("Failed to get the Folder Size for Drive: " + drive.getPath() + ": "
						+ errorMessage);
				logger.debug("The Error Stack when getting Folder Size: " + errorStack);
			}
		}
		catch (Exception ex)
		{
			logger.error("Failed to get the Folder Size for Drive: " + drive.getPath(), ex);
		}

		return free;
	}

	/*
	 * Method to create the Workflow Message to make a request to find the Folder size on the SAN.
	 */
	private WorkflowMessage createFolderSizeMessage(SANDrive drive)
	{
		WorkflowMessage message = WorkflowMessageFactory.createControlMessage(DRIVE_FREE_SPACE_MESSAGE_UID);
		String folderPath = drive.getPath();
		message.putValue(FOLDER_PATH_PARAM, folderPath);

		return message;
	}

	// ========================================================================
	// ====================== UTILITY METHODS
	// ========================================================================

	/*
	 * Method to create the Destination to which the Workflow Message to get the Folder Size needs
	 * to be sent to.
	 */
	private List<IDestination> createFileSubsystemDestination()
	{
		List<IDestination> destinations = new ArrayList<IDestination>();
		
		destinations.add(this.destination);

		return destinations;
	}

	/*
	 * Method to get the SAN Drive given the ID and clone the drive
	 */
	private SANDrive getDriveCopy(long driveId)
	{
		SANDrive drive = this.driveMap.get(driveId);

		if (drive != null)
			drive = (SANDrive) drive.clone();

		return drive;
	}

	/*
	 * Gets the Hibernate Session associated with the current thread.
	 */
	private Session getCurrentSession()
	{
		return this.factory.getCurrentSession();
	}

	/*
	 * Rollback the transaction and logs any errors when attempting to rollback transaction.
	 */
	private void rollbackTransaction(Transaction transaction)
	{
		try
		{
			transaction.rollback();
		}
		catch (RuntimeException ex)
		{
			logger.error("Failed to roll back the transaction.", ex);
		}
	}

	// ========================================================================
	// ====================== THREAD TO UPDATE DRIVE STATISTICS
	// ========================================================================

	/*
	 * Internal Class that uses a thread to query the File Subsystem to update the Drive Properties.
	 */
	private class DriveUpdateCommand implements Runnable
	{
		/**
		 * @see java.lang.Runnable#run()
		 */
		public void run()
		{
			logger.debug("Starting the Drive Update Thread to update all the Drive Properties.");

			try
			{
				List<SANDrive> driveList = SANManager.this.getAllSANDrives();
				for (SANDrive drive : driveList)
				{
					try
					{
						logger.debug("Updating properties for Drive[" + drive.getId() + "]: "
								+ drive.getPath());
						SANManager.this.updateDrive(drive);
					}
					catch (Exception ex)
					{ // Log any exception, and continue to next drive
						logger.error("Failed to store trend data and update SAN Drive["
								+ drive.getId() + "]: " + drive.getPath(), ex);
					}
				}
			}
			catch (Exception ex)
			{
				String msg = "Thread to update the Drive Configuration caught unexpected error.";
				logger.error(msg, ex);
			}

			logger.debug("Finished the Drive Update Thread to update all the Drive Properties.");
		}

		// ========================================================================
		// ====================== TEST METHODS
		// ========================================================================

		/*
		 * Test Method to check the File Listing
		 */
		@SuppressWarnings("unused")
		private void testFileListing(SANDrive drive)
		{
			try
			{
				List<SANFile> sanFiles = SANManager.this.getFileListing(drive.getPath());
				StringBuilder builder = new StringBuilder();
				builder.append("\n");
				builder.append("File listing[" + sanFiles.size() + "] for folder: "
						+ drive.getPath());
				builder.append("\n");

				for (SANFile file : sanFiles)
				{
					builder.append("\tFile: " + file.getName());
					builder.append(", Path: " + file.getAbsolutePath());
					builder.append(", LMD: " + file.getLastModified());
					builder.append(", Size: " + file.getLength());
					builder.append("\n");
				}
				builder.append("\n");

				List<SANFolder> sanFolders = SANManager.this.getFolderListing(drive.getPath());
				builder.append("Folder listing[" + sanFolders.size() + "] for folder: "
						+ drive.getPath());
				builder.append("\n");

				for (SANFolder file : sanFolders)
				{
					builder.append("\tFile: " + file.getName());
					builder.append(", Path: " + file.getAbsolutePath());
					builder.append(", LMD: " + file.getLastModified());
					builder.append(", Child Folder Count: " + file.getChildFolderCount());
					builder.append("\n");
				}
				builder.append("\n");
				builder.append("\n");

				logger.debug("SAN Stats: " + builder.toString());
			}
			catch (Exception ex)
			{
				String msg = "Thread to update the Drive Configuration caught unexpected error.";
				logger.error(msg, ex);
			}
		}
	}

}
