package com.ttv.acs.beans.serviceHandlers;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.vfs.FileSystemException;
import org.apache.log4j.Logger;

import com.tandbergtv.watchpoint.pmm.core.TitleValidationException;
import com.tandbergtv.watchpoint.pmm.core.TitleValidationMessage;
import com.tandbergtv.watchpoint.pmm.entities.ISavedTitle;
import com.tandbergtv.watchpoint.pmm.entities.ITitleActionSource;
import com.tandbergtv.watchpoint.pmm.entities.Title;
import com.tandbergtv.watchpoint.pmm.entities.TitleActionSource;
import com.tandbergtv.watchpoint.pmm.title.ingest.ITitleIngester;
import com.tandbergtv.watchpoint.pmm.title.ingest.TitleIngestException;
import com.ttv.acs.stub.adi.ADI;
import com.ttv.acs.stub.adi.ADIParseUtil;
import com.ttv.acs.stub.adi.AbstractWorkOrderRequest;
import com.ttv.acs.stub.adi.Asset;
import com.ttv.acs.stub.adi.IngestWorkOrderRequest;
import com.ttv.acs.stub.adi.PostIngestWorkOrderRequest;
import com.ttv.acs.util.ADIUtil;
import com.ttv.acs.util.ApplicationProperties;
import com.ttv.acs.util.ChecksumUtil;
import com.ttv.acs.util.FileURIConverter;
import com.ttv.acs.util.PConstants;
import com.ttv.acs.util.VirtualFileSystemManager;
import com.ttv.remote.RemoteClientFactory;

/**
 * LocalContentStoreHandler
 * <p>
 * Manage the local content store and provide a single point of entry into the
 * physical store.
 * 
 * @author <a href="mailto:cbrown@tandbergtv.com">Corey Brown</a>
 * 
 */
public class LocalContentStoreHandler {

	private static LocalContentStoreHandler instance = null;
	private final Logger log = Logger.getLogger(LocalContentStoreHandler.class);
	
	private static String DATE_FORMAT = "MM-dd-yyyy";
	
	private static String TIME_FORMAT = "HHmmssSSS";

	public static synchronized LocalContentStoreHandler getInstance() {
		if (instance == null) {
			instance = new LocalContentStoreHandler();
		}

		return instance;
	}

	/**
	 * <b>provisionPackage</b>
	 * <p>
	 * Provision a package into the content store
	 * 
	 * @param packageName
	 * @param URL
	 * @throws Exception
	 */
	public AbstractWorkOrderRequest provisionPackage(String packageName, String URL)
			throws Exception {
		log.info("Received provision request for package : " + packageName
				+ ", at URL: " + URL);

		boolean provisionResult = true;
		List<String> errorMsgs = new ArrayList<String>();

		// ADI.XML or ADI.xml
		String adiXMLFileName = getAdiXMLFileName(URL);

		ADI adi = null;
		try {
			adi = getMetadata(packageName, URL, adiXMLFileName);
		} catch (Exception ex) {
			String errMsg = "Failed to retrieve/parse URL: " + URL;
			log.error(errMsg, ex);
			errorMsgs.add(errMsg);
			provisionResult = false;
		}

		//
		// Acquire the content
		//
		String metadataFilePath = URL;
		if (adi != null) {
			Iterator<Asset> it = adi.getAssets().iterator();
			while (it.hasNext()) {
				Asset asset = it.next();

				boolean acquireResourceResult = true;
				try {
					provisionAsset(asset, adi, metadataFilePath, adiXMLFileName);	
				} catch (Exception ex) {
					log.error(ex.getMessage(), ex);
					errorMsgs.add(ex.getMessage());
					provisionResult = false;
					acquireResourceResult = false;
					it.remove();
				}
				String destinationStr = asset.getPath();
				if (ADIUtil.getCheckChecksumFlag() && acquireResourceResult
						&& !validateCheckSum(destinationStr, asset
								.getCheckSum())) {
					provisionResult = false;
					errorMsgs.add("Checksum validation failed for content "
							+ asset.getContent());
					it.remove();
				}
			}
			metadataFilePath = adi.
			getContentStoreDirectoryAbsolutePath()
					+ File.separator + adiXMLFileName;
		}

		
		if(useClassicMode()) {
			return constructIngestWorkorderRequest(metadataFilePath, provisionResult, errorMsgs, adi);			
		} else {
			return ingestPackageUsingNewMode(adi, metadataFilePath, provisionResult, errorMsgs);
		}
	}

