
package de.ivu.elect.was.exchange.ergebnis.eml;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.zip.ZipOutputStream;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.EmlExportMapperHelper;
import de.ivu.elect.NlExportFileNames;
import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.datei.entity.Datei;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisExportContext;
import de.ivu.elect.business.ergebnis.control.ErgebnisExportObject;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.exchange.control.FileKonfiguration;
import de.ivu.elect.business.exchange.control.MappingException;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.NlPolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppeEigenschaften;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.kandidatur.entity.NlListenkandidatur;
import de.ivu.elect.business.security.boundary.EmlSignKeyService;
import de.ivu.elect.business.stimmeingabe.boundary.NlVoteType;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.NlWahlart;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.nl.eml.EmlWrapper;
import de.ivu.elect.nl.eml.readable.AbstractVotesSectionWrapper;
import de.ivu.elect.nl.eml.readable.Eml510ContestWrapper;
import de.ivu.elect.nl.eml.readable.Eml510Wrapper;
import de.ivu.elect.nl.eml.readable.GroupVotesSection;
import de.ivu.elect.nl.eml.readable.IdShortCode;
import de.ivu.elect.nl.eml.readable.ReportingUnitVotesWrapper;
import de.ivu.elect.nl.eml.readable.TotalVotesSectionWrapper;
import de.ivu.elect.nl.eml.readable.VoteType;
import de.ivu.elect.util.FileUtil;

public abstract class Abstract510ErgebnisExportMapper extends AbstractEml5xxErgebnisExportMapper {

  private static final Logger LOG = LoggerFactory.getLogger(Abstract510ErgebnisExportMapper.class);

  private static final long serialVersionUID = 1L;

  @Inject
  private transient SystemGruppeEigenschaften systemGruppeEigenschaften;

  @Inject
  private transient EmlExportMapperHelper emlExportMapperHelper;

  @Inject
  private transient EmlSignKeyService emlSignKeyService;

  @Inject
  private transient CurrentCustomer currentCustomer;

  protected Abstract510ErgebnisExportMapper() {
    super();
  }

  @Override
  public void serialize(ErgebnisExportObject ergebnisExportObject, OutputStream os, ErgebnisExportContext context, FileKonfiguration fileKonfiguration) throws MappingException {
    var eml = createEml(ergebnisExportObject, context);

    try (var zos = new ZipOutputStream(os)) {
      var wahldurchfuehrung = ergebnisExportObject.getErgebnisDaten().getWahl();

      emlExportMapperHelper.writeEml(eml, zos, createEmlXmlFileName(ergebnisExportObject, context), bestimmeGebietFuerDaten(ergebnisExportObject, context),
          context,
          retrievePrivateKey(wahldurchfuehrung, context, emlSignKeyService), retrievePublicKey(wahldurchfuehrung, context, emlSignKeyService),
          retrieveNlX509Metadata(wahldurchfuehrung, context, emlSignKeyService));

      zos.finish();
    } catch (IOException e) {
      throw new MappingException(e.getMessage(), e);
    }
  }

  @Nonnull
  public EmlWrapper createEml(ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context) {
    var wahl = context.getWahl();

    var eml = EmlWrapper.createEml(getEmlId(), context.getNow());
    eml.getContent().setIssueDate(null); // ELECTVAPP-491

    var eml510Wrapper = new Eml510Wrapper(eml.getContent());

    var ergebnisDaten = ergebnisExportObject.getErgebnisDaten();

    mappeWahldaten(ergebnisExportObject, eml510Wrapper, ergebnisDaten.getWurzelgebiet(), () -> {
      List list = ergebnisDaten.getGebiete();

      return list;
    }); // ELECTVAPP-399

    fillManagingAuthority(eml510Wrapper.getManagingAuthority(), ergebnisExportObject, context);

    var eml510ContestWrapper = eml510Wrapper.addContest();

    fillContest(ergebnisExportObject, context, wahl, eml510ContestWrapper);

    fuelleReportingUnitsTotalVotes(ergebnisExportObject, context, eml510ContestWrapper);
    return eml;
  }

