package no.valg.valgnatt.frontend;

import java.io.IOException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Cacher alle rapporter for alle valgår. Klassen er laget for å støtte følgende behov:
 *
 * - RapportCache rapporter basert på id/navn
 * - Enkelt kunne lagre ett enkelt valg i ett enkelt valgår fra minne til fil
 * - Enkelt kunne hente opp ett enkelt valg i ett enkelt valgår fra fil til minne
 * - Tilby søk i ett enkelt valg i ett enkelt valgår. Søket baserer seg på rapportnavn
 */
class RapportCache {

	private static final Logger LOG = LogManager.getLogger(RapportCache.class);
	private static final double MILLION = 1000000d;


	// Datastrukturene er like i cachene bortsett fra at nøkkel i det innerste map'et er forskjellige
	// idCache  : 2017 -> (st -> ( "/2017/st" -> landsrapport))
	// idCache  : 2017 -> (st -> ( "/2017/st/02" -> akershusrapport))
	// idCache  : 2017 -> (totalrapport -> ( "/2017/totalrapport/st/stemmer/fylke" -> totalrapport for fylkene))
	// kortnavnCache: 2017 -> (st -> ( "/2017/st/akershus" -> akershusrapport))
	// hrefnavnCache: 2017 -> (st -> ( "/2017/st/%C3%B8stfold" -> østfoldrapport))
	private Map<Integer, Map<String, Map<String, String>>> idCache = new ConcurrentHashMap<>();
	private Map<Integer, Map<String, Map<String, String>>> kortnavnCache = new ConcurrentHashMap<>();
	private Map<Integer, Map<String, Map<String, String>>> hrefnavnCache = new ConcurrentHashMap<>();

	void addRapportSamlingTilCache(RapportSamling rapportSamling) throws IOException {
		for (String rapportId : rapportSamling.hentRapportSamlingMap().keySet()) {
			String rapport = rapportSamling.hentRapportSamlingMap().get(rapportId);
            int valgår = Integer.parseInt(rapportId.split("/")[1]);
            String cahceNøkkel = rapportId.split("/")[2];
			addRapportTilCache(idCache, valgår, cahceNøkkel, rapportId, rapport);
            if (!rapportId.endsWith("/ujm") && !rapportId.contains("totalrapport")) {
				addRapportTilCache(kortnavnCache, valgår, cahceNøkkel, finnVerdi(rapport, "self", "navn").toLowerCase(), rapport);
				addRapportTilCache(hrefnavnCache, valgår, cahceNøkkel, URLDecoder.decode(finnVerdi(rapport, "self", "hrefNavn"), "UTF-8").toLowerCase(), rapport);
			}
		}
	}

    private void addRapportTilCache(Map<Integer, Map<String, Map<String, String>>> cache, int valgår, String cacheNøkkel, String rapportNøkkel, String rapport) {
		Map<String, Map<String, String>> valgårMap = cache.computeIfAbsent(valgår, k -> new ConcurrentHashMap<>());
		Map<String, String> valgtypeMap = valgårMap.computeIfAbsent(cacheNøkkel, k -> new ConcurrentHashMap<>());
		valgtypeMap.put(rapportNøkkel, rapport);
	}

	/**
	 * En enkel "parser" som finner noen verdier i json-dokument, basert på at hovednøkkel er
	 * et objekt, og at man deretter lokaliserer undernøkkel (feltnavn) i dette objektet og til
	 * slutt finner verdien for denne "undernøkkelen".
	 */
	private String finnVerdi(String json, String hovednøkkel, String undernøkkel) {
		int opPosStart = json.indexOf(undernøkkel, json.indexOf("\"" + hovednøkkel + "\"")) + undernøkkel.length() + 3;
		return json.substring(opPosStart, json.indexOf(",", opPosStart)).replaceAll("\"", "").trim();
	}

	String getRapportFraCache(String href) {
		String rapport = getRapportFraCache(idCache, href);
		if (rapport == null) {
			return getRapportFraCache(hrefnavnCache, href);
		} else {
			return rapport;
		}
	}

