package gov.va.med.esr.common.batchprocess;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;

import org.apache.commons.io.IOUtils;

import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.util.ThreadPool;

import gov.va.med.esr.service.CommsEmailBulletinService;
import gov.va.med.esr.service.SystemParameterService;
import gov.va.med.esr.service.trigger.BulletinTrigger;
import gov.va.med.fw.batchprocess.AbstractDataQueryIncrementalProcess;
import gov.va.med.fw.batchprocess.DataProcessExecutionContext;
import gov.va.med.fw.batchprocess.DataQueryProcessExecutionContext;
import gov.va.med.fw.io.writer.FormattedFileWriter;
import gov.va.med.fw.service.ServiceException;

public class HandbookBatchProcess extends AbstractDataQueryIncrementalProcess {
	
	private SystemParameterService paramService;

	private FormattedFileWriter outputFileWriter;
	
    //multithreading    
    private static final int DEFAULT_THREAD_POOL_SIZE = 10;
	private static final int DEFAULT_THROTTLE_TASKCOUNT_THREASHOLD = 100;
	private static final int DEFAULT_SPAWN_RETRY_PERIOD = 3000;
	private static final int DEFAULT_EXCEPTION_UPDATE_INTERVAL = 20;
	public static final String CONTEXT_THREAD_CREATOR = "threadCreator";
	public static final String CONTEXT_TASK_COUNT = "taskCount";
	public static final String CONTEXT_THREAD_POOL = "threadPool";
	private int threadPoolSize = DEFAULT_THREAD_POOL_SIZE;
	private int throttleTaskCountThreshold = DEFAULT_THROTTLE_TASKCOUNT_THREASHOLD;
	private int spawnRetryPeriod = DEFAULT_SPAWN_RETRY_PERIOD;
	private String spawnedTaskId; 

	
	final private static String ZERO_SIZE_FILE_NAME = "     N/A           "; //with some padding
	
	private int maxToCMS = 100000; //default 100K
	
	final private String EMPTY_STRING = "";

	final private static String MAX_TO_CMS = "Maximum number of records to CMS";
	
	//put this into context.getContextData() next time, like extrat/exception writer
	protected static CommsExportStatistics stats = null;
	
	//file I/O 
	final protected static String EXTRACT_WRITER = "ExtractDataWriter";
	final protected static String EXCEPTION_WRITER = "ExceptionDataWriter";
	private Writer extractWriter;
	private Writer exceptionWriter;
	private String hbFolder = null;
	private String fileSuffix = null;
	
	// A flag indicating if this is processing a batch job.  This is initially false then set
	// to true the first time processData() is called.  It is set back to false when the 
	// summary file is written.  It is used to the initialization of the output files
	// and summary statistics.
	private boolean processing = false;

	protected void processData(DataQueryProcessExecutionContext context,
			List acquiredData) {

		if (acquiredData == null){
			return;
		}

		try {
			maxToCMS = new Integer(this.getParamService().getByName(MAX_TO_CMS).getValue()).intValue();
		} catch (ServiceException se)
		{
			logger.error("Handbook Batch Process: error getting max number of record to CMS. Use default value. Error= " + se);
		}
		if (!processing) {

			//intitilize stats
			stats = new CommsExportStatistics();

			//start the real process
			stats.setStartTime(new Date());

			initFileWriters();
			processing = true;
		}    
       
		// Add check for isInterrupted so don't have to wait for entire batch to finish
		for (int i = 0; i < acquiredData.size() && !isInterrupted(context); i++) {
		
			//SUC There will be a limit on the maximum number of handbooks generated each week with the initial default set to 100,000.
			if (context.getProcessStatistics().getNumberOfSuccessfulRecords() == maxToCMS) 
				break; //reach the max, break
			
			Object[] row = (Object[]) acquiredData.get(i);
          
			 processEntityData(context, row);
			 
            // Update statistics more frequently than once per batch (default size 500)
//            if(context.getProcessStatistics().isTotalNumberMod(DEFAULT_JOB_RESULT_UPDATE_INTERVAL))
//                this.updateJobResult(context);
		}	
	}

