package no.valg.valgnatt.backend.mandatberegning;

import static no.valg.valgnatt.backend.mandatberegning.beregning.Stemmetall.StemmetallberegningStatus.INGEN_STEMMER_FUNNET;
import static no.valg.valgnatt.backend.mandatberegning.beregning.Stemmetall.StemmetallberegningStatus.PROGNOSTISERTE_STEMMER;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.annotation.Resource;

import no.valg.valgnatt.backend.mandatberegning.beregning.Distriktsmandat;
import no.valg.valgnatt.backend.mandatberegning.beregning.DistriktsmandatResultat;
import no.valg.valgnatt.backend.mandatberegning.beregning.Stemmetall;
import no.valg.valgnatt.backend.mandatberegning.beregning.Utjevningsmandat;
import no.valg.valgnatt.backend.mandatberegning.beregning.UtjevningsmandatResultat;
import no.valg.valgnatt.database.GrunnlagsdataCache;
import no.valg.valgnatt.database.Konstanter;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.geografi.Kommune;
import no.valg.valgnatt.database.geografi.Land;
import no.valg.valgnatt.database.geografi.Område;
import no.valg.valgnatt.database.hibernate.Stemmeskjema;
import no.valg.valgnatt.database.hibernate.Valgdistrikt;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;

@Component
public class MainMandatBeregning {

	private static final Logger LOG = LogManager.getLogger(MainMandatBeregning.class);
	private static final int KOMMNR_START_POS = 6; // startPosisjon i rekalkArg dersom kommnr finnes (rekalkArg-eks: "st/03/0301")
	private static final int KOMMNR_LENGDE = 4; // kommnr er alltid 4 siffer


	@Resource
	private GrunnlagsdataCache grunnlagsdataCache;
	@Resource
	private MandatBeregningService mandatBeregningService;
	@Resource
	private MandatBeregningLoggService mandatBeregningLoggService;

	public void beregnMandaterForStemmeskjema(List<Stemmeskjema> stemmeskjemaer, boolean opptaltBeregning) {
		Set<String> valgTypeEllerKommnr = new TreeSet<>();
		for (Stemmeskjema stemmeskjema : stemmeskjemaer) {
			if (stemmeskjema.getValgType() == ValgType.KO) {
				valgTypeEllerKommnr.add(stemmeskjema.getKommnr());
			} else {
				valgTypeEllerKommnr.add(stemmeskjema.getValgType().name());
			}
		}
		utførBeregninger(lagMandatberegningArgumenterSet(valgTypeEllerKommnr, opptaltBeregning));
	}

	private Set<MandatBeregningArgumenter> lagMandatberegningArgumenterSet(Set<String> valgTypeEllerKommnr, boolean opptaltBeregning) {
		Set<MandatBeregningArgumenter> mandatBeregningArgumenterSet = new TreeSet<>();
		Map<String, Integer> mandaterPrFylkeST = toMandaterPrFylke(grunnlagsdataCache.getStortingsvalgdistrikter());
		for (String s : valgTypeEllerKommnr) {
			if (s.length() == ValgType.LENGDE) {
				ValgType vt = ValgType.valueOf(s);
				Land land = grunnlagsdataCache.getLand(vt);
				if (vt == ValgType.FY || vt == ValgType.ST) {
					lagMBArgumenterForSTogFY(opptaltBeregning, mandatBeregningArgumenterSet, mandaterPrFylkeST, land);
				} else { //SA-valg
					lagMBArgumenterForSA(mandatBeregningArgumenterSet, land);
				}
			} else { // kommnr => KO-valg
				Kommune kommune = grunnlagsdataCache.getKommune(ValgType.KO, s);
				mandatBeregningArgumenterSet.add(new MandatBeregningArgumenter(kommune, true));
				if (!opptaltBeregning && Konstanter.erPrognosekommune(kommune.getNr())) {
					mandatBeregningArgumenterSet.add(new MandatBeregningArgumenter(kommune, false));
				}
			}
		}
		return mandatBeregningArgumenterSet;
	}

	private void lagMBArgumenterForSTogFY(boolean opptaltBeregning, Set<MandatBeregningArgumenter> mandatBeregningArgumenterSet, Map<String, Integer> mandaterPrFylkeST,
										  Land land) {
		leggTilMBArgumenterMedPrognoseOgResultat(opptaltBeregning, mandatBeregningArgumenterSet, mandaterPrFylkeST, land);
		for (Område fylke : land.getFylker().values()) {
			leggTilMBArgumenterMedPrognoseOgResultat(opptaltBeregning, mandatBeregningArgumenterSet, mandaterPrFylkeST, fylke);
		}
	}