	private AbstractWorkOrderRequest ingestPackageUsingNewMode(ADI adi, String metadataFilePath, boolean provisionResult, List<String> errorMsgs) {
		if(provisionResult == false) {
			return constructProvisionFailurePostIngestWorkorderRequest(metadataFilePath, adi, errorMsgs);
		}
		
		try {
			ISavedTitle savedTitle = ingestPackage(adi, metadataFilePath);
			
			if(sendIngestedEvent()) {
				titleIngester().sendIngestedEvent(savedTitle.getTitle().getId());
			}
			
			List<TitleValidationMessage> validationMessages = validateTitle(savedTitle.getTitle());
			if(validationMessages != null && validationMessages.size() > 0) {
				return constructValidationFailurePostIngestWorkorderRequest(metadataFilePath, adi, savedTitle, validationMessages);
			}
			
			return constructSuccessPostIngestWorkorderRequest(savedTitle, metadataFilePath, adi);
		} catch(TitleValidationException e) {
			return constructValidationFailurePostIngestWorkorderRequest(metadataFilePath, adi, e.getMessage());
		} catch (Exception e) {
			return constructProvisionFailurePostIngestWorkorderRequest(metadataFilePath, adi, e.getMessage());
		}
	}

	private List<TitleValidationMessage> validateTitle(Title title) throws TitleIngestException {
		return titleIngester().validate(title);
	}

	private AbstractWorkOrderRequest constructProvisionFailurePostIngestWorkorderRequest(String metadataFilePath, ADI adi, List<String> errorMsgs) {
		String errorMessage = join(errorMsgs, "\n");
		return constructProvisionFailurePostIngestWorkorderRequest(metadataFilePath, adi, errorMessage);
	}
	
	// Joins a collection into a string whose elements are separeted by the specified delimiter
	public static String join(Collection<?> collection, String delimiter) {
        StringBuilder sb = new StringBuilder();
        Iterator<?> it = collection.iterator();
        
        while (it.hasNext()) {
            sb.append(it.next());
            if (it.hasNext()) {
                sb.append(delimiter);
            }
        }
        
        return sb.toString();
    }


	private ISavedTitle ingestPackage(ADI adi, String metadataFilePath) throws TitleIngestException, TitleValidationException {
		String packageFolderPath = adi.getContentStoreDirectoryAbsolutePath();
		
		List<String> assetsNotInPackage = findAssetsNotInPackageFolder(adi);
		
		String providerId = adi.getMetadata().getProviderId();
		
		String contentClass = getConfiguredContentClass();
		ITitleActionSource source = newTitleActionSource();
		
		ISavedTitle savedTitle = titleIngester().ingestPackage(metadataFilePath, packageFolderPath, assetsNotInPackage, providerId,
				contentClass, source);
		
		return savedTitle;
	}

	private List<String> findAssetsNotInPackageFolder(ADI adi) {
		List<String> assetsNotInPackage = new ArrayList<String>();		
		for(Asset asset : adi.getAssets()) {
			if(assetWasAlreadyOnServer(asset)) {
				assetsNotInPackage.add(asset.getPath());
			}
		}
		return assetsNotInPackage;
	}

	private boolean assetWasAlreadyOnServer(Asset asset) {
		return asset.hasFileURI();
	}
	
	private AbstractWorkOrderRequest constructProvisionFailurePostIngestWorkorderRequest(String metadataFilePath, ADI adi, String errorMessage) {
		String packageAssetId = getPackageAssetIdFromADI(adi);		
		String providerId = getProviderIdFromADI(adi);		
		
		return new PostIngestWorkOrderRequest(null, null, metadataFilePath, false, errorMessage, true, null, packageAssetId, providerId);
	}
	
