package de.ivu.elect.sitzberechnung.osv.exchange.p22;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import de.ivu.elect.sitzberechnung.ErgebnisLogEintragArt;
import de.ivu.elect.sitzberechnung.Kandidat;
import de.ivu.elect.sitzberechnung.Kandidatur;
import de.ivu.elect.sitzberechnung.Liste;
import de.ivu.elect.sitzberechnung.entity.DefaultSitzberechnungsergebnis;
import de.ivu.elect.sitzberechnung.osv.exchange.control.OSVLegacySitzberechnungsdaten;
import de.ivu.wahl.result.Candidate;
import de.ivu.wahl.result.CandidateList;
import de.ivu.wahl.result.ElectionAndCandidatesAndVotes;
import de.ivu.wahl.result.ElectoralDistrict;
import de.ivu.wahl.result.determination.GeneralList;
import de.ivu.wahl.result.determination.P2List;
import de.ivu.wahl.result.determination.P3List;
import de.ivu.wahl.result.result.Anomaly;
import de.ivu.wahl.result.result.Assignment;
import de.ivu.wahl.result.result.AssignmentSupplement;
import de.ivu.wahl.result.result.AssignmentSupplementVotes;
import de.ivu.wahl.result.result.AssignmentSupplementWithRemainderFraction;
import de.ivu.wahl.result.result.CandidateResult;
import de.ivu.wahl.result.result.DHondtFraction;
import de.ivu.wahl.result.result.DHondtSeatDistribution;
import de.ivu.wahl.result.result.ElectionResult;
import de.ivu.wahl.result.result.SubDistribution;

/*
 *
 *
 * */

public class P22DokumentMapper {


  private final OSVLegacySitzberechnungsdaten osvDaten;
  private final ElectionAndCandidatesAndVotes ecv;

  public P22DokumentMapper(OSVLegacySitzberechnungsdaten osvDaten, ElectionAndCandidatesAndVotes ecv) {
    this.osvDaten = osvDaten;
    this.ecv = ecv;
  }

  public OSVLegacyErgebnisDaten mappe(ElectionResult quelle, DefaultSitzberechnungsergebnis ergebnis) {
    OSVLegacyErgebnisDaten osvLegacyErgebnisDaten = new OSVLegacyErgebnisDaten();

    osvLegacyErgebnisDaten.setAnzahlStimmen(quelle.getNumberOfVotes());
    osvLegacyErgebnisDaten.setAnzahlSitze(quelle.getNumberOfSeats());
    osvLegacyErgebnisDaten.setVorzugsschwelleInProzent(quelle.getPreferentialBarrierInPercent());
    osvLegacyErgebnisDaten.setMussVorzugsschwelleUebertreffen(quelle.isMustExceedPreferentialBarrier());

    mapZuteilungen(quelle, osvLegacyErgebnisDaten);
    mapBesonderheiten(quelle, osvLegacyErgebnisDaten, ergebnis);
    mapDHondtSeatDistribution(quelle, osvLegacyErgebnisDaten);
    mapUnterverteilungen(quelle, osvLegacyErgebnisDaten);

    mapGewaehlteMitVorzugssitz(quelle, osvLegacyErgebnisDaten);

    mapListenMitGeaenderterReihenfolge(quelle, osvLegacyErgebnisDaten);

    return osvLegacyErgebnisDaten;
  }


  private void mapBesonderheiten(ElectionResult quelle, OSVLegacyErgebnisDaten ziel, DefaultSitzberechnungsergebnis ergebnis) {
    logListenerschoepfungen(ziel, ergebnis);

    logMehrheitssicherungsklausel(ziel, ergebnis);

    logBesonderheiten(quelle, ziel, ergebnis);
  }

  private void logBesonderheiten(ElectionResult quelle, OSVLegacyErgebnisDaten ziel, DefaultSitzberechnungsergebnis ergebnis) {
    List<BesonderheitBeiNominierungDerGewaehlten> besonderheiten = quelle.getAnomaliesToBePublishedInP22().stream()
            .map(this::toBesonderheit)
            .collect(Collectors.toList());
    ziel.setBesonderheiten(besonderheiten);
    besonderheiten.forEach(l -> ergebnis.log(ErgebnisLogEintragArt.BESONDERHEIT, l.getTyp().name(), l.getParameters().toArray()));
  }

