package no.valg.valgnatt.backend.parser;

import no.valg.valgnatt.common.ValgnattException;
import no.valg.valgnatt.database.GrunnlagsdataCache;
import no.valg.valgnatt.database.dao.StemmeoppgjorDao;
import no.valg.valgnatt.database.dao.StemmeskjemaDao;
import no.valg.valgnatt.database.domene.ValgType;
import no.valg.valgnatt.database.domene.ValideringStatus;
import no.valg.valgnatt.database.hibernate.Partiliste;
import no.valg.valgnatt.database.hibernate.Stemmeoppgjor;
import no.valg.valgnatt.database.hibernate.StemmeoppgjorId;
import no.valg.valgnatt.database.hibernate.Stemmeskjema;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

@Transactional
@Service
public class ParserService {

	@Resource
	private StemmeoppgjorDao stemmeoppgjorDao;
	@Resource
	private StemmeskjemaDao stemmeskjemaDao;
	@Resource
	private DataSource dataSource;
	@Resource
	private GrunnlagsdataCache grunnlagsdataCache;


	public List<Stemmeskjema> findByValideringStatus(final ValideringStatus valideringStatus) {
		return stemmeskjemaDao.findByValideringStatus(valideringStatus);
	}

	public void updateValideringStatus(List<Stemmeskjema> rader, final ValideringStatus valideringStatus) {
		for (Stemmeskjema rad : rader) {
			rad.setValideringstatus(valideringStatus);
			oppdaterStemmeskjema(rad);
		}
	}

	public void oppdaterStemmeskjema(Stemmeskjema stemmeskjema) {
		stemmeskjemaDao.update(stemmeskjema);
	}

	public int getTotalRefId() {
		return stemmeskjemaDao.getTotalRefId();
	}

	public int getPendingRefId() {
		return stemmeskjemaDao.getPendingRefId();
	}

	public void lagreStemmeskjema(Stemmeskjema stemmeskjema) {
		stemmeskjemaDao.save(stemmeskjema);
	}

	public void lagreSkjema(Stemmeresultater stemmeresultater, Stemmeoppgjor stemmeoppgjør, Stemmeskjema stemmeskjema) {
		lagreStemmer(stemmeskjema, stemmeresultater);
		if (stemmeoppgjør != null) {
			stemmeoppgjør.setId(new StemmeoppgjorId(stemmeskjema.getValgType(), stemmeskjema.getKommnr(), stemmeskjema.getKretsnr()));
			stemmeoppgjorDao.saveOrUpdate(stemmeoppgjør);
		}
		// MSI mar 2015: Siden Oslo ikke leverer fylkesresultater i fylkesvalg (men fylkes- og landsrapportene trenger et fullstendig
		// stemmeantall for alle fylker), så må vi kopiere stemmetall fra kommuneresultatene og lage en dummy-fylkesrapport.
		// Vi gjør dette ved å kopiere kommuneresultatene og lagre dette i stemmetall.
		// Se forøvrig MainRapport.selectNeededReports() for videre håndtering av dette unntakstilfellet.
		if (stemmeskjema.erKommunestyrevalgForOslo()) {
			Stemmeskjema stemmeskjemaKopi = new Stemmeskjema();
			stemmeskjemaKopi.setValgType(ValgType.FY); // hardkodes til FY -
			stemmeskjemaKopi.setId(stemmeskjema.getId());
			stemmeskjemaKopi.setKommnr(stemmeskjema.getKommnr());
			stemmeskjemaKopi.setKretsnr(stemmeskjema.getKretsnr());
			stemmeskjemaKopi.setOpptaltstatus(stemmeskjema.getOpptaltstatus());
			stemmeskjemaKopi.setOriginalTid(stemmeskjema.getOriginalTid());
			// ikke nødvendig å kopiere mer for å gjennomføre lagring av melding - resten ligger i stemmeresultater-objektet
			lagreStemmer(stemmeskjemaKopi, stemmeresultater);
			if (stemmeoppgjør != null) {
				Stemmeoppgjor stemmeoppgjørKopi = stemmeoppgjør.toBuilder().build();
				stemmeoppgjørKopi.setId(new StemmeoppgjorId(stemmeskjemaKopi.getValgType(), stemmeskjemaKopi.getKommnr(), stemmeskjemaKopi.getKretsnr()));
				stemmeoppgjorDao.saveOrUpdate(stemmeoppgjørKopi);
			}
		}
		oppdaterStemmeskjema(stemmeskjema);
	}