	private String getRapportFraCache(Map<Integer, Map<String, Map<String, String>>> cache, String rapportNøkkel) {
        int valgår = Integer.parseInt(rapportNøkkel.split("/")[1]);
        String cahceNøkkel = rapportNøkkel.split("/")[2];
        if (cache.get(valgår) != null && cache.get(valgår).get(cahceNøkkel) != null) {
            return cache.get(valgår).get(cahceNøkkel).get(rapportNøkkel);
        } else {
            return null;
        }
	}

	String søkEtterRapporterFraNavnCache(int valgår, String valgtype, String orgSøkeStr) {

		long start = System.nanoTime();
		String modSøkeStr = modifiserSøkeStreng(orgSøkeStr);

		try {
			Map<String, String> rapportPrNavn = kortnavnCache.get(valgår).get(valgtype);
			StringBuilder sb = new StringBuilder();
			sb.append("[");
			for (String navn : rapportPrNavn.keySet()) {
				if (navn.matches(modSøkeStr) && !"ujm".equals(navn)) {
					String rapport = rapportPrNavn.get(navn);
					sb.append("\n{");
					sb.append(rapport.substring(rapport.indexOf("\"_links")));
					sb.delete(sb.lastIndexOf("}"), sb.length());
					sb.append("},");
				}
			}
			if (sb.charAt(sb.length() - 1) == ',') {
				sb.delete(sb.length() - 1, sb.length());
			}
			sb.append("]");
			return sb.toString();
		} catch (Exception e) {
			LOG.warn("Søkestrengen " + orgSøkeStr + " førte til en uventet feil:", e);
			return null;
		} finally {
			double ms = (System.nanoTime() - start) / MILLION;
			LOG.info(String.format("Søket tok %.2f ms (orgSøkeStr: %s, modSøkeStr: %s)", ms, orgSøkeStr, modSøkeStr));
		}
	}

	/**
	 * Søket må modifiseres for å gjøre det regexp-kompatibelt og for å unngå
	 * "onde" søk som kan ta ekstremt lang tid å fullføre
	 */

	private String modifiserSøkeStreng(String søkeStr) {
		String modSøkeStr = søkeStr;
		// Søket tillater at man skriver * istedet for .* - men man trenger da litt
		// kode for å fikset dette så det blir helt korrekt
		modSøkeStr = modSøkeStr.replaceAll("\\*", ".*");
		modSøkeStr = modSøkeStr.replaceAll("\\.\\.\\*", ".*");
		// Dersom søket ikke har definert avslutning ($), så legger vi på det
		if (!modSøkeStr.endsWith("$")) {
			modSøkeStr += ".*$";
		}
		// Fjerner regexp-karakterer som kan brukes i ReDOS - det er ikke så farlig om søket ikke lenger gir mening
		// folk som skriver reqexp med parenteser og backslash tilhører uansett en ørliten minoritet som kan håndtere dette
		modSøkeStr = modSøkeStr.replace("(", "");
		modSøkeStr = modSøkeStr.replace(")", "");
		modSøkeStr = modSøkeStr.replace("\\", "");
		modSøkeStr = modSøkeStr.toLowerCase();
		if (!modSøkeStr.matches(".*[a-z,æøå]+.*")) {
			// Ingen bokstaver i søket tillates ikke, da endrer vi til "xyz" som ikke vil gi noe treff
			modSøkeStr = "xyz";
		}
		return modSøkeStr;
	}

	Map<String, Map<String, String>> getRapporterFraIdCache(int valgår) {
		return new TreeMap<>(idCache.get(valgår));
	}

	Set<Integer> getValgårSet() {
		return new TreeSet<>(idCache.keySet());
	}
	
	String getValgårString() {
		StringBuilder valgår = new StringBuilder();
		for (Integer integer : idCache.keySet()) {
			valgår.append(integer.toString());
		}
		return valgår.toString();
	}

}
