package no.valg.valgnatt.backend.rapport;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListMap;

import javax.annotation.Resource;
import no.valg.valgnatt.backend.MainBackend;
import no.valg.valgnatt.database.Configuration;
import no.valg.valgnatt.database.GrunnlagsdataCache;
import no.valg.valgnatt.database.GrunnlagsdataService;
import no.valg.valgnatt.database.Konstanter;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.geografi.Fylke;
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.Rapport;
import no.valg.valgnatt.database.hibernate.Stemmekrets;
import no.valg.valgnatt.database.hibernate.Stemmeskjema;
import no.valg.valgnatt.database.hibernate.Tilstand;

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

@Component
public class MainRapport {

	private static final Logger LOG = LogManager.getLogger(MainRapport.class);

	private NavigableMap<Integer, RappCacheEntry> rapporterIdMap = new ConcurrentSkipListMap<>();
	private Map<String, RappCacheEntry> rapporterNavnMap = new TreeMap<>();
	private List<Sender> senders = new ArrayList<>();

	@Resource
	private MainBackend mainBackend;
	@Resource
	private RapportService rapportService;
	@Resource
	private Configuration configuration;
	@Resource
	private GrunnlagsdataCache grunnlagsdataCache;
	@Resource
	private GrunnlagsdataService grunnlagsdataService;

	public void initialiser() {
		rapporterIdMap = new ConcurrentSkipListMap<>();
		rapporterNavnMap = new TreeMap<>();
		senders = new ArrayList<>();
		NavigableSet<String> pending = alleRapporter();
		List<Rapport> reports = rapportService.getReports(); // henter rapporter som allerede finnes
		for (Rapport report : reports) {
			// Ikke lag rapporter på nytt dersom de allerede er laget, men legg dem til cache
			pending.remove(report.getRappnavn());
			leggRapporterTilCache(report.getRappnavn(), report.getRappsekvens(), report.getJson());
		}
		lagRapporter(pending);

		for (String URL : configuration.getFrontendMediaURLer()) {
			Sender sender = new Sender(URL, this, false);
			sender.start();
			senders.add(sender);
		}
		for (String URL : configuration.getFrontendOffentligURLer()) {
			Sender sender = new Sender(URL, this, true);
			sender.start();
			senders.add(sender);
		}
	}

	boolean sperrefrist(boolean offentlig) {
		try {
			Tilstand tilstand = grunnlagsdataService.getTilstand(offentlig ? "sperrefrist-offentlig" : "sperrefrist-media");
			return tilstand == null || "sann".equals(tilstand.getVerdi());
		} catch (Exception e) {
			LOG.error("Klarte ikke å lese ut tilstanden for " + (offentlig ? "sperrefrist-offentlig" : "sperrefrist-media") + ", dermed gjelder sperrefrist!", e);
			return true; // default er sperrefrist, uansett hva slags feil som måtte oppstå i spørringen
		}
	}

	public NavigableMap<Integer, RappCacheEntry> getRapporterIdMap() {
		return rapporterIdMap;
	}

	public void stoppUtsendelseAvRapporter() {
		for (Sender sender : senders) {
			sender.stopp();
		}
	}

	public void rekalkulerRapporter(String... rekalkulerArgumenter) {
		NavigableSet<String> allReports = alleRapporter();

		NavigableSet<String> rapporterSomSkalRekalkuleres = avgjørHvilkeRapporterSomSkalRekalkuleres(allReports, rekalkulerArgumenter);

		for (Sender sender : senders) {
			sender.starterRapportProduksjon();
		}
		lagRapporter(rapporterSomSkalRekalkuleres);
		for (Sender sender : senders) {
			sender.avslutterRapportProduksjon();
		}
	}

	private NavigableSet<String> avgjørHvilkeRapporterSomSkalRekalkuleres(NavigableSet<String> allReports, String[] rekalkulerArgumenter) {
		NavigableSet<String> rapporterSomSkalRekalkuleres = new TreeSet<>(new RapportNavnComparator());
		for (String report : allReports) {
			if (rekalkulerArgumenter.length > 0 && rekalkulerArgumenter[0] != null) { // Legg til de rapporter som er angitt av rekalkulerArgumenter
				filtrerRapporterSomSkalRekalkulereres(rekalkulerArgumenter, rapporterSomSkalRekalkuleres, report);
			} else { // Legg til alle rapporter
				rapporterSomSkalRekalkuleres.add(report);
			}
		}
		return rapporterSomSkalRekalkuleres;
	}

	private void filtrerRapporterSomSkalRekalkulereres(String[] rekalkulerArgumenter, NavigableSet<String> rapporterSomSkalRekalkuleres, String report) {
		for (String rekalkulerArgument : rekalkulerArgumenter) {
			if (rekalkulerArgument.endsWith("$") && report.length() == rekalkulerArgument.length() - 1) {
				rekalkulerArgument = rekalkulerArgument.substring(0, rekalkulerArgument.length() - 1);
				if (rekalkulerArgument.equals(report)) {
					rapporterSomSkalRekalkuleres.add(report);
				}
			}
			if (report.startsWith(rekalkulerArgument)) {
				rapporterSomSkalRekalkuleres.add(report);
			}
		}
	}

