package ch.elexis.core.services;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.eclipse.core.runtime.IStatus;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.elexis.core.ac.AccessControlDefaults;
import ch.elexis.core.common.ElexisEventTopics;
import ch.elexis.core.model.IArticle;
import ch.elexis.core.model.IBillable;
import ch.elexis.core.model.IBillableOptifier;
import ch.elexis.core.model.IBilled;
import ch.elexis.core.model.IBillingSystemFactor;
import ch.elexis.core.model.ICoverage;
import ch.elexis.core.model.IEncounter;
import ch.elexis.core.model.IInvoice;
import ch.elexis.core.model.IMandator;
import ch.elexis.core.model.IPrescription;
import ch.elexis.core.model.InvoiceState;
import ch.elexis.core.model.ModelPackage;
import ch.elexis.core.model.prescription.EntryType;
import ch.elexis.core.model.verrechnet.Constants;
import ch.elexis.core.services.IQuery.COMPARATOR;
import ch.elexis.core.services.holder.ContextServiceHolder;
import ch.elexis.core.services.holder.CoreModelServiceHolder;
import ch.elexis.core.status.StatusUtil;
import ch.rgw.tools.Result;
import ch.rgw.tools.Result.SEVERITY;

@Component
public class BillingService implements IBillingService {
	
	private static Logger logger = LoggerFactory.getLogger(BillingService.class);
	
	@Reference(target = "(" + IModelService.SERVICEMODELNAME + "=ch.elexis.core.model)")
	private IModelService coreModelService;
	
	@Reference
	private IAccessControlService accessControlService;
	
	@Reference
	private IStockService stockService;
	
	@Reference
	private IContextService contextService;
	
	private List<IBilledAdjuster> billedAdjusters = new ArrayList<>();
	
	@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
	public void setBilledAdjuster(IBilledAdjuster adjuster){
		if (!billedAdjusters.contains(adjuster)) {
			billedAdjusters.add(adjuster);
		}
	}
	
	public void unsetBilledAdjuster(IBilledAdjuster adjuster){
		if (billedAdjusters.contains(adjuster)) {
			billedAdjusters.remove(adjuster);
		}
	}
	
	private List<IBillableAdjuster> billableAdjusters = new ArrayList<>();
	
	@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
	public void setBillableAdjuster(IBillableAdjuster adjuster){
		if (!billableAdjusters.contains(adjuster)) {
			billableAdjusters.add(adjuster);
		}
	}
	
	public void unsetBillableAdjuster(IBillableAdjuster adjuster){
		if (billableAdjusters.contains(adjuster)) {
			billableAdjusters.remove(adjuster);
		}
	}
	
	@Override
	public Result<IEncounter> isEditable(IEncounter encounter){
		ICoverage coverage = encounter.getCoverage();
		if (coverage != null) {
			if (!coverage.isOpen()) {
				return new Result<>(SEVERITY.WARNING, 0,
					"Diese Konsultation gehört zu einem abgeschlossenen Fall", encounter, false);
			}
		}
		
		IMandator encounterMandator = encounter.getMandator();
		boolean checkMandant =
			!accessControlService.request(AccessControlDefaults.LSTG_CHARGE_FOR_ALL);
		boolean mandatorOk = true;
		boolean invoiceOk = true;
		IMandator activeMandator =
			ContextServiceHolder.get().getActiveMandator().orElse(null);
		boolean mandatorLoggedIn = (activeMandator != null);
		
		// if m is null, ignore checks (return true)
		if (encounterMandator != null && activeMandator != null) {
			if (checkMandant && !(encounterMandator.getId().equals(activeMandator.getId()))) {
				mandatorOk = false;
			}
			
			IInvoice rn = encounter.getInvoice();
			if (rn == null) {
				invoiceOk = true;
			} else {
				InvoiceState state = rn.getState();
				if (state == InvoiceState.CANCELLED) {
					invoiceOk = true;
				} else {
					invoiceOk = false;
				}
			}
		}
		
		boolean ok = invoiceOk && mandatorOk && mandatorLoggedIn;
		if (ok) {
			return new Result<>(encounter);
		} else {
			String msg = "";
			if (!mandatorLoggedIn) {
				msg = "Es ist kein Mandant eingeloggt";
			} else {
				if (!invoiceOk) {
					msg = "Für diese Behandlung wurde bereits eine Rechnung erstellt.";
				} else {
					msg = "Diese Behandlung ist nicht von Ihnen";
				}
			}
			return new Result<>(SEVERITY.WARNING, 0, msg, encounter, false);
		}
	}
	
