/**
 * 
 */
package com.ejie.ad74a.service.impl;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.ejie.ad74a.exception.ResourceNotFoundException;
import com.ejie.ad74a.model.Copago;
import com.ejie.ad74a.model.Periodo;
import com.ejie.ad74a.model.Piso;
import com.ejie.ad74a.model.PrecioBeneficiario;
import com.ejie.ad74a.model.PrecioPublico;
import com.ejie.ad74a.model.Solicitante;
import com.ejie.ad74a.model.TipoPrecio;
import com.ejie.ad74a.service.CopagoService;
import com.ejie.ad74a.service.PrecioService;
import com.ejie.ad74a.util.UtilDates;
import com.ejie.ad74a.util.UtilPrecios;

/**
 * Implementacion de la interfaz de servicio para copagos
 * @author BICUGUAL
 *
 */
@Service
public class CopagoServiceImpl implements CopagoService {

	@Autowired
	PrecioService precService;

	@Override
	public List<Copago> getLstCopagosBeneficiario(Periodo mensualidad, Solicitante solicitante,
			Piso pisoTutelado) {

		List<Copago> lstCopagosBeneficiario = new ArrayList<>();

		if (null != solicitante.getnPreciosBeneficiario() && solicitante.getnPreciosBeneficiario().intValue() > 0) {

			// Obtener los precios beneficiario para el periodo mensual
			List<PrecioBeneficiario> lstPrecios = null == solicitante.getLstPreciosBeneficiario()
					? precService.getLstPreciosBeneficiarioByIdSolicitanteAndFInicioMes(solicitante.getIdSolicitante(),
							mensualidad.getFInicioAsDate(), mensualidad.getFFinAsDate())
					: solicitante.getLstPreciosBeneficiario();

			for (PrecioBeneficiario precioBeneficiario : lstPrecios) {

				LocalDate fechaInicioPrecio = UtilDates
						.getLocalDateMayor(precioBeneficiario.getFechaInicio(), mensualidad.getfInicio(),
								solicitante.getfInicio(), null == pisoTutelado ? null : pisoTutelado.getfDesde())
						.orElseThrow(() -> new ResourceNotFoundException(
								"No se ha podido establecer una fecha de inicio del precio beneficiario para el periodo"));

				LocalDate fechaFinPrecio = UtilDates
						.getLocalDateMenor(precioBeneficiario.getFechaFin(), mensualidad.getfFin(),
								solicitante.getfBaja(), null == pisoTutelado ? null : pisoTutelado.getfHasta())
						.orElseThrow(() -> new ResourceNotFoundException(
								"No se ha podido establecer una fecha de fin del precio beneficiario para el periodo"));

				Copago copago = new Copago(TipoPrecio.BENEFICIARIO, mensualidad, precioBeneficiario.getPrecioPrivado(),
						fechaInicioPrecio, fechaFinPrecio, precioBeneficiario.getIndExento());

				lstCopagosBeneficiario.add(copago);

			}

		}

		return lstCopagosBeneficiario;
	}

	@Override
	public List<Copago> getLstCopagosPublicos(Periodo mensualidad, Solicitante solicitante,
			List<Copago> lstCopagosBeneficiario, Piso piso) {

		PrecioPublico precioPublico = null == solicitante.getPrecioPublico()
				? precService.getPrecioPublico(mensualidad.getFInicioAsDate(), mensualidad.getFFinAsDate())
				: solicitante.getPrecioPublico();

		LocalDate fechaInicioPrecio = UtilDates
				.getLocalDateMayor(mensualidad.getfInicio(), solicitante.getfInicio(),
						null == piso ? null : piso.getfDesde())
				.orElseThrow(() -> new ResourceNotFoundException(
						"No se ha podido establecer una fecha de inicio del precio publico para el periodo"));

		LocalDate fechaFinPrecio = UtilDates.getLocalDateMenor(mensualidad.getfFin(),
				null != solicitante.getfBaja() ? solicitante.getfBaja().minusDays(1) : null, null == piso ? null
						: piso.getfHasta())
				.orElseThrow(() -> new ResourceNotFoundException(
						"No se ha podido establecer una fecha de fin del precio publico para el periodo"));

		// Si no hay copagosBeneficiario, solo habra un copago publico para la
		// mensualidad. Solo se puede tener un precio publico en cada mensualidad
		if (null == lstCopagosBeneficiario || lstCopagosBeneficiario.isEmpty()) {

			Copago copagoPublico = new Copago(TipoPrecio.PUBLICO, mensualidad, precioPublico.getImporte(),
					fechaInicioPrecio, fechaFinPrecio, false);

			return Arrays.asList(copagoPublico);

		}
		// Si el socicitante tiene copagosBeneficiario
		else {

			// Recorremos todos los dias del periodo para identificar cuales no tienen
			// asignado un precio beneficiario
			List<LocalDate> lstDiasPrecioPublico = new ArrayList<>();

			for (LocalDate date = fechaInicioPrecio; date.isBefore(fechaFinPrecio) || date.isEqual(fechaFinPrecio); date = date.plusDays(1)) {

				final LocalDate dia = date;

				// Recorre copagos beneficario para ver si el dia tiene asignado ya un precio
				// beneficiario
				boolean isPrecioBeneficiarioDate = lstCopagosBeneficiario.stream()
						.anyMatch(copagoBeneficiario -> UtilDates.isDateWithinRage(dia, copagoBeneficiario.getfInicio(),
								copagoBeneficiario.getfFin()));

				if (!isPrecioBeneficiarioDate) {
					lstDiasPrecioPublico.add(date);
				}
			}

			// Si existen dias con precio publico se recorren para agruparlos en copagos
			return !lstDiasPrecioPublico.isEmpty()
					? this.lstDiasPrecioPublicoToLstCopagosPublicos(lstDiasPrecioPublico, mensualidad, precioPublico)
					: new ArrayList<>();

		}
	}