  private void logMehrheitssicherungsklausel(OSVLegacyErgebnisDaten ziel, DefaultSitzberechnungsergebnis ergebnis) {
    if (kommtMehrheitssicherungsklauselZurAnwendung(ziel.getZuteilungen())) {
      ergebnis.log(ErgebnisLogEintragArt.BESONDERHEIT,
              BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_List_0_HasAbsoluteMajorityOfVotesAndReceivesSeatFromList_1.name(),
              getGewinnerBeiMehrheitssicherungsklausel(ziel.getZuteilungen()), getVerliererBeiMehrheitssicherungsklausel(ziel.getZuteilungen()));
    }
  }

  private void logListenerschoepfungen(OSVLegacyErgebnisDaten ziel, DefaultSitzberechnungsergebnis ergebnis) {
    ziel.getZuteilungen().stream()
            .filter(z -> z.getZuteilungTyp().equals(ZuteilungTyp.LISTENERSCHOEPFUNG) && z.getSitze() != 0)
            .map(listenerschoepfungZuteilungToBesonderheit())
            .forEach(b -> ergebnis.log(ErgebnisLogEintragArt.BESONDERHEIT, b.getTyp().name(), b.getParameters().toArray()));
  }

  private boolean kommtMehrheitssicherungsklauselZurAnwendung(List<Zuteilung> zuteilungen) {
    return zuteilungen.stream()
            .anyMatch(zuteilung -> zuteilung.getVerteilungTyp().equals(VerteilungTyp.OBERVERTEILUNG) && zuteilung.getZuteilungTyp().equals(ZuteilungTyp.MEHRHEITSSICHERUNGSKLAUSEL));
  }

  private String getGewinnerBeiMehrheitssicherungsklausel(List<Zuteilung> zuteilungen) {
    return zuteilungen.stream()
            .filter(zuteilung -> zuteilung.getSitze() > 0 && zuteilung.getVerteilungTyp().equals(VerteilungTyp.OBERVERTEILUNG) && zuteilung.getZuteilungTyp().equals(ZuteilungTyp.MEHRHEITSSICHERUNGSKLAUSEL))
            .findAny()
            .map(zuteilung -> zuteilung.getVerteilungAn().getGruppeKey())
            .map(gk -> Integer.toString(ecv.getP3ListsMap().get(gk).getListNumber()))
            .orElse("");
  }

  private String getVerliererBeiMehrheitssicherungsklausel(List<Zuteilung> zuteilungen) {
    return zuteilungen.stream()
            .filter(zuteilung -> zuteilung.getSitze() < 0 && zuteilung.getVerteilungTyp().equals(VerteilungTyp.OBERVERTEILUNG) && zuteilung.getZuteilungTyp().equals(ZuteilungTyp.MEHRHEITSSICHERUNGSKLAUSEL))
            .findAny()
            .map(zuteilung -> zuteilung.getVerteilungAn().getGruppeKey())
            .map(gk -> Integer.toString(ecv.getP3ListsMap().get(gk).getListNumber()))
            .orElse("null");
  }

  private Function<Zuteilung, BesonderheitBeiNominierungDerGewaehlten> listenerschoepfungZuteilungToBesonderheit() {
    return z -> {

      BesonderheitBeiNominierungDerGewaehlten result = new BesonderheitBeiNominierungDerGewaehlten();
      List<String> params = new ArrayList<>();
      int verloreneSitze = -z.getSitze();

      if (z.getVerteilungTyp().equals(VerteilungTyp.OBERVERTEILUNG)) {
        params.add(z.getVerteilungAn().getGruppeKey());
        setBesonderheitParamsUndTyp(result, params, verloreneSitze,
                BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_1_SeatsLostToOtherList,
                BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_SeatLostToOtherList,
                z.getVerteilungAn().getGruppeKey());
      }

      else if (z.getVerteilungTyp().equals(VerteilungTyp.UNTERVERTEILUNG)) {
        boolean isStel = z.getVerteilungAn().isStel();
        if (isStel) {
          setBesonderheitParamsUndTyp(result, params, verloreneSitze,
                  BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_1_2_SeatsLostToOtherListInGroup_stel,
                  BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_1_SeatLostToOtherListInGroup_stel,
                  z.getVerteilungAn().getStelKey());
          params.add(ecv.getP2ListsMap().get(z.getVerteilungAn().getStelKey()).getFirstCandidateList().getListNumberOrName());
        } else {
          setBesonderheitParamsUndTyp(result, params, verloreneSitze,
                  BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_1_2_SeatsLostToOtherListInGroup,
                  BesonderheitBeiNominierungDerGewaehltenTyp.Result_User_ListExhaustionFor_0_1_SeatLostToOtherListInGroup,
                  z.getVerteilungAn().getListeKey());
          params.add(ecv.getP2ListsMap().get(z.getVerteilungAn().getListeKey()).getFirstCandidateList().getListNumberOrName());
        }
      }

      result.setParameters(params);
      return result;
    };
  }