	private AbstractWorkOrderRequest constructValidationFailurePostIngestWorkorderRequest(String metadataFilePath,
			ADI adi, ISavedTitle savedTitle, List<TitleValidationMessage> validationMessages) {
		Long titleId = getTitleId(savedTitle);
		String titleStatus = getTitleStatus(savedTitle);
		String validationMessage = joinValidationMessages(validationMessages);
		String packageAssetId = getPackageAssetIdFromADI(adi);
		String providerId = getProviderIdFromADI(adi);

		return new PostIngestWorkOrderRequest(titleId, titleStatus, metadataFilePath, true, null, false,
				validationMessage, packageAssetId, providerId);
	}

	private String joinValidationMessages(List<TitleValidationMessage> validationMessages) {
		List<String> strMessages = new ArrayList<String>();
		for(TitleValidationMessage m : validationMessages) {
			strMessages.add(m.getMessage());
		}
		return join(strMessages, "\n");
	}

	private AbstractWorkOrderRequest constructValidationFailurePostIngestWorkorderRequest(String metadataFilePath,
			ADI adi, String validationMessages) {		
		String packageAssetId = getPackageAssetIdFromADI(adi);		
		String providerId = getProviderIdFromADI(adi);		
		
		return new PostIngestWorkOrderRequest(null, null, metadataFilePath, true, null, false, validationMessages, packageAssetId, providerId);
	}

	private Long getTitleId(ISavedTitle savedTitle) {
		if(savedTitle == null || savedTitle.getTitle() == null) {
			return null;
		}
		return savedTitle.getTitle().getId();
	}

	private String getTitleStatus(ISavedTitle savedTitle) {
		if(savedTitle == null || savedTitle.getTitle() == null || savedTitle.getTitle().getStatus() == null) {
			return null;
		}
		return savedTitle.getTitle().getStatus().toString();
	}

	private String getProviderIdFromADI(ADI adi) {
		String providerId;
		try {
			providerId = adi.getMetadata().getProviderId();
		} catch(Exception e) {
			providerId = null;
		}
		return providerId;
	}

	private String getPackageAssetIdFromADI(ADI adi) {
		String packageAssetId;
		try {
			packageAssetId = adi.getMetadata().getAssetId();
		} catch(Exception e) {
			packageAssetId = null;
		}
		return packageAssetId;
	}

	private AbstractWorkOrderRequest constructSuccessPostIngestWorkorderRequest(ISavedTitle savedTitle, String metadataFilePath, ADI adi) {
		Long titleId = savedTitle.getTitle().getId();		
		String titleStatus = getTitleStatus(savedTitle);
		String packageAssetId = getPackageAssetIdFromADI(adi);		
		String providerId = getProviderIdFromADI(adi);		
		
		return new PostIngestWorkOrderRequest(titleId, titleStatus, metadataFilePath, true, null, true, null,
				packageAssetId, providerId);
	}

	private ITitleActionSource newTitleActionSource() {
		return new TitleActionSource("Content Management", "ADI Server", "ADI Server");
	}

	private ITitleIngester titleIngester() throws TitleIngestException {
		try {
			return RemoteClientFactory.newTitleIngester();
		} catch (Exception e) {
			throw new TitleIngestException("It wasn't possible to retrieve the remote TitleIngester.", e);
		}
	}