	public void processEntityData(DataQueryProcessExecutionContext context, Object[] acquiredData) {
		
		if(isThreaded()) {
			spawnThread(context, acquiredData);
		}
		
	}
	private void spawnThread(DataQueryProcessExecutionContext context,
			Object[] acquiredData) {
		try {
			while (getTaskCount(context) >= throttleTaskCountThreshold) {
				Thread.sleep(spawnRetryPeriod);
			}
			initThreadCreatorAndIncrementTaskCount(context);
			ThreadPool threadPool = getThreadPool(context);
			// get a new Thread bean
			HandbookBatchProcessSpawnedThreadTask task =
				(HandbookBatchProcessSpawnedThreadTask) getApplicationContext().getBean(spawnedTaskId);

			//pass the writers to the thread in the context
			context.getContextData().put(EXTRACT_WRITER, extractWriter);
			context.getContextData().put(EXCEPTION_WRITER,exceptionWriter);
			
			task.setContext(context);
			task.setAcquiredData(acquiredData);	
			task.setAuditInfo(getAuditInfo()); 

			threadPool.invokeLater(task);
		} catch(InterruptedException e) {
				throwIllegalStateException("HandbookBatchProcessSpawnedThreadTask was interrupted while it was " +
						"spawning a thread. ", e); 
		};
	}
	
	
	private void initFileWriters()
	{
		hbFolder = this.getOutputFileWriter().getFileLocation(); //for data extract file 
		//file suffix timestamp.hbk
		fileSuffix = this.getOutputFileWriter().getFileSystemResource(
							this.getOutputFileWriter().getFileNameAppender().getFileNameSuffix(null)).getFilename();
		
		extractWriter = getOutputWriter(hbFolder + "ToCMS/CMS_DataExtract_", fileSuffix);
		exceptionWriter = getOutputWriter(hbFolder + "Exception/CMS_DataExtract_", fileSuffix.replaceAll("hbk", "exception"));
		
	}
	
	/*
	 * save results into 3 files -
	 * data extract:	/u02/batchProcess/Handbook/ToCMS/CMS_DataExtract_timestamp.hbk 
	 * summary:   		/u02/batchProcess/Handbook/Processed/CMS_DataExtract_summary_timestamp.txt 
	 * exception: 		/u02/batchProcess/Handbook/Exception/CMS_DataExtract_timestamp.exception
	 */
	private void saveToSummaryFile(CommsExportStatistics stats, DataQueryProcessExecutionContext context)
	{

		//since the extract file is written, set the end time and file name so that we can write summary
		stats.setEndTime(new Date());
		stats.setOriginalFileName("CMS_DataExtract_" + fileSuffix);
		
		//write the summary file
		writeFile(hbFolder + "Processed/CMS_DataExtract_summary_", fileSuffix.replaceAll("hbk", "txt"),  toSummaryTxt(stats, context));
	}
	
	private String toSummaryTxt(CommsExportStatistics stats, DataQueryProcessExecutionContext context)
	{
		StringBuffer str = new StringBuffer("### Handbook Batch Process Execution Statistics ###\n");
		
		//NOTE: the framework will have the context with end time after handleDataProcessCompleted() is done
		//		Thus these two fields are not available at this time. It's impossible to pass stats after handleDataProcessCompleted() is done.
		//		So we just set the end date here, knowing it will be set again later by the framework. There will be small inconsistency
		context.getProcessStatistics().setProcessingEndDate(new Date());
		try {
			str.append(context.getProcessStatistics().exportAsCSV());
		} catch (Exception e) {
			str.append("Unable to get the execution statistics");
		}
		 
		str.append("\n\nExtract Data Statistics:").append
				("\nCompletedDateTime: ").append(new Date()).append //stat end date is not set yet, so use current time
				("\nNumberOfFiles: 1").append
				("\nNumberLettersSent: ").append(stats.getNumberSent()).append
				("\nNumberLetterRejected: ").append(stats.getNumberRejected()).append
				("\nCommunicationsPerFormTxt: \n").append(stats.getHbCountPerFormTableText()).append
				("\nRejectPerReasonPerFormTxt: \n").append(stats.getRejectPerReasonPerFormText()).append
				("\nFileNameRecordCountTxt: \n").append(stats.getOriginalFileName()).append("  ").append(stats.getNumberSent());
		
		return str.toString();
	}
	
	
	private Writer getOutputWriter(String fileLocation, String fileName)
	{
		Writer output = null;
		try {
			File file = new File(fileLocation + fileName);
			output = new BufferedWriter(new FileWriter(file));	
		} catch (IOException e) {
		    try {
		    	if (output != null) {
		    		output.flush();
		    		output.close();
		    	}
            } catch (IOException e1)
            {
                //do nothing
            }
		    throw new RuntimeException("HandbookBatchProcess is unable to generate file to CMS! File= " + fileLocation + fileName+ "Exception: " + e);
		}
		
		return output;
	}

