package lu.tudor.santec.gecamed.billing.test;

import java.io.File;
import java.net.MalformedURLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lu.tudor.santec.gecamed.billing.ejb.entity.beans.Act;
import lu.tudor.santec.gecamed.billing.ejb.entity.beans.Invoice;
import lu.tudor.santec.gecamed.billing.ejb.session.beans.NomenclatureBean;
import lu.tudor.santec.gecamed.billing.ejb.session.beans.RuleBean;
import lu.tudor.santec.gecamed.billing.ejb.session.interfaces.NomenclatureInterface;
import lu.tudor.santec.gecamed.billing.ejb.session.interfaces.RuleInterface;
import lu.tudor.santec.gecamed.billing.test.utils.ResultBean;
import lu.tudor.santec.gecamed.billing.utils.ActComparator;
import lu.tudor.santec.gecamed.billing.utils.rules.RuleOptions;
import lu.tudor.santec.gecamed.core.gui.GECAMedLists;
import lu.tudor.santec.gecamed.core.gui.utils.UrlOpener;
import lu.tudor.santec.gecamed.core.test.TestUtils;
import lu.tudor.santec.gecamed.core.utils.ManagerFactory;
import lu.tudor.santec.gecamed.patient.ejb.entity.beans.HospitalisationClass;
import lu.tudor.santec.gecamed.patient.ejb.entity.beans.Insurance;
import lu.tudor.santec.gecamed.patient.ejb.entity.beans.Patient;
import lu.tudor.santec.gecamed.patient.ejb.session.beans.PatientAdminBean;
import lu.tudor.santec.gecamed.patient.ejb.session.interfaces.PatientAdminInterface;

import org.apache.log4j.Logger;
import org.concordion.integration.junit4.ConcordionRunner;
import org.junit.runner.RunWith;


/**
 * @author jens.ferring(at)tudor.lu
 * 
 * @version
 * <br>$Log: CnsRuleFixture.java,v $
 */
@RunWith(ConcordionRunner.class)
public class BillingRulesFixture
{
	/* ======================================== */
	// CONSTANTS
	/* ======================================== */
	
	private static final Pattern	OPTION_PATTERN_H2D_MODE		= Pattern.compile("(H2D)([012])",	Pattern.CASE_INSENSITIVE);
	private static final Pattern	OPTION_PATTERN_TP_VALUE		= Pattern.compile("(TP)(\\d+)",		Pattern.CASE_INSENSITIVE);
	private static final Pattern	OPTION_PATTERN_SESSION_MODE	= Pattern.compile("(S)([01])",		Pattern.CASE_INSENSITIVE);
	private static final Pattern	OPTION_PATTERN_PATIENT_AGE	= Pattern.compile("(AGE)(\\d+)",	Pattern.CASE_INSENSITIVE);
	
	
	
	/* ======================================== */
	// MEMBERS
	/* ======================================== */
	
	/** the logger Object for this class */
	private static Logger logger = Logger.getLogger(BillingRulesFixture.class.getName());
	
	private static boolean errorDuringPreparation = false;
	
	private static HashMap<String, HospitalisationClass> clazzMap;
	private static HashMap<String, Insurance> insuranceMap;
	
	private static DateFormat		dateFormatter;
	private static DateFormat		timeFormatter;
	
	private static Pattern			boolValueTrue;
	private static Pattern			boolValueFalse;
	
	private static Date				defaultTime;
	private static Date				defaultDate;
	private static int				defaultQuantity;
	private static Insurance		defaultInsurance;
	private static double			defaultKeyValue;
	private static Boolean			defaultFirstClassRequired;
	
	private static Patient			patient;
	
	
	private Invoice					invoice;
	private int						orderNo;
	private String					testLabel;
	private String					optionsString;
	
	private Act						lastAct;
	
	
	
	/* ======================================== */
	// CONSTRUCTORS
	/* ======================================== */
	
	public BillingRulesFixture ()
	{
		TestUtils.initLogger();
		TestUtils.initJBossConnection();
		initDefaults();
	}
	
	
	/* ======================================== */
	// CLASS BODY
	/* ======================================== */
	