	private synchronized ADI getMetadata(String packageName, String URL,
			CharSequence adiXMLFileName) throws Exception {

		//
		// Create a temporary file to house the ADI.XML
		//

		File xmlTempFile = File.createTempFile("ADI", ".XML");
		File dtdTempFile = File.createTempFile("ADI", ".DTD");

		//
		// Retrieve the XML from the remote system
		//

		URI sourceURI = new URI(URL);
		URI destinationURI = xmlTempFile.toURI();
		acquireResource(sourceURI, destinationURI);

		//
		// Retrieve the DTD from the remote system
		//

		sourceURI = new URI(URL.replace(adiXMLFileName, "ADI.DTD"));
		destinationURI = dtdTempFile.toURI();
		acquireResource(sourceURI, destinationURI);

		//
		// Parse the resulting file to determine package AssetId and ProviderId
		//
		ADI adi = ADIParseUtil.parseFile(xmlTempFile, dtdTempFile);

		//
		// Create content store directory structure
		//

		File contentStoreDirectoryPath = new File(getContentStoreAbsolutePath()
				+ File.separator + adi.getMetadata().getProviderId()
				+ File.separator + new SimpleDateFormat(DATE_FORMAT).format(new Date())
				+ File.separator + new SimpleDateFormat(TIME_FORMAT).format(new Date())
				+ File.separator + adi.getMetadata().getAssetId());

		if (!getVFS().localStorageDirectoryIsReady(
				contentStoreDirectoryPath.getAbsolutePath())) {
			String errorString = "Failed to create content store path: "
					+ contentStoreDirectoryPath.getAbsolutePath();
			log.error(errorString);
			throw new Exception(errorString);
		}

		adi.setContentStoreDirectoryAbsolutePath(contentStoreDirectoryPath
				.getAbsolutePath());

		//
		// Copy the XML and DTD into the proper directory
		//
		sourceURI = xmlTempFile.toURI();
		destinationURI = new File(contentStoreDirectoryPath.getAbsolutePath()
				+ File.separator + adiXMLFileName).toURI();
		acquireResource(sourceURI, destinationURI);

		sourceURI = dtdTempFile.toURI();
		destinationURI = new File(contentStoreDirectoryPath.getAbsolutePath()
				+ File.separator + "ADI.DTD").toURI();
		acquireResource(sourceURI, destinationURI);

		//
		// Get rid of the temporary file(s)
		//

		if (!xmlTempFile.delete()) {
			xmlTempFile.deleteOnExit();
		}

		if (!dtdTempFile.delete()) {
			dtdTempFile.deleteOnExit();
		}

		return adi;

	}

	private String getAdiXMLFileName(String url) {
		return url.substring(url.length() - 7);
	}

	private AbstractWorkOrderRequest constructIngestWorkorderRequest(String metadataFilePath,
			boolean provisionResult, List<String> errorMsgs, ADI adi) {
		return new IngestWorkOrderRequest(metadataFilePath, provisionResult,errorMsgs, adi);
	}

	private boolean validateCheckSum(String contentFilePath, String checkSum)
			throws NoSuchAlgorithmException, IOException {
		String newChecksum = ChecksumUtil.getCheckSum(contentFilePath);
		boolean validateResult = checkSum.equals(newChecksum);
		if (!validateResult) {
			log
					.error("Checksum validation failed for file: "
							+ contentFilePath);
			File contentFile = new File(contentFilePath);
			boolean deleteResult = contentFile.delete();
			if (!deleteResult) {
				log.error("Failed to delete content file " + contentFilePath);
			}
		}

		return validateResult;
	}

	/**
	 * <b>acquireResource</b>
	 * <p>
	 * Use the VirtualFileSystemManager to copy files. Depending on the URI, the
	 * source file may be on a remote system.
	 * 
	 * @param source
	 * @param destination
	 * @throws Exception
	 */
	public void acquireResource(URI source, URI destination) throws Exception {

		log.debug("Source resource: " + source.toString());
		log.debug("Destination resource: " + destination.toString());

		try {
			getVFS().copyURI(source, destination);
		} catch (FileSystemException fse) {
			String errorString = "Failed to copy sourcefile: "
					+ source.toString() + " from remote system.";
			log.error(errorString, fse);
			throw new Exception(errorString);
		} catch (IOException ioe) {
			String errorString = "Failed to copy sourcefile: "
					+ source.toString() + " from remote system.";
			log.error(errorString, ioe);
			throw new Exception(errorString);
		} catch (Exception e) {
			String errorString = "Failed to copy sourcefile: "
					+ source.toString() + " from remote system.";
			log.error(errorString, e);
			throw new Exception(errorString);
		}
	}

	/**
	 * <b>getContentStoreAbsolutePath</b>
	 * <p>
	 * Get the content store absolute path
	 * 
	 * @return String
	 */
	public String getContentStoreAbsolutePath() {
		return getConfigurationProperty(PConstants.ACS_CONTENTSTORE, PConstants.ADPOINT_ACS_DEFAULT_CONTENTSTORE);
	}

	/**
	 * <b>initializeContentStore</b>
	 * <p>
	 * Initialize the content store directory path
	 * 
	 */
	private void initializeContentStore() {

		VirtualFileSystemManager vfs = getVFS();

		if (!vfs.localStorageDirectoryIsReady(getContentStoreAbsolutePath())) {
			log.error("Failed to create content store directory structure.",
					null);
		}
	}

