package com.tandbergtv.watchpoint.pmm.title;

import static com.tandbergtv.watchpoint.pmm.title.search.EntityName.TITLE;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.COMPLETE_METADATA;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.METADATA_NAME;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.METADATA_VALUE;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.TITLE_EXTERNAL_LOCATION;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.TITLE_PROVIDER_ID;
import static com.tandbergtv.watchpoint.pmm.title.search.TitleSearchKey.TITLE_SPEC;
import static com.tandbergtv.watchpoint.pmm.title.validation.TitleValidationCode.EXTERNAL_TITLE_DUPLICATE;
import static com.tandbergtv.watchpoint.pmm.title.validation.TitleValidationCode.EXTERNAL_TITLE_MISSING;
import static com.tandbergtv.workflow.driver.search.SearchType.STRING;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.w3c.dom.Document;

import com.tandbergtv.cms.portal.util.transaction.Transactional;
import com.tandbergtv.marvin.udt.ActionMessage;
import com.tandbergtv.metadatamanager.exception.MetadataException;
import com.tandbergtv.metadatamanager.exception.SearchException;
import com.tandbergtv.metadatamanager.exception.TranslationException;
import com.tandbergtv.metadatamanager.factoryImpl.SpecHandlerFactory;
import com.tandbergtv.metadatamanager.model.Asset;
import com.tandbergtv.metadatamanager.model.Field;
import com.tandbergtv.metadatamanager.model.FieldName;
import com.tandbergtv.metadatamanager.model.Item;
import com.tandbergtv.metadatamanager.model.Spec;
import com.tandbergtv.metadatamanager.search.AssetSearchKey;
import com.tandbergtv.metadatamanager.search.AssetSearchService;
import com.tandbergtv.metadatamanager.search.FieldInfo;
import com.tandbergtv.metadatamanager.search.MetadataValueFieldInfo;
import com.tandbergtv.metadatamanager.search.SearchInfo;
import com.tandbergtv.metadatamanager.spec.IIdentifier;
import com.tandbergtv.metadatamanager.spec.ISpecHandler;
import com.tandbergtv.metadatamanager.specimpl.ttv.TTVId;
import com.tandbergtv.metadatamanager.util.AssetUtil;
import com.tandbergtv.watchpoint.pmm.core.IPMMService;
import com.tandbergtv.watchpoint.pmm.core.MultipleTitlesForAnAssetException;
import com.tandbergtv.watchpoint.pmm.core.NoTitleForAnAssetException;
import com.tandbergtv.watchpoint.pmm.core.PMMException;
import com.tandbergtv.watchpoint.pmm.core.TitleValidationException;
import com.tandbergtv.watchpoint.pmm.dao.hibernate.ApplicationContextHelper;
import com.tandbergtv.watchpoint.pmm.entities.Title;
import com.tandbergtv.watchpoint.pmm.entities.TitleStatus;
import com.tandbergtv.watchpoint.pmm.entities.event.TitleStatusUpdatedEvent;
import com.tandbergtv.watchpoint.pmm.title.conf.MenuOption;
import com.tandbergtv.watchpoint.pmm.title.search.ITitleSearchService;
import com.tandbergtv.watchpoint.pmm.title.search.ParamType;
import com.tandbergtv.watchpoint.pmm.title.search.SearchField;
import com.tandbergtv.watchpoint.pmm.title.search.TitleSearchCriteriaBuilder;
import com.tandbergtv.watchpoint.pmm.util.ProgressStatusHelper;
import com.tandbergtv.watchpoint.pmm.util.RulesEngineFacade;
import com.tandbergtv.watchpoint.pmm.util.validation.ValidationMessage;
import com.tandbergtv.watchpoint.search.Entity;
import com.tandbergtv.workflow.core.event.DefaultMediator;
import com.tandbergtv.workflow.core.service.ServiceRegistry;
import com.tandbergtv.workflow.driver.search.SearchOperator;
import com.tandbergtv.workflow.driver.search.SearchType;
import com.tandbergtv.workflow.driver.search.ValueParameter;
import com.tandbergtv.workflow.util.SearchCriteria;

public class TitleService implements ITitleService {
	private static final String SERVICE_NAME = "Title Service";
	private static final Logger logger = Logger.getLogger(TitleService.class);
	
	private static TitleService _instance;
	
	//private ISpecificationManager specManager;
	private ITitlePersistenceService titlePersistenceService;
	private ITitleSearchService titleSearchService;
	
