package info.bukova.isspst.services.tripbill;

import info.bukova.isspst.data.RequirementState;
import info.bukova.isspst.data.SettingsData;
import info.bukova.isspst.data.TripBill;
import info.bukova.isspst.data.TripBillItem;
import info.bukova.isspst.data.TripRequirement;
import info.bukova.isspst.services.AbstractOwnedService;
import info.bukova.isspst.services.HqlDataFilter;
import info.bukova.isspst.services.LazyLoader;
import info.bukova.isspst.services.settings.GlobalSettingsService;
import org.hibernate.Hibernate;
import org.hibernate.LazyInitializationException;
import org.hibernate.Query;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Hours;
import org.joda.time.LocalTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public class TripBillServiceImpl extends AbstractOwnedService<TripBill> implements
		TripBillService {

	@Autowired
	private GlobalSettingsService globalSettings;
	
	@Override
	public TripBill createTripBill(TripRequirement requirement) {
		return createBill(requirement, false);
	}

	@Override
	public TripBill createPassengersBill(TripRequirement requirement) {
		return createBill(requirement, true);
	}

	private TripBill createBill(TripRequirement requirement, boolean passengers) {
		TripBill bill = new TripBill();

		bill.setRequirement(requirement);
		bill.setSeason(requirement.getSeason());

		if (!passengers) {
			bill.setDownPayment(requirement.getDownPayment());
		}

		int daysCount = Days.daysBetween((new DateTime(requirement.getTripDate())).withTimeAtStartOfDay(),
				(new DateTime(requirement.getEndDate())).withTimeAtStartOfDay()).getDays() + 1;

		for (int i = 0 ; i < daysCount ; i++) {
			TripBillItem item = new TripBillItem();
			if (i == 0) {
				item.setTo(requirement.getFrom() + " - " + requirement.getTo());
				item.setToVehicle(requirement.getVehicle());
			}
			if (i == daysCount - 1) {
				item.setBack(requirement.getTo() + " - " + requirement.getFrom());
				item.setBackVehicle(requirement.getVehicle());
			}

			Calendar calTripDate = Calendar.getInstance();
			calTripDate.setTime(requirement.getTripDate());
			calTripDate.add(Calendar.DATE, i);
			item.setDate(calTripDate.getTime());

			bill.getBillItems().add(item);
		}

		this.calculate(bill);

		return bill;
	}

	@Override
	@Transactional
	@LazyLoader({"form", "print"})
	public void loadItems(TripBill entity) {
		try {
			if (entity.getBillItems() == null) {
				throw new LazyInitializationException("");
			}
			
			entity.getBillItems().size();
		} catch (LazyInitializationException ex) {
			TripBill e = dao.getById(entity.getId());
			e.getBillItems().size();
			entity.setBillItems(e.getBillItems());	
		}
	}

	@Transactional
	@LazyLoader("form")
	public void loadAttachments(TripBill entity) {
		TripBill e = dao.getById(entity.getId());
		Hibernate.initialize(e.getAttachedFiles());
		entity.setAttachedFiles(e.getAttachedFiles());
	}

	@Override
	public void calculate(TripBill bill) {
		bill.setTotal(BigDecimal.ZERO);
		
		for (TripBillItem item : bill.getBillItems()) {
			if (!bill.isFreeMeals()) {
				item.setFreeMealsCount(0);
			}
			
			calculateItem(item);
			bill.setTotal(bill.getTotal().add(item.getTotal()));
		}
		
		if (bill.getDownPayment() != null) {
			bill.setTotal(bill.getTotal().subtract(bill.getDownPayment()));
		}
	}
	
	private void calculateItem(TripBillItem item) {
		LocalTime timeTo;
		LocalTime timeBack;
		boolean allDay = false;

		if (item.getToArrival() == null) {
			timeTo = (new LocalTime()).withHourOfDay(0).withMinuteOfHour(0);
		} else {
			timeTo = new LocalTime(item.getToArrival());
		}
				
		if (item.getBackDeparture() == null) {
			timeBack = (new LocalTime()).withHourOfDay(23).withMinuteOfHour(59);
			allDay = true;
		} else {
			timeBack = new LocalTime(item.getBackDeparture());
		}
		
		if (timeTo.isAfter(timeBack)) {
			item.setTotal(BigDecimal.ZERO);
			return; 
		}
		
		SettingsData settings = globalSettings.getSettings();
		int hours = Hours.hoursBetween(timeTo, timeBack).getHours();
		
		if (allDay) {
			++hours;
		}
		
		int refundHour = 0;
		List<Integer> refundHours = new ArrayList<Integer>(settings.getRefunds().keySet());
		Collections.sort(refundHours);
		
		for (int key : refundHours) {
			if (refundHour <= hours && key > hours) {
				break;
			}
			
			refundHour = key;
		}
		
		item.setMeals(settings.getRefunds().get(refundHour)[item.getFreeMealsCount()]);
		
		BigDecimal carefare = item.getCarefare() == null ? BigDecimal.ZERO : item.getCarefare();
		BigDecimal housing = item.getHousing() == null ? BigDecimal.ZERO : item.getHousing();
		BigDecimal otherExp = item.getOtherExpenses() == null ? BigDecimal.ZERO : item.getOtherExpenses();
		BigDecimal distanceAmount = item.getDistanceAmount() == null ? BigDecimal.ZERO : item.getDistanceAmount();
		BigDecimal fuelAmount = item.getFuelAmount() == null ? BigDecimal.ZERO : item.getFuelAmount();
		
		item.setTotal(carefare.add(housing).add(item.getMeals()).add(otherExp).add(distanceAmount).add(fuelAmount));
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_READ')")
	public List<TripBill> getMy() {
		Query q = dao.getQuery("from TripBill");
		HqlDataFilter f = new HqlDataFilter("ownedBy = :owner");
		f.setParam("owner", getLoggedInUser());
		return filterQuery(q, f).list();
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_SHOW_ALL_BILL')")
	public List<TripBill> getAll() {
		return this.execQuery("from TripBill as tb join fetch tb.ownedBy");
	}

	@Override
	@Transactional
	@LazyLoader({"print", "form"})
	public void loadOwner(TripBill bill) {
		TripBill tb = dao.getById(bill.getId());
		tb.getOwnedBy().getFirstName();
		tb.getRequirement().getOwnedBy().getFirstName();
		bill.setOwnedBy(tb.getOwnedBy());
		bill.getRequirement().setOwnedBy(tb.getRequirement().getOwnedBy());
	}

	@Override
	@Transactional
	@LazyLoader("print")
	public void loadPassengers(TripBill bill) {
		Query q = dao.getQuery("from TripRequirement where id = :id");
		q.setParameter("id", bill.getRequirement().getId());
		TripRequirement tr = (TripRequirement) q.uniqueResult();
		tr.getPassengers().size();
		bill.getRequirement().setPassengers(tr.getPassengers());
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_PAY_BILL')")
	public void setPaid(TripBill bill, Date date) {
		if (bill.getApproval() == null || bill.getApproval().getState() != RequirementState.APPROVED) {
			throw new PayException("BillNotApproved");
		}

		bill.setPaid(true);
		bill.setPaidDate(date);
		super.update(bill);
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_EDIT') or hasPermission(#entity, this.getUpdateEntityPermission())")
	public void updateOwned(TripBill bill) {
		bill.setSaved(true);
		update(bill);
	}

	@Override
	@Transactional
	@PreAuthorize("hasPermission(this, 'PERM_EDIT') or hasPermission(#entity, this.getUpdateEntityPermission())")
	public void update(TripBill entity) {
		super.update(entity);

		TripRequirement req = entity.getRequirement();

		if (req.getOwnedBy().equals(entity.getOwnedBy())) {
			Query q = dao.getQuery("from TripBill where requirement = :req and id != :id and (saved = false or saved is null)");
			q.setParameter("req", req);
			q.setParameter("id", entity.getId());

			List<TripBill> bills = q.list();
			for (TripBill tb : bills) {
				tb.getBillItems().clear();

				for (TripBillItem item : entity.getBillItems()) {
					tb.getBillItems().add(new TripBillItem(item));
				}

				tb.setFreeCarfare(entity.isFreeCarfare());
				tb.setFreeHousing(entity.isFreeHousing());
				tb.setFreeMeals(entity.isFreeMeals());
				calculate(tb);
				super.update(tb);
			}
		}
	}

	@Override
	public boolean canPrintRecord(TripBill entity) {
		if (entity.getApproval() != null
				&& entity.getApproval().getState() == RequirementState.APPROVED
				&& (entity.getPaid() == null || !entity.getPaid())) {
			return true;
		}

		return false;
	}
}