	/**
	 * MSI nov 2015: Jeg kunne valgt å bruke
	 * Hibernate til å persistere, men ønsket meg mer kontroll til å kunne gjøre
	 * en UPDATE først, deretter en INSERT om nødvendig. Tanken er at systemet i hovedsak
	 * vil oppdatere stemmetall for en gitt krets/parti, men at det må legge inn stemmetall
	 * første gang. Dette skal imidlertid skje etter at vi har fått tilbake en melding
	 * om at 0 rader ble oppdatert.
	 * <p/>
	 * Metoden har blitt optimalisert for å kjøre batchvis oppdatering/insert, fordi vi
	 * veldig gjerne ønsker å kutte tid som blir brukt i å lese inn stemmeskjema - det vil
	 * spare en god del tid i uendelig mange testkjøringer.
	 *
	 * TODO: Når vi får installert Postgres 9.5 på Jenkins, så kan vi ta i bruk SQL-syntax som
	 * brukes i MandatBeregningLoggService.logg(). Tiltaket kan gi litt høyere ytelse og kutte ned
	 * 30 kodelinjer.
	 */
	private void lagreStemmer(Stemmeskjema stemmeskjema, Stemmeresultater sr) {
		Connection con = DataSourceUtils.getConnection(dataSource);
		try {
			List<String> partikoderIkkeOppdatert = oppdaterStemmetall(stemmeskjema, sr, con);
			if (!partikoderIkkeOppdatert.isEmpty()) {
				leggTilStemmetall(stemmeskjema, sr, con, partikoderIkkeOppdatert);
			}
		} catch (SQLException sqle) {
			throw new ValgnattException(sqle);
		} finally {
			DataSourceUtils.releaseConnection(con, dataSource);
		}
	}

	private List<String> oppdaterStemmetall(Stemmeskjema stemmeskjema, Stemmeresultater sr, Connection con) throws SQLException {
		String sql = "UPDATE stemmetall " +
				"SET ref_id = ?, fhs = ?, vts = ?, listestemmer = ?, opptaltstatus = ?, endret_tid = ? " +
				"WHERE valgtype = ? AND kommnr = ? AND kretsnr = ? AND partikode = ?";
		try (PreparedStatement p = con.prepareStatement(sql)) {
			List<String> partikoder = sr.getPartikoder();
			for (String partikode : partikoder) {
				p.setInt(1, stemmeskjema.getId());
				p.setInt(2, sr.getFhs(partikode));
				p.setInt(3, sr.getVts(partikode));
				p.setInt(4, sr.getListestemmer(partikode));
				p.setInt(5, stemmeskjema.getOpptaltstatus());
				p.setTimestamp(6, new Timestamp(stemmeskjema.getOriginalTid().getTime()));
				p.setObject(7, stemmeskjema.getValgType().name());
				p.setString(8, stemmeskjema.getKommnr());
				p.setString(9, stemmeskjema.getKretsnr());
				p.setString(10, partikode);
				p.addBatch();
			}
			int[] updateResults = p.executeBatch();
			List<String> partikoderIkkeOppdatert = new ArrayList<>();
			for (int i = 0; i < updateResults.length; i++) {
				if (updateResults[i] == 0) { // 0 rader oppdatert
					partikoderIkkeOppdatert.add(partikoder.get(i));
				} else if (updateResults[i] == PreparedStatement.EXECUTE_FAILED) {
					throw new SQLException("Oppdaterting av stemmetall for partikode " + partikoder.get(i) + " feilet for stemmeskjema " + stemmeskjema.getId());
				}
			}
			return partikoderIkkeOppdatert;
		}
	}

	private void leggTilStemmetall(Stemmeskjema stemmeskjema, Stemmeresultater sr, Connection con, List<String> partikoderIkkeOppdatert) throws SQLException {
		String sql;

		sql = "INSERT INTO stemmetall VALUES(?,?,?,?,?,?,?,?,?,?,?,?)";
		try (PreparedStatement p = con.prepareStatement(sql)) {
			for (String partikode : partikoderIkkeOppdatert) {
				p.setInt(1, stemmeskjema.getId());
				p.setObject(2, stemmeskjema.getValgType().name());
				p.setString(3, stemmeskjema.getKommnr());
				p.setString(4, stemmeskjema.getKretsnr());
				p.setString(5, partikode);
				Partiliste parti = grunnlagsdataCache.getPartiliste(stemmeskjema.getValgType(), partikode);
				if (parti == null) {
					throw new ValgnattException("Partikode " + partikode + " i stemmeskjema " + stemmeskjema.getId() + " (kommnr: " + stemmeskjema.getKommnr() + ") finnes ikke i grunnlagsdata, dermed kan ikke stemmetallene lagres.");
				}
				p.setInt(6, parti.getPartikategori());
				p.setInt(7, parti.getVisningssekvens());
				p.setInt(8, sr.getFhs(partikode));
				p.setInt(9, sr.getVts(partikode));
				p.setInt(10, sr.getListestemmer(partikode));
				p.setInt(11, stemmeskjema.getOpptaltstatus());
				// Vi bruker originalTid heller enn endretTid, slik at vi får det reelle tms fra stemmeskjemaet - se XMLparser.hentOriginalTid()
				p.setTimestamp(12, new Timestamp(stemmeskjema.getOriginalTid().getTime()));
				p.addBatch();
			}
			int[] insertResults = p.executeBatch();
			for (int i = 0; i < insertResults.length; i++) {
				if (insertResults[i] == PreparedStatement.EXECUTE_FAILED) {
					throw new SQLException("Insert av stemmetall for partikode " + partikoderIkkeOppdatert.get(i) + " feilet for stemmeskjema " + stemmeskjema.getId());
				}
			}
		}
	}
}