	public void process(List<Stemmeskjema> rader) {
		NavigableSet<String> pending = nødvendigeRapporter(rader);
		for (Sender sender : senders) {
			sender.starterRapportProduksjon();
		}
		lagRapporter(pending);
		for (Sender sender : senders) {
			sender.avslutterRapportProduksjon();
		}
	}

	private NavigableSet<String> nødvendigeRapporter(List<Stemmeskjema> rader) {
		NavigableSet<String> pending = new TreeSet<>(new RapportNavnComparator());
		Set<ValgType> valgtyper = new HashSet<>();
		for (Stemmeskjema stemmeskjema : rader) {
			valgtyper.add(stemmeskjema.getValgType());
			Land land = grunnlagsdataCache.getLand(stemmeskjema.getValgType());
			pending.add(land.getRapportid());
			Kommune kommune = land.getKommune(stemmeskjema.getKommnr());
			if (stemmeskjema.getValgType() == ValgType.SA) {
				String samevalgdistriktnr = kommune.getValgdistrikt().getId().getNr();
				pending.add(land.getSamevalgdistrikt(samevalgdistriktnr).getRapportid());
			} else {
				leggTilNødvendigeRapporterForSTFYKOValg(pending, stemmeskjema, land, kommune);
			}
		}
		for (ValgType valgType : valgtyper) {
			pending.addAll(alleTotalrapportNavn(valgType));
			if (valgType == ValgType.FY || valgType == ValgType.ST) {
				pending.add(valgType.name().toLowerCase() + "/ujm"); // Utjevningsmandatrapporten
			}
		}
		return pending;
	}

	private void leggTilNødvendigeRapporterForSTFYKOValg(NavigableSet<String> pending, Stemmeskjema stemmeskjema, Land land, Kommune kommune) {
		Stemmekrets stemmekrets = kommune.getStemmekrets(stemmeskjema.getKretsnr());
		if (kommune.harKretsvisOpptelling()) { // regner ikke ut rapport for stemmekrets dersom det bare finnes en enkelt stemmekrets i kommunene
			// (Kommunerapport=Stemmekretsrapport)
			pending.add(stemmekrets.getRapportid());
		}
		pending.add(kommune.getRapportid());
		if (stemmeskjema.getValgType() == ValgType.ST) { // Vi må oppdatere alle fylker i ST-valg, fordi utjevningsmandater kan flytte seg mellom fylker
			for (Fylke fylke : land.getFylker().values()) {
				pending.add(fylke.getRapportid());
			}
		} else {
			pending.add(kommune.getFylke().getRapportid());
		}
		if (kommune.getNr().equals(Konstanter.OSLO_KNR)) {
			pending.add(kommune.getBydel(stemmekrets.getBydelnr()).getRapportid());
			if (stemmeskjema.getValgType() == ValgType.KO) { // Dersom Oslo i KO-valget, så må vi også oppdatere FY-rapporter (fordi Oslo-stemmer blir kopiert
				// inn i FY-valget)
				Fylke osloFylke = grunnlagsdataCache.getFylke(ValgType.FY, Konstanter.OSLO_FNR);
				pending.add(osloFylke.getRapportid());
				pending.add(osloFylke.getLand().getRapportid());
			}
		}
	}

	private void lagRapporter(NavigableSet<String> pending) {
		for (String rapportNavn : pending) {
			int sisteSekvensNr = 1;
			if (rapporterIdMap.size() > 0) {
				sisteSekvensNr = rapporterIdMap.lastEntry().getValue().getSeqNr();
			}
			Rapport rapport = rapportService.lagRapport(rapportNavn, this, sisteSekvensNr);
			leggRapporterTilCache(rapportNavn, rapport.getRappsekvens(), rapport.getJson());
		}
	}

	private void leggRapporterTilCache(String rapportNavn, Integer seqNr, String json) {
		RappCacheEntry rce = new RappCacheEntry(rapportNavn, seqNr, json);
		RappCacheEntry old = rapporterNavnMap.put(rapportNavn, rce);
		rapporterIdMap.put(seqNr, rce);
		if (old != null) {
			rapporterIdMap.remove(old.getSeqNr());
		}
	}

	public boolean erRapporterSynketTilAlleKlienter() {
		for (Sender sender : senders) {
			if (sender.rapportProduksjonPågår()) {
				return false;
			}
			if (!sender.erRapportMottakerOppdatert()) {
				return false;
			}
		}
		return true;
	}

	Map<String, MainRapport.RappCacheEntry> getRapportNavnMap() {
		return rapporterNavnMap;
	}

	GrunnlagsdataCache getGrunnlagsdataCache() {
		return grunnlagsdataCache;
	}