  private void setBesonderheitParamsUndTyp(BesonderheitBeiNominierungDerGewaehlten result, List<String> params, int verloreneSitze,
                                           BesonderheitBeiNominierungDerGewaehltenTyp mehrereVerloreneSitzeProperty,
                                           BesonderheitBeiNominierungDerGewaehltenTyp einzelnerVerlorenerSitzProperty, String key) {
    params.add(key);
    if (verloreneSitze > 1) {
      result.setTyp(mehrereVerloreneSitzeProperty);
      params.add(Integer.toString(verloreneSitze));
    } else {
      result.setTyp(einzelnerVerlorenerSitzProperty);
    }
  }

  private BesonderheitBeiNominierungDerGewaehlten toBesonderheit(Anomaly anomaly) {
    BesonderheitBeiNominierungDerGewaehlten result = new BesonderheitBeiNominierungDerGewaehlten();
    result.setIndex(anomaly.getIndex());
    BesonderheitBeiNominierungDerGewaehltenTyp typ = BesonderheitBeiNominierungDerGewaehltenTyp.valueOf(anomaly.getMessageKey());
    result.setTyp(typ);
    result.setParameters(anomaly.getParameters());
    return result;
  }

  private void mapZuteilungen(ElectionResult quelle, OSVLegacyErgebnisDaten ziel) {
    ziel.setZuteilungen(quelle.getAssignments().stream()
            .map(this::toZuteilung).collect(Collectors.toList()));
  }

  private Zuteilung toZuteilung(Assignment assignment) {
    Zuteilung result = new Zuteilung();

    result.setVerteilungTyp(VerteilungTyp.byId(assignment.getDistribution().name()));
    result.setZuteilungTyp(ZuteilungTyp.byKey(assignment.getAssignmentType().getKey()));
    result.setSchrittnummer(assignment.getIndex());
    result.setVerteilungInnerhalbVon(toAllgemeineListe(assignment.getAssignmentParent()));
    result.setVerteilungAn(toAllgemeineListe(assignment.getList()));
    result.setLosentscheid(assignment.isByLot());
    result.setSitze((int) assignment.getSeats());

    AssignmentSupplement supplement = assignment.getSupplement();
    if (supplement instanceof AssignmentSupplementVotes) {
      AssignmentSupplementVotes sup = (AssignmentSupplementVotes) supplement;
      result.setZaehler(sup.getVotes());
    }
    if (supplement instanceof AssignmentSupplementWithRemainderFraction) {
      AssignmentSupplementWithRemainderFraction sup = (AssignmentSupplementWithRemainderFraction) supplement;
      int remainderDenominator = (int) sup.getRemainder().getDenominator();
      int remainderNumerator = (int) sup.getRemainder().getNumerator();
      result.setZaehlerVomRest(remainderNumerator);
      result.setNennerVomRest(remainderDenominator);
    }

    return result;
  }

  private void mapDHondtSeatDistribution(ElectionResult quelle, OSVLegacyErgebnisDaten ziel) {
    ziel.setDHondtSitzverteilungen(
            quelle.getDHondtSeatDistributions().stream().map(this::toDHondtSitzverteilung).collect(Collectors.toList()));
  }

  private DHondtSitzverteilung toDHondtSitzverteilung(DHondtSeatDistribution dHondtSeatDistribution) {
    DHondtSitzverteilung result = new DHondtSitzverteilung();

    result.setSitze(dHondtSeatDistribution.getSeats());
    result.setSitzeRest(dHondtSeatDistribution.getResidualSeats());
    result.setSitzeGesamtZuVerteilen(dHondtSeatDistribution.getTotalNumberOfSeatsToDistribute());
    result.setVerteilungInnerhalbVon(toAllgemeineListe(dHondtSeatDistribution.getParent()));
    result.setVerteilungAn(toAllgemeineListe(dHondtSeatDistribution.getList()));
    result.setVerteilungTyp(VerteilungTyp.byId(dHondtSeatDistribution.getDistributionType().name()));
    result.setDHondtQuotienten(
            dHondtSeatDistribution.getdHondtFractions().stream().map(this::toDHondtQuotient).collect(Collectors.toList()));

    return result;
  }

  private DHondtQuotient toDHondtQuotient(DHondtFraction dHondtFraction) {
    DHondtQuotient result = new DHondtQuotient();

    result.setLauf(dHondtFraction.getColumnIndex());
    result.setNenner((int) dHondtFraction.getDenominator());
    result.setSitzAusRestanteil(dHondtFraction.receivesSeat());
    result.setZaehler((int) dHondtFraction.getNumerator());

    return result;
  }