	private void leggTilMBArgumenterMedPrognoseOgResultat(boolean opptaltBeregning, Set<MandatBeregningArgumenter> mandatBeregningArgumenterSet, Map<String, Integer> mandaterPrFylkeST, Område område) {
		mandatBeregningArgumenterSet.add(new MandatBeregningArgumenter(område, true, mandaterPrFylkeST));
		if (!opptaltBeregning) {
            mandatBeregningArgumenterSet.add(new MandatBeregningArgumenter(område, false, mandaterPrFylkeST));
        }
	}

	private void lagMBArgumenterForSA(Set<MandatBeregningArgumenter> mandatBeregningArgumenterSet, Land land) {
		for (Område samevalgdistrikt : land.getSamevalgdistrikter().values()) {
			mandatBeregningArgumenterSet.add(new MandatBeregningArgumenter(samevalgdistrikt, true));
		}
	}

	private static Map<String, Integer> toMandaterPrFylke(Collection<Valgdistrikt> valgdistrikter) {
		Map<String, Integer> result = new TreeMap<>();
		for (Valgdistrikt valgdistrikt : valgdistrikter) {
			// valgdistrikt "00" brukes i ST/FY-valg for utjevningsmandater, men kan ikke knyttes til noe fylke
			if (!valgdistrikt.getId().getNr().equals(Konstanter.VALGDISTRIKT_NORGE)) {
				result.put(valgdistrikt.getId().getNr(), valgdistrikt.getAntallmandater());
			}
		}
		return result;
	}

	private void utførBeregninger(Set<MandatBeregningArgumenter> pending) {
		for (MandatBeregningArgumenter mandatBeregningArgumenter : pending) {
			long start = System.currentTimeMillis();
			beregn(mandatBeregningArgumenter);
			LOG.info("Kjørte Mandatberegning med argumentene " + mandatBeregningArgumenter.id() + "(" + (System.currentTimeMillis() - start) + " ms)");
		}
	}

	/**
	 * MSI des 2016:
	 *
	 * Beregning av mandater gjøres slik for et valgdistrikt (VD):
	 *
	 * 1) Finne stemmetall i VD etter følgende metode
	 * 1.1) Prognose
	 * 1.2) Opptalte stemmer
	 * 1.3) Opptalte listestemmer (finnes i oppgjøret i KO-valg)
	 * 2) Beregne mandater for VD basert på stemmetallene og antall mandater tilgjengelig i VD. To typer beregninger finnes:
	 * 2.1) Distriktsmandater - dette er den vanlige som gjelder for alle kommuner/fylker/samevalgdistrikter
	 * 2.2) Utjevningsmandater - dette er den uvanlige som bare gjelder for land i ST/FY-valg
	 *
	 * Ang FY-valg i 2.2, så gjør man her en utjevningsmandatberegning som OM det var ST-valg, fordi media ønsker den type info.
	 */
	private void beregn(MandatBeregningArgumenter mba) {
		Logg.setSisteStemmeskjemaId(grunnlagsdataCache.getSisteStemmeskjemaId() == null ? 0 : grunnlagsdataCache.getSisteStemmeskjemaId().getId());

		Område område = mba.getOmråde();

		Stemmetall.StemmetallberegningStatus status = Stemmetall.beregn(mba, område, grunnlagsdataCache.getConfiguration().getBetaPartier());
		if (status == INGEN_STEMMER_FUNNET) {
			return;
		}
		boolean prognose = status == PROGNOSTISERTE_STEMMER;
		mandatBeregningService.lagreStemmetallBruktIBeregningen(område, prognose);

		if (mba.erDistriktsmandatBeregning()) {
			DistriktsmandatResultat distriktsmandatResultat = Distriktsmandat.beregnOgOppdater(område, område.getValgdistrikt().getAntallmandater(), false, prognose);
			DistriktsmandatResultat distriktsmandatResultatST = null;
			if (område.getValgType() == ValgType.FY) {
				distriktsmandatResultatST = Distriktsmandat.beregnOgOppdater(område, mba.getAntallMandaterSTValg(område), true, prognose);
			}
			mandatBeregningService.lagreDistriktsmandatberegning(område, distriktsmandatResultat, distriktsmandatResultatST == null ? null : distriktsmandatResultatST
					.getAntallPrParti(), prognose);

		} else if (mba.erUtjevningsmandatBeregning()) {
			mandatBeregningService.lagreDistriktsmandatberegning((Land) område, prognose);
			if (finnesDistriktsmandaterIAlleFylker((Land) område)) {
				UtjevningsmandatResultat utjevningsmandatResultat = Utjevningsmandat.beregnOgOppdater((Land) område, mba.getMandaterPrFylkeST(), prognose);
				mandatBeregningService.lagreUtjevningsmandatberegning((Land) område, utjevningsmandatResultat, prognose);
			}
		}
		mandatBeregningLoggService.logg(mba.getOmråde(), grunnlagsdataCache.getSisteStemmeskjemaId(), prognose);
	}