	public void setTestLabel (String testName, String testCategory)
	{
		if (testName != null)
			testLabel = testName;
		else
			testLabel = null;
		
		if (testCategory != null)
			testLabel = (testLabel != null ? testLabel + " (" : "(") + testCategory + ")";
		
		if (testLabel != null && testLabel.trim().length() == 0)
			testLabel = null;
	}
	
	
	public void addAct (String insuranceCode, String optionsString, String firstClassRequired, String clazzString, String dateString, String timeString, 
			String keyValueString, String coefficientString, String code, String suffix, String quantityString)
	{
		try
		{
			HospitalisationClass	hospClass	= getHospitalisationClass(clazzString);
			Date					actDate		= getActDate(dateString);
			Date					actTime		= getActTime(timeString);
			Calendar				calendar	= getCalendar(actDate, actTime);
			Act						act			= getRate(code, calendar.getTime());
			Double					keyValue	= getKeyValue(keyValueString);
			Double					coefficient	= getCoefficient(coefficientString);
			Integer					quantity	= getQuantity(quantityString);
			
			
			if (errorsDuringPreparation())
				return;
			
			if (invoice == null)
			{
				Insurance	insurance	= getInsurance(insuranceCode);
				Boolean		_1stClassReq= get1stClassRequired(firstClassRequired);
				invoice	= new Invoice();
				invoice.setPatient(patient);
				this.optionsString	= optionsString;
				invoice.setFirstClassRequired(_1stClassReq);
				invoice.setHospitalisationClass(hospClass);
				invoice.setInvoiceDate(new Date());
				invoice.setHealthInsurance(insurance);
				orderNo	= 1;
			}
			
			
			act = invoice.addAct(act, calendar.getTime(), suffix);
	//		act.setSuffixes(suffix);
	//		act.setPerformedTime(actTime);
	//		act.setPerformedDate(actDate);
			act.setHospitalisationClass(hospClass.getAcronym(), invoice);
			act.setKeyValue(keyValue, 1);
			act.setCoefficient(coefficient);
			act.setQuantity(quantity);
			act.setId(Integer.valueOf(orderNo++));
			
			if (errorsDuringPreparation())
				return;
			
	//		System.out.println("BillingRulesFixture.addAct:");
	//		System.out.println(act.print());
			
			lastAct		= act;
		}
		catch (Exception e)
		{
			logger.error("Error while adding act to test invoice", e);
		}
	}
	
	
	public Iterable<?> applyRules ()
	{
		return applyRules(false);
	}
	
	
	public Iterable<?> applyRulesVerbose ()
	{
		return applyRules(true);
	}
	
	
	public Iterable<?> applyRules (boolean verbose)
	{
		RuleInterface	manager;
		ActComparator	comparator;
		TreeSet<Act>	acts;
		
		
		if (errorsDuringPreparation())
			return new LinkedList<Act>();
		
		try
		{
			invoice.updateFirstClassRequired();
			invoice.monetize();
			
//			System.out.println("BillingRulesFixture.applyRules:");
//			for (Act act : invoice.getActs())
//				System.out.println(act.print());
			
			manager = (RuleInterface) ManagerFactory.getRemote(RuleBean.class);
			try
			{
				invoice = manager.applyRules(invoice, readOptions(this.optionsString), 
						testLabel == null ? "Next test ..." : "Applying rule: " + testLabel);
				invoice.monetize();
				
				comparator	= new ActComparator();
				comparator.addSortCriterion(ActComparator.c_Id, ActComparator.c_Ascending);
				acts		= new TreeSet<Act>(comparator);
				acts.addAll(invoice.getActs());
				
				if (verbose)
				{
					StringBuilder b = new StringBuilder("RESULT:\n");
					b.append(" #NO    |CODE   |SUFFIX |QUANT. |AMOUNT ");
					b.append(" -------+-------+-------+-------+-------");
					
					for (Act a : acts)
					{
						b.append(" ")
								.append(a.getId()).append("\t| ")
								.append(a.getCode()).append("\t| ")
								.append(a.getSuffixes()).append("\t| ")
								.append(a.getQuantity()).append("\t| ")
								.append(a.getAmount());
					}
					b.append("\n");
					logger.info(b);
				}
				
				LinkedList<ResultBean>	result	= new LinkedList<ResultBean>();
				for (Act a : acts)
					result.add(new ResultBean(invoice, a, result.isEmpty()));
				
				return result;
			}
			catch (Exception e)
			{
				logger.error("Error while applying billing rules.", e);
				return new LinkedList<Act>();
			}
		}
		finally
		{
			invoice		= null;
			lastAct		= null;
		}
	}
	
	
	public Invoice getInvoice ()
	{
		return invoice;
	}
	
	
	public static boolean errorsDuringPreparation ()
	{
		return errorDuringPreparation;
	}
	
	
	/* ======================================== */
	// HELP METHODS
	/* ======================================== */
	