  protected void fuelleReportingUnitsTotalVotes(ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context, Eml510ContestWrapper eml510ContestWrapper) {
    TotalVotesSectionWrapper totalVotesSectionWrapper = null;
    var totalVotesGebiet = bestimmeTotalVotesGebiet(ergebnisExportObject, context);

    if (!Objects.isNull(totalVotesGebiet)) {
      totalVotesSectionWrapper = eml510ContestWrapper.addTotalVotes().getVotesWrapper();
    }

    Map<Wahldurchfuehrungsgebiet, EmlVotes> reportingUnitVotes = new HashMap<>();

    for (var reportingUnitGebiet : bestimmeReportingUnitGebiete(ergebnisExportObject, context)) {
      var reportingUnitVotesWrapper = eml510ContestWrapper.addReportingUnitVotes(
          isEml510ReportingUnitsAusgeben(ergebnisExportObject.getErgebnisDaten().getWahl()));

      fillReportingUnit(reportingUnitGebiet, reportingUnitVotesWrapper);

      reportingUnitVotes.put(reportingUnitGebiet, fillVotes(reportingUnitGebiet, reportingUnitVotesWrapper.getVotesWrapper(), ergebnisExportObject, context));
    }

    if (totalVotesSectionWrapper != null) {
      if (totalVotesGebiet.isErfassungsgebiet()) {
        fillVotes(totalVotesGebiet, totalVotesSectionWrapper, ergebnisExportObject, context);
      } else {
        // Zusammenzählen aller ReportingUnits, wenn TotalVotesGebiet selbst kein Erfassungsgebiet
        fillTotalVotes(reportingUnitVotes, totalVotesSectionWrapper, context.getWahl(), totalVotesGebiet);
      }
    }
  }

  protected boolean isEml510ReportingUnitsAusgeben(Wahl wahl) {
    return true; // ELECTVAPP-1704
  }

  protected void fillReportingUnit(Wahldurchfuehrungsgebiet reportingUnitGebiet, ReportingUnitVotesWrapper reportingUnitVotesWrapper) {
    reportingUnitVotesWrapper.setId(reportingUnitGebiet.getNummer());
    reportingUnitVotesWrapper.setName(reportingUnitGebiet.getName());
  }

  private void fillTotalVotes(Map<Wahldurchfuehrungsgebiet, EmlVotes> reportingUnitVotes, TotalVotesSectionWrapper votesWrapper, Wahl wahl,
                              Wahldurchfuehrungsgebiet totalVotesGebiet) {
    var totalEmlVotes = new EmlVotes();

    for (var value : reportingUnitVotes.values()) {
      totalEmlVotes.add(value);
    }

    // ELECTVAPP-2179
    var validVotes = totalEmlVotes.getValidVotes();
    var wahlberechtigteGesamt = validVotes.get(VoteType.WAHLBERECHTIGTE);
    if (wahlberechtigteGesamt == null || wahlberechtigteGesamt.longValue() < 1) {
      var anzahlWahlberechtigteOhneWahlschein = totalVotesGebiet.getAnzahlWahlberechtigteOhneWahlschein();
      if (anzahlWahlberechtigteOhneWahlschein != null) {
        validVotes.put(VoteType.WAHLBERECHTIGTE, BigInteger.valueOf(anzahlWahlberechtigteOhneWahlschein));
      }
    }

    votesWrapper.setValidVotes(validVotes);
    votesWrapper.setCandidateVotesPerGroup(totalEmlVotes.getCandidateVotes(), !isUseShortCodeForCandidateId(wahl), isUseShortCodeForCandidateId(wahl));
  }

  protected abstract List<Wahldurchfuehrungsgebiet> bestimmeReportingUnitGebiete(ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context);

