package no.valg.valgnatt.backend;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import no.valg.valgnatt.backend.eksport.Eksport;
import no.valg.valgnatt.backend.mandatberegning.MainMandatBeregning;
import no.valg.valgnatt.backend.parser.MainParser;
import no.valg.valgnatt.backend.parser.ParserService;
import no.valg.valgnatt.backend.rapport.MainRapport;
import no.valg.valgnatt.database.Configuration;
import no.valg.valgnatt.database.EvaAdminSkjema;
import no.valg.valgnatt.database.FlywayLoader;
import no.valg.valgnatt.database.GrunnlagsdataCache;
import no.valg.valgnatt.database.GrunnlagsdataService;
import no.valg.valgnatt.database.Konstanter;
import no.valg.valgnatt.database.domene.SkjemaType;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.domene.ValideringStatus;
import no.valg.valgnatt.database.forberedvalg.ForberedValg;
import no.valg.valgnatt.database.forberedvalg.util.SkjemaFunksjoner;
import no.valg.valgnatt.database.geografi.Fylke;
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 MainBackend implements Runnable {

	private static final Logger LOG = LogManager.getLogger(MainBackend.class);
	private static final int ETT_SEKDUND = 1000; // Milliseconds
	private static long instance = System.currentTimeMillis();
	private volatile KjøreTilstand ønsketKjøreTilstand;
	private volatile KjøreTilstand faktiskKjøreTilstand;
	private volatile Beregningsmetode ønsketBeregningsmetode;
	private volatile Beregningsmetode faktiskBeregningsmetode;
	private volatile boolean rekalkuler;
	private volatile String rekalkulerArg;
	private boolean oppdaterStemmeberettigede;
	@Resource
	private GrunnlagsdataService grunnlagsdataService;
	@Resource
	private GrunnlagsdataCache grunnlagsdataCache;
	@Resource
	private Configuration configuration;
	@Resource
	private MainParser mainParser;
	@Resource
	private ParserService parserService;
	@Resource
	private MainMandatBeregning mainMandatBeregning;
	@Resource
	private MainRapport mainRapport;
	@Resource
	private FlywayLoader flywayLoader;

	@Override
	public void run() {
		try {
			while (true) { // Loop for å håndtere NULLSTILL_ALT tilfellet
				initialiser();
				while (ønsketKjøreTilstand == KjøreTilstand.NORMAL || ønsketKjøreTilstand == KjøreTilstand.PAUSE) {
					long startTimeNs = System.nanoTime();
					kjørHovedLoop(startTimeNs);
				}
				mainRapport.stoppUtsendelseAvRapporter();
				if (ønsketKjøreTilstand == KjøreTilstand.NULLSTILL_ALT) {
					nullstillOgInitialiser();
					ønsketKjøreTilstand = KjøreTilstand.NORMAL;
				} else {
					break; // KjøreTilstand.STOPP
				}
			}
		} catch (Exception e) {
			LOG.fatal("Feil oppstod i MainBackend, systemet avslutter!!: " + e.getMessage(), e);
		}
	}

	private void nullstillOgInitialiser() throws IOException {
		grunnlagsdataService.nullstillDatabase();
		flywayLoader.initialize();
		grunnlagsdataCache.nullstill();
		grunnlagsdataCache.initialize();
		newInstance();
	}

	public static long getInstance() {
		return instance;
	}

	public static void newInstance() {
		instance = System.currentTimeMillis();
	}

	private void kjørHovedLoop(long startTimeNs) throws InterruptedException {
		try {
			if (oppdaterStemmeberettigede) {
				oppdaterStemmeberettigede();
			}
			if (ønsketKjøreTilstand != faktiskKjøreTilstand) {
				endreKjøreTilstand(ønsketKjøreTilstand);
			}
			if (faktiskKjøreTilstand == KjøreTilstand.NORMAL) {
				if (ønsketBeregningsmetode != faktiskBeregningsmetode) {
					// Dette er noe som trigges via "/valg_backend/setPrognosis=true" eller "/valg_backend/setActual=true"
					endreBeregningsmetode();
				} else if (rekalkuler) {
					// Rekalkulering trengs når man oppdaterer stemmeberettigede og evt. for intern testing (endrer kode og tester på nytt)
					rekalkuler();
				} else {
					// Dette er det vanlige tilfellet, hvor man leser inn ny stemmeskjema fra Admin (eller fra Simulatoren)
					lesInnNyeStemmeskjema();
					beregnMandater(faktiskBeregningsmetode == Beregningsmetode.OPPTALTESTEMMER);
					kalkulerRapporter();
				}
			}
			sov(startTimeNs);
		} catch (Exception e) {
			if (e.getClass() != InterruptedException.class) { // InterruptedException kommer hver gang vi stopper serveren (fra sov())
				LOG.error("Feil oppstod MainBackend hovedloop, men systemet avslutter ikke (ubehandlede stemmeskjema vil fortsatt bli forsøkt behandlet): " + e
						.getMessage(), e);
			}
			sov(startTimeNs);
		}
	}

	private void oppdaterStemmeberettigede() {
		long start = System.currentTimeMillis();
		grunnlagsdataService.oppdaterStemmeberettigedeForÅretsValg(grunnlagsdataCache);
		LOG.info("Oppdaterte GrunnlagsdataCache med endrede antall stemmeberettigede i stemmekretsene (" + (System.currentTimeMillis() - start) + " ms)");
		beOmRekalkulering(null);
		oppdaterStemmeberettigede = false;
	}

	void beOmRekalkulering(String arg) {
		if (arg != null) {
			rekalkulerArg = arg;
		}
		rekalkuler = true;
	}

	private void endreKjøreTilstand(KjøreTilstand ønsketKjøreTilstand) {
		LOG.info("MainBackend kjøretilstand endret fra " + faktiskKjøreTilstand + " til " + ønsketKjøreTilstand);
		if (ønsketKjøreTilstand == KjøreTilstand.NORMAL) {
			long start = System.currentTimeMillis();
			mainRapport.initialiser();
			LOG.info("Initialiserte mainRapport (" + (System.currentTimeMillis() - start) + " ms)");
		} else {
			mainRapport.stoppUtsendelseAvRapporter();
		}
		faktiskKjøreTilstand = ønsketKjøreTilstand;
	}

	private void endreBeregningsmetode() {
		Tilstand tilstand = grunnlagsdataService.getTilstand("prognose");
		LOG.info("MainBackend beregningstilstand endret fra " + faktiskBeregningsmetode + " til " + ønsketBeregningsmetode);
		boolean opptaltBeregning = ønsketBeregningsmetode == Beregningsmetode.OPPTALTESTEMMER;
		mainMandatBeregning.beregnMandaterForPrognoseRelaterteValgdistrikter(opptaltBeregning);
		rekalkulerRapporter(rapporterSomEndresVedEndringAvBeregningsmetode());
		tilstand.setVerdi(ønsketBeregningsmetode == Beregningsmetode.PROGNOSE ? "sann" : "usann");
		grunnlagsdataService.lagreTilstand(tilstand);
		faktiskBeregningsmetode = ønsketBeregningsmetode;
	}

	private String[] rapporterSomEndresVedEndringAvBeregningsmetode() {
		List<String> rapporterSomSkalRekalkuleres = new ArrayList<>();
		for (ValgType vt : grunnlagsdataCache.getValgTyper()) {
			if (vt == ValgType.FY || vt == ValgType.ST) {
				for (Fylke fylke : grunnlagsdataCache.getFylker(vt)) {
					rapporterSomSkalRekalkuleres.add(vt.name().toLowerCase() + "/" + fylke.getNr() + "$");
				}
				rapporterSomSkalRekalkuleres.add(vt.name().toLowerCase() + "$");
				rapporterSomSkalRekalkuleres.add(vt.name().toLowerCase() + "/ujm");
			} else if (vt == ValgType.KO) {
				for (String kommnr : Konstanter.getPrognosekommuner()) {
					rapporterSomSkalRekalkuleres.add("ko/" + kommnr.substring(0, 2) + "/" + kommnr + "$");
					rapporterSomSkalRekalkuleres.add("ko/" + kommnr.substring(0, 2) + "$");
					rapporterSomSkalRekalkuleres.add("ko$");
				}
			}
		}
		return rapporterSomSkalRekalkuleres.toArray(new String[]{});
	}

	private void rekalkulerRapporter(String... rekalkulerArg) {
		long start = System.currentTimeMillis();
		mainRapport.rekalkulerRapporter(rekalkulerArg);
		LOG.info("Rekalkulerte Rapporter (" + (System.currentTimeMillis() - start) + " ms)");
	}

	private void rekalkuler() {
		try {
			boolean opptaltBeregning = ønsketBeregningsmetode == Beregningsmetode.OPPTALTESTEMMER;
			mainMandatBeregning.beregnMandaterGrunnetRekalkulering(opptaltBeregning, rekalkulerArg);
			rekalkulerRapporter(rekalkulerArg);
		} finally {
			rekalkulerArg = null;
			rekalkuler = false;
		}
	}

	private void sov(long startTimeNs) throws InterruptedException {
		long tidBrukt = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNs);
		if (tidBrukt < ETT_SEKDUND) {
			Thread.sleep(ETT_SEKDUND - tidBrukt);
		}
	}

	void lesInnNyeStemmeskjema() {
		List<Stemmeskjema> rader = parserService.findByValideringStatus(ValideringStatus.I);
		if (!rader.isEmpty()) {
			long start = System.currentTimeMillis();
			mainParser.process(rader);
			LOG.info("Parsing utført (" + rader.size() + " stemmeskjema) (" + (System.currentTimeMillis() - start) + " ms)");
			start = System.currentTimeMillis();
			grunnlagsdataService.populerStemmetallPrParti(grunnlagsdataCache);
			grunnlagsdataService.populerOppgjørPrKrets(grunnlagsdataCache);
			LOG.info("Oppdaterte GrunnlagsdataCache med nyeste stemmer (" + (System.currentTimeMillis() - start) + " ms)");
			// valideringStatus ble endret fra I til M i mainParser.process(), slik at vi ikke får opp disse radene på nytt
		}
	}

	void beregnMandater(boolean opptaltBeregning) {
		List<Stemmeskjema> rader = parserService.findByValideringStatus(ValideringStatus.M);
		if (!rader.isEmpty()) {
			long start = System.currentTimeMillis();
			mainMandatBeregning.beregnMandaterForStemmeskjema(rader, opptaltBeregning);
			LOG.info("Mandatberegning utført (" + rader.size() + " stemmeskjema) (" + (System.currentTimeMillis() - start) + " ms)");
			parserService.updateValideringStatus(rader, ValideringStatus.P);
		}
	}

	void kalkulerRapporter() {
		List<Stemmeskjema> rader = parserService.findByValideringStatus(ValideringStatus.P);
		if (!rader.isEmpty()) {
			long start = System.currentTimeMillis();
			mainRapport.process(rader);
			LOG.info("Rapportproduksjon utført (" + rader.size() + " stemmeskjema) (" + (System.currentTimeMillis() - start) + " ms)");
			parserService.updateValideringStatus(rader, ValideringStatus.O);
		}
	}

	private void initialiser() {
		faktiskKjøreTilstand = configuration.isSuspendAtStartup() ? KjøreTilstand.PAUSE : KjøreTilstand.NORMAL;
		ønsketKjøreTilstand = faktiskKjøreTilstand;

		if (faktiskKjøreTilstand == KjøreTilstand.PAUSE) {
			LOG.info("MainBackend er i pausemodus ved oppstart!");
		} else {
			long start = System.currentTimeMillis();
			mainRapport.initialiser();
			LOG.info("Initialiserte mainRapport (" + (System.currentTimeMillis() - start) + " ms)");
		}

		faktiskBeregningsmetode = "sann".equals(grunnlagsdataService.getTilstand("prognose").getVerdi()) ? Beregningsmetode.PROGNOSE : Beregningsmetode.OPPTALTESTEMMER;
		ønsketBeregningsmetode = faktiskBeregningsmetode;
		rekalkuler = false;
	}

	void opphevMediaSperrefrist() {
		opphevSperrefrist("media");
	}

	private void opphevSperrefrist(String type) {
		Tilstand tilstand = grunnlagsdataService.getTilstand("sperrefrist-" + type);
		tilstand.setVerdi("usann");
		grunnlagsdataService.lagreTilstand(tilstand);
	}

	void opphevOffentligSperrefrist() {
		opphevSperrefrist("offentlig");
	}

	void innførMediaSperrefrist() {
		innførSperrefrist("media");
	}

	private void innførSperrefrist(String type) {
		Tilstand tilstand = grunnlagsdataService.getTilstand("sperrefrist-" + type);
		tilstand.setVerdi("sann");
		grunnlagsdataService.lagreTilstand(tilstand);
	}

	void innførOffentligSperrefrist() {
		innførSperrefrist("offentlig");
	}

	String nullstill() {
		if (configuration.erNullstillingTillatt()) {
			ønsketKjøreTilstand = MainBackend.KjøreTilstand.NULLSTILL_ALT;
			do {
				try {
					Thread.sleep(ETT_SEKDUND);
				} catch (InterruptedException e) {
					// ignore
				}
			} while (ønsketKjøreTilstand != KjøreTilstand.NORMAL);
			return "OK";
		} else {
			return "Avvist - ikke tillatt";
		}

	}

	void beOmOppdateringAvStemmeberettigede() {
		oppdaterStemmeberettigede = true;
	}

	void setØnsketKjøreTilstand(KjøreTilstand kjøreTilstand) {
		ønsketKjøreTilstand = kjøreTilstand;
	}

	void setØnsketBeregningsmetode(Beregningsmetode beregningsmetode) {
		ønsketBeregningsmetode = beregningsmetode;
	}

	int getTotalRefId() {
		return parserService.getTotalRefId();
	}

	int getPendingRefid() {
		return parserService.getPendingRefId();
	}

	void eksporter() throws IOException {
		Eksport.eksportValgdistrikterPartierOgKandidater(grunnlagsdataCache);
		Eksport.eksporterStemmekretser(grunnlagsdataCache);
	}

	boolean rekalkuleringPågår() {
		return rekalkuler || oppdaterStemmeberettigede;
	}

	void stoppUtsendelseAvRapporter() {
		mainRapport.stoppUtsendelseAvRapporter();
	}

	String rekonfigurer() throws IOException {
        if (configuration.erNullstillingTillatt()) {
            slettKatalog(Konstanter.AUTOOPPSETT_ROTKATALOG);
            kopierKonfigurasjonTilKatalog(Konstanter.AUTOOPPSETT_GRUNNLAGSDATAKATALOG);
			korrigerValgdistriktnrForOslo(Konstanter.AUTOOPPSETT_GRUNNLAGSDATAKATALOG);
            ForberedValg.main(new String[]{"runtimeconfig"});
		    return nullstill();
        } else {
            return "Avvist - Nullstilling er ikke tillatt!";
        }
	}

	private void slettKatalog(Path runtimeconfig) throws IOException {
	    if (Files.exists(runtimeconfig)) {
            Files.walkFileTree(runtimeconfig, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        }
    }

    private void kopierKonfigurasjonTilKatalog(Path destination) throws IOException {
        Files.createDirectories(destination);
        File wildflyDataDir = Konstanter.WILDFLY_DATAKATALOG.toFile();
        String[] skjematyper = {SkjemaType.GRUNNLAG_STEMMEKRETSER.kode, SkjemaType.GRUNNLAG_VALGDISTRIKTER_PARTIER_KANDIDATER.kode};
        for (String skjematype : skjematyper) {
            for (ValgType valgType : grunnlagsdataCache.getValgTyper()) {
                File[] files = wildflyDataDir.listFiles((dir, name) -> name.startsWith(skjematype + "-" + valgType.name() + "-" + grunnlagsdataCache.getValgAar()));
                File latest = getLatestFile(files);
                if (latest != null) {
                    Files.copy(latest.toPath(), destination.resolve(latest.toPath().getFileName()), StandardCopyOption.REPLACE_EXISTING);
                } else {
                    LOG.warn("Fant ingen '" + skjematype + "-" + valgType.name() + "-" + grunnlagsdataCache.getValgAar() + "'-filer i " + wildflyDataDir);
                }
            }
        }
    }

    private File getLatestFile(File[] files) {
        File latest = null;
        if (files != null) {
            for (File file : files) {
                if (latest == null || latest.lastModified() < file.lastModified()) {
                    latest = file;
                }
            }
        } 
        return latest;
    }

	private void korrigerValgdistriktnrForOslo(Path runtimeconfigGrunnlagsdataPath) throws IOException {
		File grunnlagsdataDir = runtimeconfigGrunnlagsdataPath.toFile();
		File[] filer = grunnlagsdataDir.listFiles();
		if (filer != null) {
			for (File fil : filer) {
                if (fil.getName().startsWith(SkjemaType.GRUNNLAG_STEMMEKRETSER.kode + "-" + ValgType.ST) || fil.getName().startsWith(SkjemaType.GRUNNLAG_STEMMEKRETSER.kode + "-" + ValgType.FY)) {
                    EvaAdminSkjema korrigertFil = SkjemaFunksjoner.korrigerValgdistriktnrForOsloGsk(new EvaAdminSkjema(fil));
                    korrigertFil.skrivTilFil(runtimeconfigGrunnlagsdataPath.toString());
                } else if (fil.getName().startsWith(SkjemaType.GRUNNLAG_VALGDISTRIKTER_PARTIER_KANDIDATER.kode + "-" + ValgType.ST) || fil.getName().startsWith(SkjemaType.GRUNNLAG_VALGDISTRIKTER_PARTIER_KANDIDATER.kode + "-" + ValgType.FY)) {
                    EvaAdminSkjema korrigertFil = SkjemaFunksjoner.korrigerValgdistriktnrForOsloGvpk(new EvaAdminSkjema(fil));
                    korrigertFil.skrivTilFil(runtimeconfigGrunnlagsdataPath.toString());
                }
            }
		}
	}

    public enum KjøreTilstand {
		NORMAL, PAUSE, STOPP, NULLSTILL_ALT
	}

	public enum Beregningsmetode {
		OPPTALTESTEMMER, PROGNOSE
	}

	public Beregningsmetode getFaktiskBeregningsmetode() {
		return faktiskBeregningsmetode;
	}
}