	@Override
	public Result<IBilled> bill(IBillable billable, IEncounter encounter, double amount){
		IBillable beforeAdjust = billable;
		CoreModelServiceHolder.get().refresh(encounter, true);
		logger.info("Billing [" + amount + "] of [" + billable + "] on [" + encounter + "]");
		for (IBillableAdjuster iBillableAdjuster : billableAdjusters) {
			billable = iBillableAdjuster.adjust(billable, encounter);
		}
		if (billable != null) {
			Result<IBillable> verificationResult =
				billable.getVerifier().verifyAdd(billable, encounter, amount);
			if (verificationResult.isOK()) {
				IBillableOptifier optifier = billable.getOptifier();
				Result<IBilled> optifierResult = optifier.add(billable, encounter, amount);
				
				if (billable instanceof IArticle) {
					IStatus status =
						stockService.performSingleDisposal((IArticle) billable, doubleToInt(amount),
							contextService.getActiveMandator().map(m -> m.getId()).orElse(null));
					if (!status.isOK()) {
						StatusUtil.logStatus(logger, status, true);
					}
				}
				
				// TODO refactor
				if (!optifierResult.isOK() && optifierResult.getCode() == 11) {
					String initialResult = optifierResult.toString();
					// code 11 is tarmed exclusion due to side see TarmedOptifier#EXKLUSIONSIDE
					// set a context variable to specify the side see TarmedLeistung#SIDE, TarmedLeistung#SIDE_L, TarmedLeistung#SIDE_R
					optifier.putContext("Seite", "r");
					optifierResult = optifier.add(billable, encounter, amount);
					if (!optifierResult.isOK() && optifierResult.getCode() == 11) {
						optifier.putContext("Seite", "l");
						optifierResult = optifier.add(billable, encounter, amount);
					}
					if (optifierResult.isOK()) {
						String message = "Achtung: " + initialResult
							+ "\n Es wurde bei der Position " + billable.getCode()
							+ " automatisch die Seite gewechselt."
							+ " Bitte korrigieren Sie die Leistung falls dies nicht korrekt ist.";
						optifierResult.addMessage(SEVERITY.OK, message);
					}
					optifier.clearContext();
				}
				
				if (optifierResult.get() != null) {
					for (IBilledAdjuster iBilledAdjuster : billedAdjusters) {
						iBilledAdjuster.adjust(optifierResult.get());
					}
				}
				
				return optifierResult;
			} else {
				return translateResult(verificationResult);
			}
		} else {
			return new Result<IBilled>(Result.SEVERITY.WARNING, 1, "Folgende Leistung '"
				+ beforeAdjust.getCode()
				+ "' konnte im aktuellen Kontext (Fall, Konsultation, Gesetz) nicht verrechnet werden.",
				null, false);
		}
	}
	
	/**
	 * Get double as int rounded half up.
	 * 
	 * @param value
	 * @return
	 */
	private int doubleToInt(double value){
		BigDecimal bd = new BigDecimal(value);
		bd = bd.setScale(0, RoundingMode.HALF_UP);
		return bd.intValue();
	}
	
	@Override
	public Result<?> removeBilled(IBilled billed, IEncounter encounter){
		Result<IEncounter> editable = isEditable(encounter);
		if(!editable.isOK()) {
			return editable;
		}
		
		encounter.removeBilled(billed);
		
		IBillable billable = billed.getBillable();
		if(billable instanceof IArticle) {
			
			// TODO stock return via event
			IArticle article = (IArticle) billable;
			String mandatorId = contextService.getActiveMandator().map(m->m.getId()).orElse(null);
			stockService.performSingleReturn(article, (int) billed.getAmount(), mandatorId);
			
			// TODO prescription via event
			Object prescId = billed.getExtInfo(Constants.FLD_EXT_PRESC_ID);
			if(prescId instanceof String) {
				IPrescription prescription = coreModelService.load((String)prescId, IPrescription.class).orElse(null);
				if(prescription != null && EntryType.SELF_DISPENSED == prescription.getEntryType()) {
					coreModelService.remove(prescription);
					ContextServiceHolder.get().postEvent(ElexisEventTopics.EVENT_RELOAD, prescription);
				}
			}
		}
		
		return Result.OK();
	}

	private Result<IBilled> translateResult(Result<IBillable> verificationResult){
		Result<IBilled> ret = new Result<>();
		verificationResult.getMessages()
			.forEach(msg -> ret.addMessage(msg.getSeverity(), msg.getText()));
		return ret;
	}
	
	@Override
	public Optional<IBillingSystemFactor> getBillingSystemFactor(String system, LocalDate date){
		IQuery<IBillingSystemFactor> query =
			coreModelService.getQuery(IBillingSystemFactor.class);
		query.and(ModelPackage.Literals.IBILLING_SYSTEM_FACTOR__SYSTEM, COMPARATOR.EQUALS, system);
		query.and(ModelPackage.Literals.IBILLING_SYSTEM_FACTOR__VALID_FROM,
			COMPARATOR.LESS_OR_EQUAL, date);
		query.and(ModelPackage.Literals.IBILLING_SYSTEM_FACTOR__VALID_TO,
			COMPARATOR.GREATER_OR_EQUAL, date);
		return query.executeSingleResult();
	}
	
	@Override
	public void setBillingSystemFactor(LocalDate from, LocalDate to, double factor, String system){
		if (to == null) {
			// 20380118, TimeTool.END_OF_UNIX_EPOCH
			to = LocalDate.of(2038, 1, 18);
		}
		
		IQuery<IBillingSystemFactor> query =
				coreModelService.getQuery(IBillingSystemFactor.class);
		query.and(ModelPackage.Literals.IBILLING_SYSTEM_FACTOR__SYSTEM, COMPARATOR.EQUALS, system);
		List<IBillingSystemFactor> existingWithSystem = query.execute();
		for (IBillingSystemFactor iBillingSystemFactor : existingWithSystem) {
			if (iBillingSystemFactor.getValidTo() == null
				|| iBillingSystemFactor.getValidTo().isAfter(from)) {
				iBillingSystemFactor.setValidTo(from);
				coreModelService.save(iBillingSystemFactor);
			}
		}
		IBillingSystemFactor billingSystemFactor =
			coreModelService.create(IBillingSystemFactor.class);
		billingSystemFactor.setFactor(factor);
		billingSystemFactor.setSystem(system);
		billingSystemFactor.setValidFrom(from);
		billingSystemFactor.setValidTo(to);
		coreModelService.save(billingSystemFactor);
	}
}