	private NavigableSet<String> alleRapporter() {
		NavigableSet<String> pending = new TreeSet<>(new RapportNavnComparator());
		for (ValgType valgtype : grunnlagsdataCache.getValgTyper()) {
			Område område = grunnlagsdataCache.getLand(valgtype);
			//ResultatRapport
			pending.add(område.getRapportid());
			pending.addAll(finnAllePåLavereNivå(område));
			//TotalRapport
			pending.addAll(alleTotalrapportNavn(valgtype));
		}
		if (grunnlagsdataCache.hasValgType(ValgType.ST)) {
			pending.add("st/ujm"); // Utjevningsmandatrapporten
		}
		if (grunnlagsdataCache.hasValgType(ValgType.FY)) {
			pending.add("fy/ujm");
		}
		return pending;
	}

	private NavigableSet<String> finnAllePåLavereNivå(Område område) {
		NavigableSet<String> set = new TreeSet<>(new RapportNavnComparator());
		for (Område delOmråde : område.getUnderordnedeRapportOmråder()) {
			set.add(delOmråde.getRapportid());
			set.addAll(finnAllePåLavereNivå(delOmråde));
		}
		return set;
	}

	private Collection<String> alleTotalrapportNavn(ValgType valgtype) {
        List<String> rapporter = new ArrayList<>();
        rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/partier/land");
        rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/stemmer/land");
        rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/partier/fylke");
        rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/stemmer/fylke");
        if (valgtype != ValgType.SA) {
            rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/partier/kommune");
            rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/stemmer/kommune");
            rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/partier/stemmekrets");
            rapporter.add("totalrapport/" + valgtype.name().toLowerCase() + "/stemmer/stemmekrets");
        }
		return rapporter;
	}

	public String rapporter2JsonSamlerapport(int nyesteRapportIdHosMottaker) {
		NavigableMap<Integer, MainRapport.RappCacheEntry> tailRapp = rapporterIdMap.tailMap(nyesteRapportIdHosMottaker, false);
		StringBuilder sb = new StringBuilder();

		boolean first = true;
		sb.append("{\n");
		sb.append("\"instance\":" + MainBackend.getInstance() + ",\n");
		sb.append("\"valgaar\": " + getValgAar() + ",\n");
		sb.append("\"stemmetallmottatt\":" + stemmetallMottatt() + ",\n");
		sb.append("\"report\":[\n");
		for (MainRapport.RappCacheEntry rce : tailRapp.values()) {
			if (!first) {
				sb.append(",\n");
			}
			sb.append("{");
			sb.append("\"fullName\":\"").append("/").append(getValgAar()).append("/").append(rce.getFullName()).append("\",");
			sb.append("\"seqNr\":").append(rce.getSeqNr()).append(",");
			sb.append("\"content\":").append(rce.getJson());
			sb.append("}");
			first = false;
		}
		sb.append("\n]\n");
		sb.append("}\n");
		return sb.toString();
	}

	Integer getValgAar() {
		return grunnlagsdataCache.getValgAar();
	}

	boolean stemmetallMottatt() {
		for (ValgType vt : grunnlagsdataCache.getValgTyper()) {
			if (grunnlagsdataCache.getLand(vt).sumStemmerTotal().getStemmer() > 0) {
				return true;
			}
		}
		return false;
	}

	public static class RappCacheEntry {
		private String fullName;
		private Integer seqNr;
		private String json;

		RappCacheEntry(String fullName, Integer seqNr, String json) {
			this.fullName = fullName;
			this.seqNr = seqNr;
			this.json = json;
		}

		String getFullName() {
			return fullName;
		}

		public Integer getSeqNr() {
			return seqNr;
		}

		String getJson() {
			return json;
		}
	}

	/**
	 * Vi trenger å sortere rapportnavnene, slik at produksjonen av rapportene går i korrekt
	 * rekkefølge. Grunnen er at man i ResultatRapporten i feltet "lenker.rel.{k}.rapportGenerert"
	 * henter opp dette tidspunktet fra rapportcache for underordnede rapporter. Da må denne
	 * underordnede rapporten bli generert før den overordnede. I all hovedsak kunne dette vært
	 * ivaretatt med en enkel omvendt string-compare, slik at lange stringer kommer før korte stringer
	 * i omvendt alfabetisk rekkefølge:
	 *
	 * st/02/0229/0002
	 * st/02/0229/0001
	 * st/02/0229
	 * st/02
	 * st
	 *
	 * Imidlertid så er bydeler et problem, da de er et unntak fra URL-skjema, og de kommer da i feil rekkefølge
	 *
	 * st/03/0301/9
	 * st/03/0301/0901
	 *
	 * Regelen blir derfor at det lengste rapportnavnet kommer først, men ved lik lengde så brukes standard
	 * string-compare (trenger ikke å være omvendt lenger)
	 */
	public static class RapportNavnComparator implements Comparator<String>, Serializable {

		@Override
		public int compare(String s1, String s2) {
			return s2.length() - s1.length() == 0 ? s1.compareTo(s2) : s2.length() - s1.length();
		}
	}

    public MainBackend getMainBackend() {
        return mainBackend;
    }
}