	private boolean finnesDistriktsmandaterIAlleFylker(Land land) {
		for (no.valg.valgnatt.database.geografi.Fylke fylke : land.getFylker().values()) {
			if (fylke.sumStemmerTotal().getStemmer() == 0) {
				return false; // Kan bare regne utjevningsmandat om det er beregnet distriktsmandater i alle fylker
			}
		}
		return true;
	}

	public void beregnMandaterForPrognoseRelaterteValgdistrikter(boolean opptaltBeregning) {
		Set<String> valgTypeEllerKommnr = new TreeSet<>();
		if (grunnlagsdataCache.hasValgType(ValgType.ST)) {
			valgTypeEllerKommnr.add(ValgType.ST.name());
		}
		if (grunnlagsdataCache.hasValgType(ValgType.FY)) {
			valgTypeEllerKommnr.add(ValgType.FY.name());
		}
		if (grunnlagsdataCache.hasValgType(ValgType.KO)) {
			for (String kommnr : Konstanter.getPrognosekommuner()) {
				// Denne trengs dersom man ikke har "full geografi"
				if (grunnlagsdataCache.getKommune(ValgType.KO, kommnr) != null) {
					valgTypeEllerKommnr.add(kommnr);
				}
			}
		}
		// ValgType.SA har ingen prognoserelaterte rapporter
		utførBeregninger(lagMandatberegningArgumenterSet(valgTypeEllerKommnr, opptaltBeregning));
	}

	public void beregnMandaterGrunnetRekalkulering(boolean opptaltBeregning, String rekalkArg) {
		if (rekalkArg == null) { // reberegn absolutt alt
			beregnAlleValgdistrikter(opptaltBeregning);
		} else {
			beregnUtvalgteValgdistrikter(opptaltBeregning, rekalkArg);
		}
	}

	private void beregnUtvalgteValgdistrikter(boolean opptaltBeregning, String rekalkArg) {
		Set<String> valgTypeEllerKommnr = new TreeSet<>();
		try {
			ValgType valgType = ValgType.valueOf(rekalkArg.substring(0, ValgType.LENGDE).toUpperCase());
			if (valgType == ValgType.KO) {
				leggTilKommuneValgdistrikter(rekalkArg, valgTypeEllerKommnr);
			} else {
				valgTypeEllerKommnr.add(valgType.name());
			}
		} catch (Exception e) {
			LOG.warn("Rekalkuleringsargument " + rekalkArg + " er ikke korrekt, forventer (ko|sa|st|fy)[/<fnr>/<kommnr>] i korrekt valg, mandatberegning " +
					"utføres på 0 områder", e);
			return;
		}
		utførBeregninger(lagMandatberegningArgumenterSet(valgTypeEllerKommnr, opptaltBeregning));
	}

	private void leggTilKommuneValgdistrikter(String rekalkArg, Set<String> valgTypeEllerKommnr) {
		for (Kommune kommune : grunnlagsdataCache.getKommuner(ValgType.KO)) {
			if (rekalkArg.length() == ValgType.LENGDE || rekalkArg.substring(KOMMNR_START_POS, KOMMNR_START_POS + KOMMNR_LENGDE).equals(kommune.getNr())) {
				valgTypeEllerKommnr.add(kommune.getNr());
			}
		}
	}

	private void beregnAlleValgdistrikter(boolean opptaltBeregning) {
		Set<String> valgTypeEllerKommnr = new TreeSet<>();
		for (ValgType valgType : grunnlagsdataCache.getValgTyper()) {
			if (valgType == ValgType.KO) {
				for (Kommune kommune : grunnlagsdataCache.getKommuner(ValgType.KO)) {
					valgTypeEllerKommnr.add(kommune.getNr());
				}
			} else {
				valgTypeEllerKommnr.add(valgType.name());
			}
		}
		utførBeregninger(lagMandatberegningArgumenterSet(valgTypeEllerKommnr, opptaltBeregning));
	}


}