	/**
	 * <b>getVFS</b>
	 * <p>
	 * Get an instance of a VirtualFileSystemManager
	 * 
	 * @return
	 */
	private VirtualFileSystemManager getVFS() {
		return new VirtualFileSystemManager();
	}

	/**
	 * Private constructor
	 * 
	 */
	private LocalContentStoreHandler() {
		initializeContentStore();
	}

	private void provisionAsset(Asset asset, ADI adi, String metadataFilePath,
			String metadataFileName) throws Exception {
		if (asset.hasFileURI())
			provisionAssetWithFileURI(asset,adi);
		else
			provisionAssetWithFTPURI(asset, adi, metadataFilePath,
					metadataFileName);
	}
	/**
	 * converts file URI to linux path and sets the value in Asset object. The idea here is not to
	 * copy the file but use the same path on filesystem.
	 * @param asset 
	 * @throws Exception when file URI cannot be converted to linux path
	 */
	private void provisionAssetWithFileURI(Asset asset, ADI adi) throws Exception {
		String content = asset.getContent();
		log.debug("Processing content :" + content);
		String contentPath = new FileURIConverter().convertURIToUnix(
				content);
		log.debug("Converted path :" + contentPath);
		if (contentPath == null) {
			asset.setPath(content);
			log
					.error("[Configuration Error] "
							+ content
							+ " fileURIConverter.properties cannot find converter entry for "
							+ content);
			throw new Exception("Fail to convert URI:" + content);
		} else {
			asset.setPath(contentPath);
		}
	}

	/**
	 * converts content URI to FTP URI if metadata contains only asset file name. If metadata contains
	 * a FTP URI to asset file then use the FTP URI to download the file. It also prepares the destination
	 * path where the file will be downloaded.
	 * 
	 * @param asset
	 * @param adi
	 * @param metadataFilePath
	 * @param metadataFileName
	 * @throws Exception
	 */
	private void provisionAssetWithFTPURI(Asset asset, ADI adi,
			String metadataFilePath, String metadataFileName) throws Exception {
		String content = asset.getContent();
		log.debug("Processing content :" + content);
		URI sourceURI = null;
		URI destinationURI = null;
		/* calculate source path and destination path for provisioning */
		if (asset.hasFTPURI()) {
			sourceURI = new URI(content);
			/* get file name from source path */
			String path = sourceURI.getPath();
			String fileName = path.substring(path.lastIndexOf("/") + 1, path
					.length());
			/* set destination URI */
			destinationURI = new URI(adi.getContentStoreDirectoryAbsolutePath()
					+ File.separator + fileName);
			asset.setPath(destinationURI.getPath());
			
		} else {
			destinationURI = new URI(adi.getContentStoreDirectoryAbsolutePath()
					+ File.separator + content);
			asset.setPath(destinationURI.getPath());
			sourceURI = new URI(metadataFilePath
					.replace(metadataFileName, content));
		}
		try {
			acquireResource(sourceURI, destinationURI);
		} catch (Exception ex) {
			String errMsg = "Failed to acquire content: "
					+ sourceURI.toString();
			throw new Exception(errMsg, ex);
		
		}
	}
	
	private boolean useClassicMode() {
		String useClassicIngestMode = getConfigurationProperty(PConstants.USE_CLASSIC_INGEST_MODE_PROPERTY, Boolean.TRUE.toString());
		return useClassicIngestMode.toLowerCase().equals(Boolean.TRUE.toString());
	}

	private boolean sendIngestedEvent() {
		String useClassicIngestMode = getConfigurationProperty(PConstants.SEND_INGESTED_EVENT, Boolean.FALSE.toString());
		return useClassicIngestMode.toLowerCase().equals(Boolean.TRUE.toString());
	}
	
	private String getConfiguredContentClass() {
		return getConfigurationProperty(PConstants.CONTENT_CLASS, null);
	}
	
	// Returns the property set at adi.properties
	private String getConfigurationProperty(String propertyName, String propertyDefault) {
		try {
			return ApplicationProperties.getInstance().getProperty(propertyName, propertyDefault);
		} catch (Exception e) {
			log.error("Failed to get property " + propertyName, e);
			throw new RuntimeException(e);
		}
	}
}