	private void writeFile(String fileLocation, String fileName, String text)
	{
		Writer output = null;
		java.io.FileWriter fw = null;
		try {
			File file = new File(fileLocation + fileName);
			fw = new FileWriter(file);
			output = new BufferedWriter(fw);	
				
			output.write(text);
			output.flush();
		} catch (IOException e) {
		    throw new RuntimeException("HandbookBatchProcess is unable to generate file to CMS! File= " + fileLocation + fileName+ "Exception: " + e);
		}finally {
	    	if (output != null) {		    		
	    		IOUtils.closeQuietly(output);
	    	}
			if (fw != null) {		    		
	    		IOUtils.closeQuietly(fw);
	    	}

		}
	}

	private void sendEmailNotification(CommsExportStatistics stats) {
		//CCR 11301: this is hardcoded in 3.6 because it's too late for ADR to add new bulletin entries for handbook batch process
		//This will be added in ESR 3.7 and this hardcoded code will be removed.
		Hashtable dataTab = new Hashtable();
		if (stats != null) {
			dataTab.put("CompletedDateTime", stats.getEndTime().toString());
			dataTab.put("NumberOfFiles", "1");
			dataTab.put("NumberLettersSent", stats.getNumberSent()+EMPTY_STRING);
			dataTab.put("NumberLetterRejected", stats.getNumberRejected()+EMPTY_STRING);
			dataTab.put("NumberLettersRemailed", stats.getNumberRemailed()+EMPTY_STRING);
			dataTab.put("CommunicationsPerFormTxt", stats.getHbCountPerFormTableText());
			dataTab.put("RejectPerReasonPerFormTxt", stats.getRejectPerReasonPerFormText());
			dataTab.put("FileNameRecordCountTxt", stats.getOriginalFileName() + "      " + stats.getNumberSent());
		} else {
			dataTab.put("CompletedDateTime", EMPTY_STRING);
			dataTab.put("NumberOfFiles", "1");
			dataTab.put("NumberLettersSent", EMPTY_STRING);
			dataTab.put("NumberLetterRejected", EMPTY_STRING);
			dataTab.put("NumberLettersRemailed", EMPTY_STRING);
			dataTab.put("CommunicationsPerFormTxt", EMPTY_STRING);
			dataTab.put("RejectPerReasonPerFormTxt", EMPTY_STRING);
			dataTab.put("FileNameRecordCountTxt", EMPTY_STRING);
		}
		
/*		String emailTxt = "Handbook Processing to Content Management System completed on " + stats.getEndTime().toString() +
			"\n\nFiles created for CMS: 1" +
			"\nLetters to send to CMS: " + stats.getNumberSent() +
			"\nLetters Rejected: " + stats.getNumberRejected() +
			"\nTotal Number being Resent for Remail: " + stats.getNumberRemailed() +
			"\n\nTotal number of form:\n" + stats.getHbCountPerFormTableText() +
			"\n\nTotal number of forms rejected sorted by reason:\n" + stats.getRejectPerReasonPerFormText() +
			"\n\n          File Name                    Record Count" +
			  "\n----------------------------------     ------------" +  
			"\n" + stats.getOriginalFileName()+ "      " + stats.getNumberSent();*/
		
		
		try {
			CommsEmailBulletinService emailSrv = (CommsEmailBulletinService) getComponent("commsEmailBulletinService");
			emailSrv.sendEmailBulletin(BulletinTrigger.DataType.HANDBOOK_LETTER_PROCESSING, dataTab, null);

/*			String[] toList =this.getOutputFileWriter().getBulletinToList().split(",");
			emailSrv.sendSimpleMailMessage("PII                      ", toList,
					null, "Handbook batch processing completed", emailTxt);*/
			
		} catch (Exception exlog) {
			logger.error("Error while sending bulletin for Handbook Process"
					+ exlog.getMessage());
		}
	}
	
	 private void cleanThreadPool(DataProcessExecutionContext context)
	    {
	        ThreadPool threadPool = getThreadPool(context);
	        threadPool.stop();
	        context.getContextData().put(CONTEXT_THREAD_POOL, null);
	    }
	 