  protected EmlVotes fillVotes(Wahldurchfuehrungsgebiet gebiet, AbstractVotesSectionWrapper<?> wrapper, ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context) {
    var ergebnisDaten = ergebnisExportObject.getErgebnisDaten();

    var stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet = ergebnisDaten.getStimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet(
        gebiet);
    Map<Gruppe, Liste> listenNachGruppe = bestimmeListeDerGruppe(stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet);
    var positionenDerGruppe = getPositionenDerGruppen(stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet);

    var emlVotes = new EmlVotes();

    fillVotes(emlVotes, gebiet, ergebnisDaten, listenNachGruppe, positionenDerGruppe);

    wrapper.setCandidateVotesPerGroup(emlVotes.getCandidateVotes(), !isUseShortCodeForCandidateId(context.getWahl()) /* ELECTVAPP-1348 */,
        isUseShortCodeForCandidateId(context.getWahl()));
    wrapper.setValidVotes(emlVotes.getValidVotes());

    return emlVotes;
  }

  protected void fillVotes(EmlVotes emlVotes, Wahldurchfuehrungsgebiet gebiet, ErgebnisDaten ergebnisDaten, Map<Gruppe, Liste> listenNachGruppe, Map<Gruppe, Long> positionenDerGruppe) {
    List<Stimmen> direktListenSystemStimmenAufGebiet = new ArrayList<>(ergebnisDaten.getStimmen().getDirektListenSystemStimmenAufGebiet(gebiet));
    Map<Gruppe, GroupVotesSection> gruppenMap = new HashMap<>();
    var entityAufStimmzettelDaten = ergebnisDaten.getEntityAufStimmzettelDaten();

    for (var stimmen : direktListenSystemStimmenAufGebiet) {
      if (stimmen.getGruppe().isSystemGruppe()) {
        var voteType = bestimmeVoteType(stimmen);

        if (voteType == null) {
          LOG.warn("Kein VoteType gefunden: " + stimmen);
        } else {
          emlVotes.put(voteType, toBigInteger(stimmen.getAnzahl()));
        }
      } else {
        var gruppe = stimmen.getGruppe();
        var groupVotesSection = gruppenMap.computeIfAbsent(gruppe,
            x -> new GroupVotesSection(getGruppenId(gruppe, positionenDerGruppe), gruppe.getName(), toBigInteger(stimmen.getAnzahl()),
                new NlPolitischeGruppe(gruppe).isBlancoListe()/*ELECTVAPP-1745*/));
        var gruppenStimmen = emlVotes.getGroupVotes(groupVotesSection);

        var liste = listenNachGruppe.get(gruppe);

        if (liste != null) {
          for (var listenkandidatur : liste.getListenkandidaturenAufStimmzettelSortiertNachPosition(entityAufStimmzettelDaten)) {
            gruppenStimmen.put(getKandidatId(listenkandidatur, entityAufStimmzettelDaten), getKandidaturStimmen(listenkandidatur, gebiet, ergebnisDaten));
          }
        }
      }
    }
  }

  protected Map<Gruppe, Long> getPositionenDerGruppen(Map<Wahlvorschlag, Long> stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet) {
    return stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet.entrySet().stream()
        .collect(Collectors.toMap(entry -> entry.getKey().getGruppe(), Map.Entry::getValue, (a, b) -> b));
  }

  protected HashMap<Gruppe, Liste> bestimmeListeDerGruppe(Map<Wahlvorschlag, Long> stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet) {
    return stimmzettelPositionenZugelasseneWahlvorschlaegeAufGebiet.keySet().stream().filter(AbstractWahlvorschlag::isListe)
        .collect(Collectors.toMap(AbstractWahlvorschlag::getGruppe, wahlvorschlag -> wahlvorschlag.castTo(Liste.class).getEntity(), (a, b) -> b, HashMap::new));
  }

  @Nullable
  private VoteType bestimmeVoteType(Stimmen stimmen) {
    return Arrays.stream(NlVoteType.values()).filter(value -> stimmen.getStimmenart().equals(value.getStimmenart()))
        .filter(value -> value.getSystemGruppe().equalsGruppe(stimmen.getGruppe(), systemGruppeEigenschaften)).findFirst()
        .map(value -> VoteType.valueOf(value.name())).orElse(null);
  }