	@Override
	public List<Copago> fusionarCopagosSolicitante(List<Copago> lstCopagosBeneficiario,List<Copago> lstCopagosPublicos) {
		List<Copago> lstCopagos = new ArrayList<>();
		
		//Agrupar copagos mensuales
		lstCopagos.addAll(this.fusionarCopagosByFechas(lstCopagosBeneficiario));
		lstCopagos.addAll(this.fusionarCopagosByFechas(lstCopagosPublicos));
		
		return lstCopagos.stream()
				//Filtrar copago con exencion
				.filter(copago -> !copago.isIndExento())
				//Filtrar copagos importe 0
				.filter(copago -> copago.getImporteMensual().compareTo(BigDecimal.ZERO) > 0)
				//Ordenar copagos por fecha inicio
				.sorted(Comparator.comparing(Copago::getfInicio)).collect(Collectors.toList());
	}
	
	
	/**
	 * Recorre la lista de dias publicos agrupadolos en una lista de copagos.
	 * 
	 * @param lstDiasPrecioPublico
	 * @param mensualidad
	 * @param precioPublico
	 * @return
	 */
	private List<Copago> lstDiasPrecioPublicoToLstCopagosPublicos(List<LocalDate> lstDiasPrecioPublico,
			Periodo mensualidad, PrecioPublico precioPublico) {
		List<Copago> lstCopagosPublicos = new ArrayList<>();

		// Cargo los datos de inicio de copago
		Copago copagoPublico = new Copago(TipoPrecio.PUBLICO, mensualidad, precioPublico.getImporte(),
				lstDiasPrecioPublico.get(0));

		for (int i = 1; i < lstDiasPrecioPublico.size() - 1; i++) {

			LocalDate currentDay = lstDiasPrecioPublico.get(i);
			LocalDate previousDay = lstDiasPrecioPublico.get(i - 1);

			// Si el dia de la posicion anterior no es contiguo
			if (!currentDay.isEqual(previousDay.plusDays(1))) {
				// calculo los datos con el fin de copago e inserto en la lista
				lstCopagosPublicos.add(this.setFechaFinCopago(copagoPublico, previousDay));

				// Cargo los datos de inicio de copago
				copagoPublico = new Copago(TipoPrecio.PUBLICO, mensualidad, precioPublico.getImporte(), currentDay);
			}
		}

		// calculo los datos con el fin de copago e inserto en la lista
		lstCopagosPublicos.add(this.setFechaFinCopago(copagoPublico,
				lstDiasPrecioPublico.get(lstDiasPrecioPublico.size() - 1)));

		return lstCopagosPublicos;
	}

	/**
	 * Dada una instancia de copago que se ha creado sin conocer su fecha de fin,
	 * una vez conocida, el metodo setea y calcula la fecha de fin, importe, ndias y
	 * meses
	 * 
	 * @param copago
	 * @param diaFinCopago
	 * @return
	 */
	private Copago setFechaFinCopago(Copago copago, LocalDate diaFinCopago) {

		copago.setfFin(diaFinCopago);
		int nDiasCopago = UtilDates.getDaysBetweenTwoLocalDates(copago.getfInicio(), copago.getfFin())
				.intValue();

		if (copago.getMensualidad().getNdias() == nDiasCopago) {
			copago.setnMeses(1);
		} else {
			copago.setnDias(nDiasCopago);
		}

		copago.setImporte(UtilPrecios.calcularImporte(nDiasCopago, copago.getMensualidad().getNdias(),
				copago.getImporteMensual()));

		return copago;
	}
	
	/**
	 * Fusion para lista copagos con mismo tipo de precios (beneficiarios o
	 * publicos). Agrupa en un solo copago los copagos con mismo importe cuando la
	 * fecha de ultimo dia del copago previo es igual al primer dia del siguiente
	 * copago
	 * 
	 * @param lstCopagos
	 * @return
	 */
	private List<Copago> fusionarCopagosByFechas(List<Copago> lstCopagos) {
		List<Copago> fusionCopagos = new ArrayList<>();

		if (!lstCopagos.isEmpty()) {

			Copago previous = lstCopagos.get(0);

			for (int i = 1; i < lstCopagos.size(); i++) {

				Copago current = lstCopagos.get(i);

				// Comparar copagos
				if (current.getImporteMensual().equals(previous.getImporteMensual())
						&& previous.getfFin().plusDays(1).isEqual(current.getfInicio())) {
					// Cambio fecha fin
					previous.setfFin(current.getfFin());
					previous.getMensualidad().setfFin(current.getMensualidad().getfFin());
					// Sumatorio ndias
					previous.setnDias(previous.getnDias() + current.getnDias());
					// Sumatorio nMeses
					previous.setnMeses(previous.getnMeses() + current.getnMeses());
					// Sumatorio de importe
					previous.setImporte(previous.getImporte().add(current.getImporte()));
				} else {
					fusionCopagos.add(previous);
					previous = current;
				}
			}

			fusionCopagos.add(previous);
		}

		return fusionCopagos;
	}	
}