	private static void initDefaults ()
	{
		if (errorsDuringPreparation())
			return;
		
		if (defaultTime == null)
		{
			Integer	patientId	= null;
			
			try
			{
				Properties p = new Properties();
				p.load(BillingRulesFixture.class.getResourceAsStream("resources/test_defaults.properties"));
				
				dateFormatter				= new SimpleDateFormat(p.getProperty("date.format"));
				timeFormatter				= new SimpleDateFormat(p.getProperty("time.format"));
				defaultTime					= timeFormatter.parse(p.getProperty("time.default"));
				defaultDate					= dateFormatter.parse(p.getProperty("date.default"));
				defaultQuantity				= getQuantity(p.getProperty("quantity"));
				defaultKeyValue 			= getKeyValue(p.getProperty("keyValue"), null);
				defaultInsurance			= getInsurance(p.getProperty("insuranceCode"));
				defaultFirstClassRequired	= get1stClassRequired(p.getProperty("firstClassRequired"));
				
				// load the patient, if it is set
				try
				{
					patientId				= Integer.valueOf(p.getProperty("patient.id"));
				}
				catch (Exception e) {}
				
				if (patientId != null)
				{
					try
					{
						PatientAdminInterface manager = (PatientAdminInterface) ManagerFactory.getRemote(PatientAdminBean.class);
						patient				= manager.getPatient(patientId);
					}
					catch (Exception e)
					{
						logger.error("Error while loading patient", e);
					}
				}
				
				if (patient == null)
				{
					patient	= new Patient();
				}
				
				// load the suffixes
				NomenclatureInterface nomenclaturManager	= (NomenclatureInterface) ManagerFactory.getRemote(NomenclatureBean.class);
				Act.setAllSuffixes(nomenclaturManager.getAllSuffixes());
			}
			catch (Exception e)
			{
				// If an exception happens here, something is not configured correctly. 
				// End the test
				errorDuringPreparation = true;
				throw new RuntimeException("Error while initializing the default values.", e);
			}
		}
	}
	
	
	private RuleOptions readOptions (String optionsString)
	{
		boolean		sessionMode;
		int			hospCumulMode;
		int			tierPayantValue;
		int			value;
		Calendar	patientsBirthday;
		Calendar	earliestActDate;
		
		
		// read and set the rule options
		value			= getOptionValue(optionsString, OPTION_PATTERN_SESSION_MODE);
		sessionMode		= (value == 1) ? Boolean.TRUE : Boolean.FALSE;
		
		value			= getOptionValue(optionsString, OPTION_PATTERN_H2D_MODE);
		hospCumulMode	= value;
		
		// read and set the tiers payent (3rd party payer) value
		value	= getOptionValue(optionsString, OPTION_PATTERN_TP_VALUE);
		tierPayantValue	= Integer.valueOf(value);
		
		// read and set the patient age
		value	= getOptionValue(optionsString, OPTION_PATTERN_PATIENT_AGE);
		earliestActDate			= getEarliestActDate(invoice.getActs());
		earliestActDate.add(Calendar.DAY_OF_YEAR, -1);
		patientsBirthday		= new GregorianCalendar();
		patientsBirthday.setTime(patient.getBirthDate());
		if (patientsBirthday.get(Calendar.DAY_OF_YEAR) > earliestActDate.get(Calendar.DAY_OF_YEAR))
			value++;
		patientsBirthday.set(Calendar.YEAR, earliestActDate.get(Calendar.YEAR) - value);
		
		return new RuleOptions(sessionMode, hospCumulMode, patientsBirthday.getTime(), tierPayantValue);
	}
	
	
	private Calendar getEarliestActDate (Set<Act> acts)
	{
		Calendar	earliest	= null;
		Calendar	current;
		
		
		for (Act a : acts)
		{
			current	= a.getPerformedCalendar();
			if (earliest == null || current.before(earliest))
				earliest	= current;
		}
		
		return earliest;
	}
	
	
	private int getOptionValue (String optionsString, Pattern p)
	{
		Matcher m;
		
		m	= p.matcher(optionsString);
		if (m.find())
			return Integer.parseInt(m.group(2));
		else
			throw new RuntimeException("Options (" + optionsString + ") does not contain the pattern " + p.pattern());
	}
	
	
	private static Calendar getCalendar (Date actDate, Date actTime)
	{
		Calendar	date	= new GregorianCalendar();
		Calendar	time	= new GregorianCalendar();
		
		
		time.setTime(actTime);
		date.setTime(actDate);
		date.set(Calendar.HOUR_OF_DAY,	time.get(Calendar.HOUR_OF_DAY));
		date.set(Calendar.MINUTE,		time.get(Calendar.MINUTE));
		date.set(Calendar.SECOND,		time.get(Calendar.SECOND));
		date.set(Calendar.MILLISECOND,	time.get(Calendar.MILLISECOND));
		
		return date;
	}
	
	
	private static Integer getQuantity (String quantityString)
	{
		if (quantityString == null || quantityString.length() <= 0)
			return defaultQuantity;
		else
			return Integer.valueOf(quantityString);
	}
	
	
	private Double getKeyValue (String keyValueString)
	{
		return getKeyValue(keyValueString, lastAct);
	}
	
	
	private static Double getKeyValue (String keyValueString, Act lastAct)
	{
		if (keyValueString == null || keyValueString.length() <= 0)
		{
			if (lastAct == null)
				return defaultKeyValue;
			else 
				return lastAct.getKeyValue();
		}
		else
			return Double.parseDouble(keyValueString
					.replace(",", "."));
	}
	
	
	private static Double getCoefficient (String coefficientString)
	{
		return Double.parseDouble(coefficientString.replace(",", "."));
	}
	
	
	private Date getActTime (String timeString)
	{
		return getActTime(timeString, lastAct);
	}
	
	
	private static Date getActTime (String timeString, Act lastAct)
	{
		if (timeString == null || timeString.trim().length() <= 0)
		{
			if (lastAct == null)
				return defaultTime;
			else
				return lastAct.getPerformedDate();
		}
		else
		{
			try
			{
				return timeFormatter.parse(timeString);
			}
			catch (ParseException e)
			{
				errorDuringPreparation = true;
				throw new RuntimeException("Error parsing time " + timeString, e);
			}
		}
	}
	
	
	private Date getActDate (String dateString)
	{
		return getActDate(dateString, lastAct);
	}
	
	
	private static Date getActDate (String dateString, Act lastAct)
	{
		if (dateString == null || dateString.trim().length() <= 0)
		{
			if (lastAct == null)
				return defaultDate;
			else 
				return lastAct.getPerformedDay();
		}
		else
		{
			try
			{
				int addDays = Integer.parseInt(dateString);
				Calendar calendar = new GregorianCalendar();
				if (lastAct == null)
					calendar.setTime(defaultDate);
				else 
					calendar.setTime(lastAct.getPerformedDay());
				calendar.add(Calendar.DATE, addDays);
				
				return calendar.getTime();
			}
			catch (NumberFormatException e) 
			{
				try
				{
					return dateFormatter.parse(dateString);
				}
				catch (ParseException e1)
				{
					errorDuringPreparation = true;
					throw new RuntimeException("Error parsing time " + dateString, e1);
				}
			}
		}
	}
	
	
	private static Act getRate (String code, Date actDate)
	{
		NomenclatureInterface	manager;
		Act						act	= new Act();
		
		
		try
		{
			act.setPerformedDate(actDate);
			
			manager	= (NomenclatureInterface) ManagerFactory.getRemote(NomenclatureBean.class);
			act		= manager.initializeAct(code, act);
			act.setAdjustment(Act.DEFAULT_ADJUSTMENT);
			
			if (act == null)
			{
				logger.error("No rate found with code "+code);
				errorDuringPreparation = true;
			}
			
			return act;
		}
		catch (Exception e)
		{
			errorDuringPreparation = true;
			throw new RuntimeException("Error fetching rate " + code, e);
		}
	}
	
	
	private static Insurance getInsurance (String acronym)
	{
		Insurance insurance;
		
		
		if (insuranceMap == null)
		{
			List<Insurance> insurances = GECAMedLists.getListReference(Insurance.class);
			insuranceMap	= new HashMap<String, Insurance>(insurances.size());
			
			for (Insurance i : insurances)
				insuranceMap.put(i.getAcronym(), i);
		}
		
		if (acronym == null || acronym.length() <= 0)
			insurance	= defaultInsurance;
		else
			insurance	= insuranceMap.get(acronym);
		
		if (insurance == null)
		{
			errorDuringPreparation = true;
			throw new RuntimeException("No insurance found for acronym " + acronym);
		}
		
		return insurance;
	}
	
	
	private static Boolean get1stClassRequired (String firstClassRequired)
	{
		if (boolValueTrue == null)
			boolValueTrue = Pattern.compile("y(es)?|t(rue)?", Pattern.CASE_INSENSITIVE);
		if (boolValueFalse == null)
			boolValueFalse = Pattern.compile("n(o)?|f(alse)?", Pattern.CASE_INSENSITIVE);
		
		if (firstClassRequired == null || firstClassRequired.trim().length() <= 0)
			return defaultFirstClassRequired;
		
		if (boolValueTrue.matcher(firstClassRequired).matches())
			return Boolean.TRUE;
		else if (boolValueFalse.matcher(firstClassRequired).matches())
			return Boolean.FALSE;
		
		// incorrect data
		errorDuringPreparation = true;
		throw new RuntimeException("Invalid boolean value \""+firstClassRequired+"\"");
	}
	
	
	private HospitalisationClass getHospitalisationClass (String acronym)
	{
		return getHospitalisationClass(acronym, lastAct);
	}
	
	
	private static HospitalisationClass getHospitalisationClass (String acronym, Act lastAct)
	{
		HospitalisationClass clazz;
		
		
		if (clazzMap == null)
		{
			List<HospitalisationClass> clazzes = GECAMedLists.getListReference(HospitalisationClass.class);
			clazzMap	= new HashMap<String, HospitalisationClass>(clazzes.size());
			
			for (HospitalisationClass hc : clazzes)
				clazzMap.put(hc.getAcronym(), hc);
		}
		
		if (acronym == null || acronym.trim().length() <= 0)
		{
			if (lastAct != null)
				acronym = lastAct.getHospitalisationClass();
		}
		
		clazz = clazzMap.get(acronym);
		
		if (clazz == null)
		{
			errorDuringPreparation = true;
			throw new RuntimeException("No hospitalization class found for acronym " + acronym);
		}
		
		return clazz;
	}
	
	
	public static void openResultHtml ()
	{
		String url;
		File output = new File(new StringBuilder(System.getProperty("java.io.tmpdir"))
				.append(File.separator)
				.append("concordion")
				.append(File.separator)
				.append(BillingRulesFixture.class.getPackage().getName().replace(".", File.separator))
				.append(File.separator)
				.append(BillingRulesFixture.class.getSimpleName().replace("Fixture", ".html"))
				.toString());
		
		if (!output.exists())
		{
			logger.warn("Output file \""+output.getAbsolutePath()+"\" does not exist.");
			return;
		}
		
		try
		{
			// get the URL of the file ...
			url = output.toURI().toURL().toString();
			// ... and open it
			UrlOpener.openURL(url);
		}
		catch (MalformedURLException e)
		{
			logger.error("Invalid URL to open the result HTML file.", e);
		}
	}
	
	
//	public static void main (String[] args)
//	{
//		BillingRulesFixture brf = new BillingRulesFixture();
////	
//		brf.addAct(Insurance.INSURANCE_CNS, "no", "A", "2014-01-01", "12:00", "4.0017", "9.75", "C1", "", "1");
//		brf.addAct(Insurance.INSURANCE_CNS, "", "A", "2014-01-02", "12:00", "4.0017", "9.75", "C1", "", "1");
//		brf.applyRules(true);
//		System.out.println();
//		System.out.println(" #NO    |CODE   |SUFFIX |QUANT. |AMOUNT ");
//		System.out.println(" -------+-------+-------+-------+-------");
//		
//		for (Act a : iter)
//		{
//			System.out.println(new StringBuilder(" ")
//					.append(a.getId()).append("\t| ")
//					.append(a.getCode()).append("\t| ")
//					.append(a.getSuffixes()).append("\t| ")
//					.append(a.getQuantity()).append("\t| ")
//					.append(a.getAmount())
//					.toString());
//		}
//		System.out.println();
//	}
}