  private BigInteger getKandidaturStimmen(Listenkandidatur listenkandidatur, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, ErgebnisDaten ergebnisDaten) {
    var stimmenFuerListenkandidatur = ergebnisDaten.getStimmenFuerListenkandidatur(listenkandidatur, wahldurchfuehrungsgebiet);

    if (stimmenFuerListenkandidatur == null) {
      return BigInteger.ZERO;
    }

    return BigInteger.valueOf(stimmenFuerListenkandidatur);
  }

  protected IdShortCode getKandidatId(Listenkandidatur listenkandidatur, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    var stimmzettelPosition = listenkandidatur.getStimmzettelPosition(entityAufStimmzettelDaten);
    var position = stimmzettelPosition.getPosition();
    var shortCode = new NlListenkandidatur(listenkandidatur).getShortCode(Collections.emptyList()/* im WAS egal */, Collections.emptyMap()/* im WAS egal */,
        getCustomer());
    return new IdShortCode(position, shortCode);
  }

  protected boolean isUseShortCodeForCandidateId(Wahl wahl) {
    return false;
  }

  private String getGruppenId(Gruppe gruppe, Map<Gruppe, Long> positionenDerGruppe) {
    var position = positionenDerGruppe.get(gruppe);

    if (position == null) {
      return "";
    }

    return Long.toString(position);
  }

  private BigInteger toBigInteger(Long anzahl) {
    if (anzahl == null) {
      return BigInteger.ZERO;
    }

    return BigInteger.valueOf(anzahl);
  }

  @Nullable
  protected abstract Wahldurchfuehrungsgebiet bestimmeTotalVotesGebiet(ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context);

  protected abstract String getEmlId();

  @Override // ELECTVAPP-991, ELECTVAPP-992
  protected String createSimpleFileName(ErgebnisExportObject ergebnisExportObject, ErgebnisExportContext context) {
    var wahl = ergebnisExportObject.getErgebnisDaten().getWahl();

    var simpleFileName = "";

    simpleFileName += getMessage(exportsTotaaltelling() ? "eml.total.telling.prefix" : "eml.telling.prefix");
    simpleFileName += "_";
    simpleFileName += wahl.getKennzeichen();

    if (isEml510GebietAnhaengen(ergebnisExportObject)) {

      var regionName = bestimmeGebietFuerDaten(ergebnisExportObject, context);
      var oberstesGebiet = bestimmeOberstesGebiet(ergebnisExportObject, context);

      if (regionName == null) {
        regionName = oberstesGebiet;
      }

      if (isWahlebeneIrrelevant(wahl)) {

        if (NlWahlart.valueOf(ergebnisExportObject.getErgebnisDaten().getWahl()).isEml510GebietAnhaengen()) {
          simpleFileName += "_";
          simpleFileName += oberstesGebiet.getName();
        }

        var gebietFuerDaten = regionName;

        if (!gebietFuerDaten.equals(oberstesGebiet)) {
          simpleFileName += "_";
          simpleFileName += gebietFuerDaten.getName();
        }
      } else {
        simpleFileName += "_";
        simpleFileName += getRegionType(regionName);
        simpleFileName += "_";
        simpleFileName += regionName.getName();
      }
    }
    return FileUtil.normalizeFileNamePreserveCase(simpleFileName);
  }

  protected boolean isEml510GebietAnhaengen(ErgebnisExportObject ergebnisExportObject) {
    return NlWahlart.valueOf(ergebnisExportObject.getErgebnisDaten().getWahl()).isEml510GebietAnhaengen();
  }

  protected String getRegionType(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return NlExportFileNames.getRegionType(wahldurchfuehrungsgebiet).toString().toLowerCase();
  }

  protected boolean exportsTotaaltelling() {
    return false; // ELECTVAPP-585
  }

  @Override
  public void addProperties(FormBuilder formBuilder, ErgebnisExportContext context, @Nullable Datei datei) {
    super.addProperties(formBuilder, context, datei);

    addSignProperties(context.getWahl(), formBuilder, context, getMessages());
  }
}
