package org.gcube.application.geoportal.managers;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

import org.gcube.application.geoportal.model.Record;
import org.gcube.application.geoportal.model.content.AssociatedContent;
import org.gcube.application.geoportal.model.db.DBConstants;
import org.gcube.application.geoportal.model.db.PostgisTable;
import org.gcube.application.geoportal.model.db.PostgisTable.Field;
import org.gcube.application.geoportal.model.db.PostgisTable.FieldType;
import org.gcube.application.geoportal.model.fault.ConfigurationException;
import org.gcube.application.geoportal.model.fault.PersistenceException;
import org.gcube.application.geoportal.model.fault.PublishException;
import org.gcube.application.geoportal.model.fault.SDIInteractionException;
import org.gcube.application.geoportal.model.fault.ValidationException;
import org.gcube.application.geoportal.model.report.PublicationReport;
import org.gcube.application.geoportal.model.report.ValidationReport;
import org.gcube.application.geoportal.model.report.ValidationReport.ValidationStatus;
import org.gcube.application.geoportal.storage.ContentHandler;
import org.gcube.application.geoportal.storage.PostgisDBManager;
import org.gcube.application.geoportal.storage.PostgisDBManagerI;

import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class AbstractRecordManager<T extends Record> {	

	public static void setDefaultProvider(EMFProvider provider) {
		defaultProvider=provider;
	}

	private static EMFProvider defaultProvider=new DefatulEMFProvider();

	@Synchronized
	protected static EntityManagerFactory getEMF() {
		return defaultProvider.getFactory();
	}

	@Synchronized
	public static void shutdown() {
		defaultProvider.shutdown();
	}

	public static Record getByID(long id) {
		EntityManager em=getEMF().createEntityManager();
		try {
			log.debug("Looking for record by ID : "+id);
			EntityTransaction tr=em.getTransaction();
			Record toReturn=em.find(Record.class, id);
			log.debug("Loaded Record "+toReturn);
			return toReturn;
		}finally {
			if(em.isJoinedToTransaction())
				em.flush();
			em.close();
		}
	}


	public static Collection<Record> getList(){
		EntityManager em=getEMF().createEntityManager();
		try {
			log.debug("Getting entire list");
			EntityTransaction tr=em.getTransaction();
			tr.begin();
			List<Record> toReturn=em.createQuery("select r from Record r ",
					Record.class).getResultList();
			log.debug("Loaded size "+toReturn.size());
			tr.commit();
			return toReturn;
		}finally {
			if(em.isJoinedToTransaction())
				em.flush();
			em.close();
		}
	}

	public static <E extends Record> Collection<E> getListByClass(Class<E> clazz){
		EntityManager em=getEMF().createEntityManager();
		try {
			log.debug("Getting entire list");
			EntityTransaction tr=em.getTransaction();
			tr.begin();

			String simpleClassName=clazz.getName().substring(clazz.getName().lastIndexOf(".")+1);

			List<E> toReturn=em.createQuery("select r from "+simpleClassName+" r",
					clazz).getResultList();
			log.debug("Loaded size "+toReturn.size());
			tr.commit();
			return toReturn;
		}finally {
			if(em.isJoinedToTransaction())
				em.flush();
			em.close();
		}
	}
	//	protected T storeInfo() 
	//	{
	//		log.debug("Storing Record "+theRecord);
	//		entityManager.persist(theRecord);
	//		return theRecord;		
	//	}

	//Transaction management

	EntityTransaction transaction=null;
	EntityManager entityManager=null;

	//************************ INSTANCE

	private T theRecord;

	private ContentHandler<T> contentHandler;

	protected AbstractRecordManager(T theRecord){
		entityManager=getEMF().createEntityManager();
		transaction=entityManager.getTransaction();
		transaction.begin();
		this.theRecord=theRecord;
		//		log.debug("Storing Record "+theRecord);
		if(theRecord.getId()==0) {
			log.debug("Passed record ID = 0. Assuming new ..");
			entityManager.persist(theRecord);			
		}else {
			log.debug("Passed record ID = "+theRecord.getId()+". Mergeing..");
			this.theRecord=(T) entityManager.find(theRecord.getClass(),theRecord.getId());
		}
		this.contentHandler=new ContentHandler<T>(theRecord);
	}	





	protected ContentHandler<T> getContentHandler() {
		return contentHandler;
	}

	public T getRecord() {
		return theRecord;
	};	


	/**
	 * Commit to storages 
	 * 
	 * @return
	 * @throws PersistenceException 
	 * @throws PublishException 
	 */
	public T commit(boolean publish) throws PersistenceException,ValidationException, PublishException {

		log.trace("Committing record "+theRecord+" Publish is "+publish);
		try {
			ValidationReport report=theRecord.validate();
			log.debug("Validated Report is "+report);
			


			log.debug("Record is valid, storing changed content");
			contentHandler.storeChanges();
			commit();
			transaction.begin();
			
			if(publish) {
				if(report.getStatus().equals(ValidationStatus.ERROR))
					throw new ValidationException(report,"Cannot publish project. See validation report");
				
				log.debug("Publishing record "+theRecord);
				contentHandler.publish();
				commit();
				
				transaction.begin();
				log.debug("Registering centroid of "+theRecord);
				registerCentroid();
				commit();
			}


			
			return theRecord;
		}catch(ValidationException | PublishException | PersistenceException e) {
			log.warn("Committing session for "+theRecord);
			if(transaction.isActive())	transaction.rollback();
			throw e;
		}
	}
	
	
	private void commit() {
		log.debug("Successufully committing "+theRecord);
		  entityManager.flush();
		    entityManager.clear();	  
		transaction.commit();
		log.debug("Calling postCommit");
		postcommit();
	}
	protected abstract void postcommit();

	public PublicationReport commitSafely(boolean publish) {
		log.debug("Safely publishing "+theRecord);


		PublicationReport toReturn=new PublicationReport("Publication Report");
		toReturn.setTheRecord(getRecord());

		ValidationReport validation=theRecord.validate();
		commit();
		transaction.begin();
		validation.setObjectName("Validation report for "+validation.getObjectName());
		if(validation.getStatus().equals(ValidationStatus.ERROR)) {			
			toReturn.addMessage(publish?ValidationStatus.ERROR:ValidationStatus.WARNING, "Record not valid.");
		}
		
		toReturn.addChild(validation);

		try {
			log.debug("Storing changed content [Publish is :"+publish+"]");
			toReturn.addChild(contentHandler.storeChanges());
			
			commit();
			transaction.begin();
			
			if(publish) {
				toReturn.addChild(contentHandler.publish());				
				commit();
				log.debug("Registering centroid of "+theRecord);
				registerCentroid();				
				toReturn.addMessage(ValidationStatus.PASSED, "Inserito centroide per record "+theRecord.getId());
			}

			log.debug("Committing session for "+theRecord);
			commit();
		} catch (PersistenceException e) {
			toReturn.addChild(e.getReport());
			log.warn("Unexpected internal exception ",e);
			log.debug("Rollback Session for "+theRecord);
			if(transaction.isActive())	transaction.rollback();
		} catch (PublishException e) {
			toReturn.addMessage(ValidationStatus.WARNING, "Centroide non registrato");
			log.warn("Unexpected internal exception ",e);
			log.debug("Committing session for "+theRecord);
			if(transaction.isActive())	commit();
		}


		try {
			log.info("Report is "+toReturn.prettyPrint());
		}catch (Exception e) {
			log.warn("Unable to pretty print report "+toReturn,e);
		}
		return toReturn;

	}

	
	public void publish() {
		PublicationReport toReturn=new PublicationReport("Publication Report");
		toReturn.setTheRecord(getRecord());
		transaction.begin();
		try {
			toReturn.addChild(contentHandler.publish());
			commit();
			transaction.begin();
			log.debug("Registering centroid of "+theRecord);
			registerCentroid();
			
			toReturn.addMessage(ValidationStatus.PASSED, "Inserito centroide per record "+theRecord.getId());
			
			log.debug("Committing session for "+theRecord);
			commit();
		}catch (PublishException e) {
			toReturn.addMessage(ValidationStatus.WARNING, "Centroide non registrato");
			log.warn("Unexpected internal exception ",e);
			log.debug("Committing session for "+theRecord);
			if(transaction.isActive())commit();
		}

	}

	public void delete() {
		
		onDelete();
		removeCentroid();
		transaction.commit();
	}


	protected abstract void onDelete();

	public void close() {
		try {
			if(transaction.isActive()) {
				transaction.rollback();
			}	
		}catch(Throwable t) {
			log.warn("Exception while closing ",t);
		}
	}

	@Override
	protected void finalize() throws Throwable {
		close();
	}

	private void removeCentroid() {
		try {
			PostgisDBManagerI db=PostgisDBManager.get();
			PostgisTable centroidsTable=getCentroidsTable();
			log.debug("Deleting centroid if present. ID is "+theRecord.getId());
			db.deleteByFieldValue(centroidsTable, new Field(DBConstants.Concessioni.PRODUCT_ID,FieldType.TEXT), theRecord.getId()+"");
		}catch(Exception e) {
			log.warn("Unable to remove centroid ",e);
		}
	}

	private void registerCentroid() throws PublishException{

		try {
			log.debug("Evaluating Centroid");
			Map<String,String> centroidRow=evaluateCentroid();

			log.debug("Contacting postgis DB .. ");
			PostgisDBManagerI db=PostgisDBManager.get();

			PostgisTable centroidsTable=getCentroidsTable();
			log.debug("Inserting / updated centroid Row {} ",centroidRow);

			PreparedStatement ps = db.prepareInsertStatement(centroidsTable, true, true);

			log.debug("Deleting centroid if present. ID is "+theRecord.getId());
			db.deleteByFieldValue(centroidsTable, new Field(DBConstants.Concessioni.PRODUCT_ID,FieldType.TEXT), theRecord.getId()+"");

			centroidsTable.fillCSVPreparedStatament(centroidRow, ps, false);
			ps.executeUpdate();
			db.commit();

			initCentroidLayer();


		}catch(SQLException e) {
			log.warn("Unable to publish Centroid for record "+theRecord,e);
			throw new PublishException("Unable to publish centroid.",e, null);
		}catch(SDIInteractionException e) {
			log.warn("Unable to publish Centroid Layer for record type "+getRecord().getRecordType(),e);
			throw new PublishException("Unable to publish centroid.",e, null);
		} catch (ConfigurationException e) {
			log.warn("Unable to contact centroids db "+getRecord().getRecordType(),e);
			throw new PublishException("Unable to publish centroid.",e, null);
		}

	}

	protected abstract PostgisTable getCentroidsTable();
	protected abstract void initCentroidLayer() throws SDIInteractionException;

	protected abstract Map<String,String> evaluateCentroid();



	//*********** PERSISTENCE

}