	 protected void handleDataProcessCompleted(DataProcessExecutionContext  context) {
			if(isThreaded()) {
				try {
					if(getTaskCount(context) != 0) {
						synchronized(this) {
							boolean stillProcessing = true;
							while(stillProcessing) {
								wait();
								if(getTaskCount(context) == 0)
									stillProcessing = false;
							}
						}
					}
				} catch(InterruptedException e) {
					throwIllegalStateException("HandbookBatchProcess was interrupted while it was waiting for " +
							"its spawned threads to complete", e);
				} finally {
					getThreadPool(context).stop();
	                cleanThreadPool(context);
				}
			}
			
			//update stats
			this.updateJobResult(context); 
			
			//close file writers for extract & exception data
		    try {
		    	if (this.extractWriter != null) {
		    		extractWriter.flush();
		    		extractWriter.close(); }
            } catch (IOException e1) {
            	logger.error("Unable to close Data Extract File" + e1.getMessage());
            }
		    try {
		    	if (this.exceptionWriter != null) {
		    		exceptionWriter.flush();
		    		exceptionWriter.close(); }
            } catch (IOException e1){
            	logger.error("Unable to close Data Extract File" + e1.getMessage());
            }
            extractWriter = null;
            exceptionWriter = null;
            processing = false;

            try {
            	saveToSummaryFile(stats, (DataQueryProcessExecutionContext)context);
            } catch (Exception e1) {
            	logger.error("Unable to write summary file " + e1.getMessage());
            }

			//Send email bulletin
		    sendEmailNotification(stats);

	       super.handleDataProcessCompleted(context);
		   stats = null;

 	     
		}
	
	public int getThrottleTaskCountThreshold() {
		return throttleTaskCountThreshold;
	}

	public void setThrottleTaskCountThreshold(int throttleTaskCountThreshold) {
		this.throttleTaskCountThreshold = throttleTaskCountThreshold;
	}

	public int getSpawnRetryPeriod() {
		return spawnRetryPeriod;
	}

	public void setSpawnRetryPeriod(int spawnRetryPeriod) {
		this.spawnRetryPeriod = spawnRetryPeriod;
	}
	
	private ThreadPool getThreadPool(DataProcessExecutionContext context) {
		ThreadPool threadPool = (ThreadPool) context.getContextData().get(CONTEXT_THREAD_POOL);
		if(threadPool == null) {
			threadPool = new ThreadPool("HandbookBatchProcessSpawnedThreadTask", threadPoolSize);
			context.getContextData().put(CONTEXT_THREAD_POOL, threadPool);
		}
		return threadPool;
	}
	
	private void initThreadCreatorAndIncrementTaskCount(DataQueryProcessExecutionContext context) {
		if(!context.getContextData().containsKey(CONTEXT_THREAD_CREATOR))
			context.getContextData().put(CONTEXT_THREAD_CREATOR, this);
							
		adjustTaskCount(context, 1);
	}
	
	static int getTaskCount(DataProcessExecutionContext context) {
		Integer count = (Integer) context.getContextData().get(CONTEXT_TASK_COUNT);
		return count != null ? count.intValue() : 0;
	}

	static void adjustTaskCount(DataProcessExecutionContext context, int adjustment) {		
		synchronized(context) {
			context.getContextData().put(CONTEXT_TASK_COUNT, new Integer(getTaskCount(context) + adjustment));
		}
	}
	
	private boolean isThreaded() {
		return StringUtils.isNotBlank(spawnedTaskId);
	}

	public String getSpawnedTaskId() {
		return spawnedTaskId;
	}

	public void setSpawnedTaskId(String spawnedTaskId) {
		this.spawnedTaskId = spawnedTaskId;
	}
	
	/**
	 * @return Returns the threadPoolSize.
	 */
	public int getThreadPoolSize() {
		return threadPoolSize;
	}

	/**
	 * @param threadPoolSize The threadPoolSize to set.
	 */
	public void setThreadPoolSize(int threadPoolSize) {
		this.threadPoolSize = threadPoolSize;
	}


	public FormattedFileWriter getOutputFileWriter() {
		return outputFileWriter;
	}

	public void setOutputFileWriter(FormattedFileWriter outputFileWriter) {
		this.outputFileWriter = outputFileWriter;
	}

	
	public SystemParameterService getParamService() {
		return paramService;
	}

	public void setParamService(SystemParameterService paramService) {
		this.paramService = paramService;
	}

	
}