  private void mapUnterverteilungen(ElectionResult quelle, OSVLegacyErgebnisDaten ziel) {
    ziel.setUnterverteilungen(
            quelle.getSubDistributions().stream().map(this::toUnterverteilung).collect(Collectors.toList()));
  }

  private Unterverteilung toUnterverteilung(SubDistribution subDistribution) {
    Unterverteilung result = new Unterverteilung();
    result.setSitze(subDistribution.getSeats());
    result.setStimmen(subDistribution.getVotes());
    result.setGruppeKey(toGruppeKey(subDistribution.getP3List()));

    return result;
  }


  void mapListenMitGeaenderterReihenfolge(ElectionResult quelle, OSVLegacyErgebnisDaten ziel) {
    if (quelle.getNewListPositions().isEmpty()) {
      return;
    }

    for (Map.Entry<P2List, List<Candidate>> p2ListListEntry : quelle.getOldListPositions().entrySet()) {
      P2List p2List = p2ListListEntry.getKey();
      if (!Objects.equals(oldPositionsOhneVerstorbeneKandidaten(p2ListListEntry.getValue()), quelle.getNewListPositions().get(p2List))) {
        ziel.addListeMitGeaenderterReihenfolge(toAllgemeineListe(p2List));
      }
    }
  }

  private List<Candidate> oldPositionsOhneVerstorbeneKandidaten(List<Candidate> p2List) {
    //ELECTVAPP-639
    return p2List.stream()
            .filter(c -> !ecv.getDeceasedCandidates().contains(c))
            .collect(Collectors.toList());
  }

  private void mapGewaehlteMitVorzugssitz(ElectionResult quelle, OSVLegacyErgebnisDaten ziel) {
    if (quelle.getNewListPositions().isEmpty()) {
      return;
    }

    for (Kandidat kandidat : osvDaten.getSitzberechnungsdaten().getAlleKandidaten()) {
      List<Kandidatur> kandidatMitVorzugGewaehltListen = getKandidaturenAlsMitVorzugGewaehlt(quelle.getCandidateResultsElectedHere(), kandidat);
      for (Kandidatur kandidatur : kandidatMitVorzugGewaehltListen) {
        ziel.addMitVorzugGewaehlt(kandidatur.getKey());
      }
    }
  }

  private List<Kandidatur> getKandidaturenAlsMitVorzugGewaehlt(List<CandidateResult> osvLegacyCandidateElectedHereResults, Kandidat electKandidat) {
    List<Liste> listenMitKandidatAlsVorzugGewaehlt = osvLegacyCandidateElectedHereResults.stream()
            .filter(c -> c.isAbovePreferentialBarrier() && c.getCandidate().getExternalKey().equals(electKandidat.getKey()))
            .findAny() //Der Kandidat kann nur auf einer einzigen P2Liste gewählt werden.
            .map(candidateResult -> osvDaten.getElectListenEinerP2Liste(candidateResult.getP2List())) //P2Liste könnte mehrere Elect-Listen haben, wenn es um Stels geht.
            .orElse(Collections.emptyList());

    return listenMitKandidatAlsVorzugGewaehlt.stream().flatMap(l -> l.getListenkandidaturen().stream()
            .filter(k -> k.getKandidat().getKey().equals(electKandidat.getKey())))
            .collect(Collectors.toList());
  }

  private AllgemeineListe toAllgemeineListe(GeneralList generalList) {
    AllgemeineListe result = new AllgemeineListe();
    if (generalList instanceof P2List) {
      P2List p2List = (P2List) generalList;
      if (p2List.isStel()) {
        result.setStelKey(toStelKey(p2List));
      } else {
        result.setListeKey(toListeKey(p2List));
      }
    } else if (generalList instanceof P3List) {
      result.setGruppeKey(toGruppeKey((P3List) generalList));
    }
    return result;
  }

  private String toListeKey(P2List p2List) {
    CandidateList candidateList = p2List.getCandidateLists().iterator().next();
    ElectoralDistrict electoralDistrict = candidateList.getElectoralDistrict();
    Object externalP3ListKey = candidateList.getExternalP3ListKey();

    return osvDaten.getSitzberechnungsdaten().findListeNachGruppeKeyUndGebietKey((String) externalP3ListKey, (String) electoralDistrict.getExternalKey()).getKey();
  }

  private String toStelKey(P2List p2List) {
    return osvDaten.findStelNachKey((String) p2List.getExternalKey()).getKey();
  }

  private String toGruppeKey(P3List p3List) {
    return osvDaten.getSitzberechnungsdaten().findGruppeNachKey((String) p3List.getExternalKey()).getKey();
  }

}