	/**
	 * Gets the singleton instance
	 */
	public static synchronized TitleService getInstance() {
		if(_instance == null)
			_instance = new TitleService();
		return _instance;
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
		//specManager = getService(ISpecificationManager.class);
		titlePersistenceService = getService(ITitlePersistenceService.class);
		titleSearchService = getService(ITitleSearchService.class);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {}

	/**
	 * {@inheritDoc}
	 */
	@Deprecated
	public String evaluateJobParameterMenuOption(Title title, String jobParameterMenuOptionName) {
		/*
		Specification s = specManager.getSpecificationByName(title.getSpecification());
		MenuOption jobParameterMenuOption = s.getJobParameterMenuOption(jobParameterMenuOptionName);
		return (jobParameterMenuOption != null) ? evaluate(title, jobParameterMenuOption) : null;
		*/
		return null;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Title approve(long titleID) throws PMMException {
		logger.debug("Approving title: [" + titleID + "]");
		Title title = titlePersistenceService.get(titleID);
		if(title.getStatus() != TitleStatus.READY) {
			throw new PMMException("Title status is " + title.getStatus() +
					" | Has to be " + TitleStatus.READY + " to be approved");
		}
		title.setStatus(TitleStatus.APPROVED);
		save(title);
		return title;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Title disapprove(long titleID) throws PMMException {
		logger.debug("Disapproving title: [" + titleID + "]");
		Title title = titlePersistenceService.get(titleID);
		if(title.getStatus() != TitleStatus.APPROVED) {
			throw new PMMException("Title status is " + title.getStatus() +
					" | Has to be " + TitleStatus.APPROVED + " to be unapproved");
		}
		title.setStatus(TitleStatus.READY);
		//save would determine the correct title status (NEW/READY)
		save(title);
		return title;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public Collection<Asset> findAssetsWithFileName(String fileName) {		
		AssetSearchService service = ApplicationContextHelper.getInstance()
				.getAssetSearchService();
		SearchInfo info = buildSearchInfo(fileName);
		SearchCriteria criteria = service.getCriteria("asset", info, null,
				TitleSearchCriteriaBuilder.LATEST_REVISION);
		return service.search(criteria);		
	}

	private SearchInfo buildSearchInfo(String filePath) {
		SearchInfo info = new SearchInfo();		
		info.setProperty(AssetSearchKey.FIELDS.toString());
		List<FieldInfo> searchFields = new ArrayList<FieldInfo>();
		searchFields.add(new MetadataValueFieldInfo(
				FieldName.URL.toString(),
				SearchOperator.EQUAL, filePath));
		info.setFields(searchFields);
		return info;		
	}

	// ========================================================================
	// ============ EXTERNAL TITLE MANAGEMENT
	// ========================================================================

	/**
	 * {@inheritDoc}
	 */
	@Deprecated
	public Title getExternalTitle(String specification, String externalLocationId,
			Map<String, String> keys) {
		/* Build the search criteria for the external title and perform search */
		SearchCriteria criteria = buildSearchCriteria(specification, externalLocationId, keys);
		Collection<Title> titles = this.titleSearchService.search(criteria);
		
		/* Validate that only one title was returned by the search */
		if (titles == null || titles.size() == 0) {
			String msg = "Failed to find any Title from external source[" + externalLocationId
					+ "] for specification: " + specification + " using keys: " + keys;

			ValidationMessage message = new ValidationMessage(EXTERNAL_TITLE_MISSING.getCode());
			throw new TitleServiceException(message, msg);
		} else if (titles.size() > 1) {
			String msg = "Failed to find a unique Title from external source[" + externalLocationId
					+ "] for specification: " + specification + " using keys: " + keys + ", found "
					+ titles.size() + " titles.";
			ValidationMessage message = new ValidationMessage(EXTERNAL_TITLE_DUPLICATE.getCode());
			message.getProperties().add(Integer.toString(titles.size()));
			throw new TitleServiceException(message, msg);
		}

		return titles.iterator().next();
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Deprecated
	public Title importTitle(Title title) {
		/* Save the external title */
		this.save(title);

		/* Send all required Status Updates */
		this.updateProgressItems(title);

		return title;
	}

	/**
	 * {@inheritDoc}
	 */
	@Deprecated
	public Title syncTitle(Title title) {
		/* Get the Key Metadata fields from the title based on the Specification */
		/*Map<String, String> metadataKeys = new HashMap<String, String>();
		String specName = title.getSpecification();
		Specification specification = specManager.getSpecificationByName(specName);
		List<Variable> specKeys = specification.getKeyMetadataProps();
		for (Variable specKey : specKeys) {
			Title specificTitle = title.getTitle(specKey.getTitleConfName());
			String name = specKey.getName();
			String value = (specificTitle != null) ? specificTitle.getMetadataFieldValue(name) : "";
			if (value == null)
				value = "";
			metadataKeys.put(name, value);
		}*/

		/* Perform the external search */
		/*String externalLocationId = title.getExternalLocation();
		Title externalTitle = this.getExternalTitle(specName, externalLocationId, metadataKeys);*/

		/* Need to sync the saved title with the external title */
		/*this.syncTitle(externalTitle, title);*/

		/* Save the title after the sync */
		/*this.save(title);*/
		
		/* Fire the status events for any additional title sections added */
		/*this.updateProgressItems(title);
		
		return title;*/
		
		return null;
	}

	/* Performs a sync of the target title with the data present in the source title. */
	@Deprecated
	private void syncTitle(Title source, Title target) {
		/* Copy all the metadata and locations from the source to the target */
		/*target.setMetadata(source.getMetadata());
		target.setMetadataLocation(source.getMetadataLocation());
		target.setAssetLocation(source.getAssetLocation());*/
		
		/* Sync each of the children of the source title */
		/*Collection<Title> sourceChildren = source.getChildren();
		if (sourceChildren != null) {
			for (Title childSource : sourceChildren) {
				Title childTarget = target.getChild(childSource.getName());
				if (childTarget == null) {
					childTarget = new Title(childSource.getSpecification(), childSource.getName());
					childTarget.setExternalLocation(target.getExternalLocation());
					target.addChild(childTarget);
				}
				this.syncTitle(childSource, childTarget);
			}
		}*/

		/* Sync the children of the target title that are not in the source title */ 
		/*Collection<Title> targetChildren = target.getChildren();
		if (targetChildren != null) {
			for (Title childTarget : targetChildren) {
				Title childSource = source.getChild(childTarget.getName());
				if (childSource == null) {*/
					// TODO Should this target child and all its children be 'deleted'?
					/* Create a 'NULL' title to sync with target with */
					/*Title nullTitle = new Title(null, null);
					this.syncTitle(nullTitle, childTarget);
				}
			}
		}*/
	}

	/*
	 * Send all status updates for each of the Title assets and for metadata. The sectionNames set
	 * must be null if all sections in the title require progress updates.
	 */
	@Deprecated
	private void updateProgressItems(Title title) {
		ServiceRegistry registry = ServiceRegistry.getDefault();
		IPMMService pmmService = registry.lookup(IPMMService.class);
		
		/* Every Title has Metadata */
        pmmService.sendMetadataReceivedStatus(title, null, null, null);

		/* Send Asset Received Status for all title sections that have assets present */        
		for(Asset sectionAsset : title.getAsset().getAllDescendantItems(false)) {
			Item sectionItem = (Item) sectionAsset;
			// TODO: Only if the item has a metadata for a file name set the asset recvd status			
			Field fileMetadataField = sectionItem
					.getFirstField(FieldName.URL.toString());
			if (fileMetadataField != null
					&& fileMetadataField.getValue() != null
					&& fileMetadataField.getValue().trim().length() > 0) {
				pmmService.sendAssetReceivedStatus(title,
						sectionItem.getType(), null, null, null);
			}
		}
	}

	/*
	 * Evaluates the given title property from the given title.
	 * 
	 * Examples of properties:
	 * package.assetLocation
	 * package.metadata_packageAssetID
	 * movie.metadata_assetID
	 */
	@Deprecated
	private String evaluate(Title rootTitle, MenuOption jpmo) {
		/*String titleName = jpmo.getTitleName();
		Title title = rootTitle.getTitle(titleName);

		// check if the section was found before trying to get the
		// property value.
		if (title == null) {
			return null;
		}
		
		if(jpmo.isAssetLocation()) {
			return title.getAssetLocation();
		} else if(jpmo.isMetatadataLocation()) {
			return title.getMetadataLocation();
		} else if(jpmo.isMetadata()) {
			String metadataName = jpmo.getMetadataProperty();
			TitleMetadata metadata = title.getMetadataField(metadataName);
			return (metadata != null) ? metadata.getValue() : null;
		} else {
			return null;
		}*/
		
		return null;
	}
	
	/**
	 * Looks up implementation of the given service interface in the Service Registry.
	 */
	private <T> T getService(Class<T> clazz) {
		return ServiceRegistry.getDefault().lookup(clazz);
	}
	
	/* Build the Search Criteria for performing an external search to find a unique Title. */
	private SearchCriteria buildSearchCriteria(String specName, String externalSourceId,
			Map<String, String> metadataKeys) {
		/* Try fetching 2 records to ensure the search key is for a unique title */
		SearchCriteria criteria = new SearchCriteria();
		criteria.setRecordsCount(2);

		/* Add the Title Entity with basic filters */
		Entity entity = new Entity(TITLE.toString(), Title.class, "t");
		criteria.addParameter(entity);

		/* Add required filter: Title Specification */
		entity.addParameter(new ValueParameter(TITLE_SPEC.toString(), STRING, specName));

		/* Add required filters: Title Provider Id and Title Provider Instance Id */
		String titleProviderId = this.getTitleProviderId(externalSourceId);
		criteria.addParameter(new ValueParameter(TITLE_PROVIDER_ID.toString(), STRING,
				titleProviderId));
		entity.addParameter(new ValueParameter(TITLE_EXTERNAL_LOCATION.toString(), STRING,
				externalSourceId));

		/* Go through all metadata filters and add as Metadata fields */
		if (metadataKeys != null) {
			for (String key : metadataKeys.keySet()) {
				String value = metadataKeys.get(key);
				Entity metadata = new Entity(key, COMPLETE_METADATA.toString(), "m");
				metadata.addParameter(new ValueParameter(METADATA_NAME.toString(), STRING, key));
				metadata.addParameter(new ValueParameter(METADATA_VALUE.toString(), STRING, value));
				entity.addParameter(metadata);
			}
		}

		return criteria;
	}

	/* Get the provider Id from the title external source Id */
	private String getTitleProviderId(String externalSourceId) {
		String providerId = null;
		
		if (externalSourceId != null) {
			String[] values = externalSourceId.split("-");
			providerId = (values.length > 0) ? values[0] : "";
		}
		
		return providerId;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Title getTitle(Asset rootAsset) {
		Collection<SearchField> searchFields = new ArrayList<SearchField>();
		SearchField field = new SearchField();
		field.setName("asset.id");
		
		List<String> values = new ArrayList<String>();
		values.add(String.valueOf(rootAsset.getId()));
		field.setValues(values);
		
		field.setParamType(ParamType.VALUE);
		field.setSearchOperator(SearchOperator.EQUAL);
		field.setSearchType(SearchType.NUMERIC);		
		searchFields.add(field);
		
		SearchCriteria criteria = TitleSearchCriteriaBuilder.getCriteria(true,
				"", "", searchFields, 0, Integer.MAX_VALUE, null, null,
				null);
		Collection<Title> matchingTitles = titleSearchService.search(criteria);
		if (matchingTitles.size() == 0) {
			throw new NoTitleForAnAssetException("No title found with asset id: "
					+ rootAsset.getId());
		}
		if (matchingTitles.size() > 1) {
			throw new MultipleTitlesForAnAssetException("Multiple titles found with asset id: "
					+ rootAsset.getId());
		}
		return matchingTitles.iterator().next();
	}
	
	/**
	 * Gets the title corresponding to the assets in the metadata passed in the given spec.
	 * 
	 * @param metadata
	 * @param spec
	 * @return
	 * @throws PMMException
	 */
	public Collection<Title> getTitle(Document metadata) {
		ISpecHandler specHandler = SpecHandlerFactory.getInstance(metadata);
		List<Asset> matchingAssets = new ArrayList<Asset>();
		try {
			List<IIdentifier> identifiers = specHandler.getIdentifiers(metadata);
			if(identifiers != null) {
				for(IIdentifier identifier : identifiers) {
					matchingAssets.add(identifier.getAsset());
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(
					"Failed to get matching Asset for the metadata document.",
					e);
		}
		Collection<Title> titles = new ArrayList<Title>();
		for (Asset a : matchingAssets) {
			titles.add(getTitle(a));
		}
		return titles;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@Transactional	
	public Collection<Field> getAllDecendantFields(long id) {
		ITitlePersistenceService service = ServiceRegistry.getDefault().lookup(
				ITitlePersistenceService.class);
		Title t = service.get(id);
		t.getAsset().getAllDescendantAssetFields().size();
		// Also init the parent asset.
		for (Field f : t.getAsset().getAllDescendantAssetFields()) {
			f.setParentAsset(new AssetUtil().unWrap(f.getParentAsset()));
		}
		return t.getAsset().getAllDescendantAssetFields();
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public void nationalizeAndSave(Long titleId, String sourceComponentName, String sourceEntityName, String sourceId) {
		//get title
		Title title = titlePersistenceService.get(titleId);
		
		//call Rules Engine to nationalize the title
		Title nationalizedTitle = RulesEngineFacade.nationalize(title);
		
		//send progress status - Nationalized
		IPMMService pmmService = getService(IPMMService.class);
		pmmService.sendStatus(nationalizedTitle,
				ProgressStatusHelper.NATIONALIZED, null, true,
				sourceComponentName, sourceEntityName, sourceId);
		
		try {
			validateAndSave(nationalizedTitle, sourceComponentName, sourceEntityName, sourceId);
			RulesEngineFacade.sendNationalizedEvent(nationalizedTitle.getId());
		} catch (TitleValidationException e) {
			//save nationalized title as draft
			saveAsDraft(nationalizedTitle);

			//send progress status - Validation Failure
			pmmService.sendStatus(nationalizedTitle,
					ProgressStatusHelper.VALIDATION_FAILURE,
					e.getValidationMessagesAsString(), false,
					sourceComponentName, sourceEntityName, sourceId);
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public void saveAsDraft(Title title) {
		//TODO should a broadcast event for title status change should be sent?
		title.setStatus(TitleStatus.NEW);
		titlePersistenceService.save(title);
	}
	
	private void save(Title title) {
		TitleStatus oldStatus = title.getStatus();

		//update the title status (Eg: mark as ready if ready)
		getService(IPMMService.class).updateTitleStatus(title);
		
		//save
		titlePersistenceService.save(title);
		
		//if the title status is changed, broadcast event
		TitleStatus newStatus = title.getStatus();
		if (newStatus != oldStatus) {
			TitleStatusUpdatedEvent event = new TitleStatusUpdatedEvent(this,
					title, oldStatus, newStatus);
			DefaultMediator.getInstance().sendAsync(event);
		}
	}
	
	private void validateAndSave(Title title,
			String sourceComponentName, String sourceEntityName, String sourceId)
			throws TitleValidationException {
		List<ActionMessage> validationMessages = RulesEngineFacade.validate(title);
		
		//if validation failed
		if(validationMessages != null && !validationMessages.isEmpty()) {
			throw new TitleValidationException(validationMessages);
		}
		else {	//if validation succeeded
			//save
			save(title);

			//send progress status - validation success
			IPMMService pmmService = getService(IPMMService.class);
			pmmService.sendStatus(title, ProgressStatusHelper.VALIDATION_SUCCESS, null, true,
					sourceComponentName, sourceEntityName, sourceId);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public void create(Title title,
			String sourceComponentName, String sourceEntityName, String sourceId)
			throws TitleValidationException {

		validateAndSave(title, sourceComponentName, sourceEntityName, sourceId);
		
		//update status - created
		IPMMService pmmService = getService(IPMMService.class);
		pmmService.sendStatus(title, ProgressStatusHelper.CREATED, null, true,
				sourceComponentName, sourceEntityName, sourceId);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public void update(Title title,
			String sourceComponentName, String sourceEntityName, String sourceId)
			throws TitleValidationException {

		validateAndSave(title, sourceComponentName, sourceEntityName, sourceId);
		
		//update status - updated
		IPMMService pmmService = getService(IPMMService.class);
		pmmService.sendStatus(title, ProgressStatusHelper.UPDATED, null, true,
				sourceComponentName, sourceEntityName, sourceId);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public Title updateMetadata(Title title,
			String sourceComponentName, String sourceEntityName, String sourceId)
			throws TitleValidationException {
		Title existingTitle = titlePersistenceService.get(title.getId());
		existingTitle.setAsset(title.getAsset());
		update(existingTitle, sourceComponentName, sourceEntityName, sourceId);
		return existingTitle;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Transactional
	public Title rollback(Long titleId, String version) 
		throws MetadataException, SearchException, TranslationException {
		//get ttvId using titleId
		Title title = titlePersistenceService.get(titleId);
		TTVId ttvId = title.getAsset().getTTVId();
		Session session = ApplicationContextHelper.getInstance().getSessionFactory().getCurrentSession();
		session.evict(ttvId);
		session.evict(title.getAsset());
		
		//rollback
        ISpecHandler ttvHandler = SpecHandlerFactory.getInstance("TTV");
        ttvHandler.rollBackToRevision(ttvId, version);
        
        //refetch the title with latest (rolledback) version
        return titlePersistenceService.get(titleId);
	}
	
}
