package org.texttechnologylab.project.gruppe_05_1.database;

import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import io.javalin.http.Context;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.texttechnologylab.project.gruppe_05_1.database.domainimp.speeches.Speaker_MongoDB_Impl;
import org.texttechnologylab.project.gruppe_05_1.database.domainimp.speeches.Speech_MongoDB_Impl;
import org.texttechnologylab.project.gruppe_05_1.domain.html.HtmlSpeech;
import org.texttechnologylab.project.gruppe_05_1.domain.html.Parlamentarier;
import org.texttechnologylab.project.gruppe_05_1.domain.html.ParlamentarierDetails;
import org.texttechnologylab.project.gruppe_05_1.domain.nlp.*;
import org.texttechnologylab.project.gruppe_05_1.domain.speaker.Membership;
import org.texttechnologylab.project.gruppe_05_1.domain.speech.SpeechMetaData;
import org.texttechnologylab.project.gruppe_05_1.util.GeneralUtils;
import org.texttechnologylab.project.gruppe_05_1.util.Logger;
import org.texttechnologylab.project.gruppe_05_1.util.PPRUtils;
import org.texttechnologylab.project.gruppe_05_1.xml.speeches.Interfaces.Speech;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

import static com.mongodb.client.model.Filters.eq;

/**
 * Diese Klasse beinhaltet Mongo-Utilities, welche spezifisch für die PPR-Datenstrukturen sind.
 * Mongo-Utilities genereller Natur stehen in der Klasse MongoDBHandler.
 */
public class MongoPprUtils {

    /*
     * Collection-Information und-zugang
     * =======================================================================
     */
    public static final String SPEAKER_COLLECTION_NAME = "speaker";
    public static final String SPEECH_COLLECTION_NAME = "speech";

    public static final String SESSION_COLLECTION_NAME = "sessions";
    public static final String AGENDA_ITEMS_COLLECTION_NAME = "agendaItems";
    public static final String HISTORY_COLLECTION_NAME = "history";
    public static final String PICTURES_COLLECTION_NAME = "pictures";
    public static final String COMMENT_COLLECTION_NAME = "comment";

    private static MongoCollection<Document> speakerCollection = null;
    private static MongoCollection<Document> speechCollection = null;
    private static MongoCollection<Document> sessionCollection = null;
    private static MongoCollection<Document> agendaItemsCollection = null;
    private static MongoCollection<Document> picturesCollection = null;
    private static MongoCollection<Document> commentCollection = null;

    public static MongoCollection<Document> getSpeakerCollection() {
        if (speakerCollection == null) speakerCollection = MongoDBHandler.getMongoDatabase().getCollection(SPEAKER_COLLECTION_NAME);
        return speakerCollection;
    }

    public static MongoCollection<Document> getSpeechCollection() {
        if (speechCollection == null) speechCollection = MongoDBHandler.getMongoDatabase().getCollection(SPEECH_COLLECTION_NAME);
        return speechCollection;
    }

    public static MongoCollection<Document> getSessionCollection() {
        if (sessionCollection == null) sessionCollection = MongoDBHandler.getMongoDatabase().getCollection(SESSION_COLLECTION_NAME);
        return sessionCollection;
    }

    public static MongoCollection<Document> getAgendaItemsCollection() {
        if (agendaItemsCollection == null) agendaItemsCollection = MongoDBHandler.getMongoDatabase().getCollection(AGENDA_ITEMS_COLLECTION_NAME);
        return agendaItemsCollection;
    }

    public static MongoCollection<Document> getPicturesCollection() {
        if (picturesCollection == null) picturesCollection = MongoDBHandler.getMongoDatabase().getCollection(PICTURES_COLLECTION_NAME);
        return picturesCollection;
    }

    /**
     * Create the Speaker Collection and useful indices for it
     */
    public static void createIndexForSpeakerCollection() {
        // MongoDBHandler.createIndexForCollection(getSpeakerCollection(), Arrays.asList("name", "firstName", "party"), true);
        MongoDBHandler.createIndexForCollection(getSpeakerCollection(),"name", true);
        MongoDBHandler.createIndexForCollection(getSpeakerCollection(),"firstName", true);
        MongoDBHandler.createIndexForCollection(getSpeakerCollection(),"party", true);
    }

    /**
     * Create the Speech Collection and useful indices for it
     */
    public static void createIndexForSpeechCollection() {
        MongoDBHandler.createIndexForCollection(getSpeechCollection(), "speakerId", true);
        MongoDBHandler.createIndexForCollection(getSpeechCollection(), "speechKey", true);
    }


    /**
     * Truncate the Speaker Collection.
     * Note that it is quicker (and saves space) to drop and re-create rather than removing all documents using "remove({})"
     */
    public static void truncateSpeakerCollection() {

        getSpeakerCollection().drop();
        createIndexForSpeechCollection();
    }

    /*
     * Parlamentarier
     * =======================================================================
     */


    /**
     * Holt alle Parlamentarier, die einen Suchkriterium erfüllen.
     * Das Suchkriterium wird auf allen Feldern angewandt: Vorname, Nachname, Partei.
     * Ist das Suchkriterium leer, werden alle Parlamentarier zurückgeliefert
     * @param filter (optional): Suchkriterium
     * @return List<Parlamentarier>
     */
    public static List<Parlamentarier> getAllParlamentarier(String filter) {
        List<Parlamentarier> plist = new ArrayList<>();

        MongoCursor<Document> cursor;

        if (filter== null || filter.isBlank()) {
            cursor = getSpeakerCollection().find().iterator();
        } else {
            String pattern = ".*" + filter + ".*";
            Document searchDocument = new Document("$or", List.of(
                    new Document("name", new Document("$regex", pattern).append("$options", "i")),
                    new Document("firstName", new Document("$regex", pattern).append("$options", "i")),
                    new Document("party", new Document("$regex", pattern).append("$options", "i")),
                    new Document("_id", new Document("$regex", pattern).append("$options", "i"))
            ));
            cursor = getSpeakerCollection().find(searchDocument).cursor();
        }

        try {
            while (cursor.hasNext()) {
                Parlamentarier p = readParlamentarierFromSpeaker(cursor.next());
                plist.add(p);
            }
        } catch (Throwable t) {
            Logger.error(String.valueOf(t));
        } finally {
            cursor.close();
        }

        return plist;
    }

    public static List<Parlamentarier> getFilteredMembers(Context ctx) {
        // Get optional filter arguments
        String memberIdParam = ctx.queryParam("memberId");
        Integer memberId = (memberIdParam != null) ? Integer.parseInt(memberIdParam) : null;

        String name = ctx.queryParam("name");

        String lastName = ctx.queryParam("lastName");

        String firstName = ctx.queryParam("firstName");

        String title = ctx.queryParam("title");

        String dateOfBirth = ctx.queryParam("dateOfBirth");

        String dateOfDeath = ctx.queryParam("dateOfDeath");

        String placeOfBirth = ctx.queryParam("placeOfBirth");

        String gender = ctx.queryParam("gender");

        String religion = ctx.queryParam("religion");

        String party = (!Objects.equals(ctx.queryParam("party"), "")) ? ctx.queryParam("party") : null;

        String firstLegislativePeriod = ctx.queryParam("firstLegislativePeriod");
        Integer firstLegislativePeriodInt = (firstLegislativePeriod != null) ? Integer.parseInt(firstLegislativePeriod) : null;

        String lastLegislativePeriod = ctx.queryParam("lastLegislativePeriod");
        Integer lastLegislativePeriodInt = (lastLegislativePeriod != null) ? Integer.parseInt(lastLegislativePeriod) : null;

        List<Bson> filters = new ArrayList<>();
        if (memberId != null) filters.add(eq("id", memberId));
        if (name != null) filters.add(Filters.regex("name", ".*" + name + ".*", "i"));
        if (lastName != null) filters.add(Filters.regex("lastName", ".*" + lastName + ".*", "i"));
        if (firstName != null) filters.add(Filters.regex("firstName", ".*" + firstName + ".*", "i"));
        if (title != null) filters.add(Filters.regex("title", ".*" + title + ".*", "i"));
        if (dateOfBirth != null) filters.add(eq("dateOfBirth", dateOfBirth));
        if (dateOfDeath != null) filters.add(eq("dateOfDeath", dateOfDeath));
        if (placeOfBirth != null) filters.add(Filters.regex("placeOfBirth", ".*" + placeOfBirth + ".*", "i"));
        if (gender != null) filters.add(eq("gender", gender));
        if (religion != null) filters.add(eq("religion", religion));
        if (party != null) filters.add(eq("party", party));
        if (firstLegislativePeriodInt != null)
            filters.add(eq("firstLegislativePeriod", firstLegislativePeriodInt));
        if (lastLegislativePeriodInt != null)
            filters.add(eq("lastLegislativePeriod", lastLegislativePeriodInt));

        // Check if the filters list is empty
        Bson filter;
        if (filters.isEmpty()) {
            filter = Filters.empty();  // No filters, match all documents
        } else {
            filter = Filters.and(filters);  // Combine all filters with AND
        }
        Bson projection = Projections.fields(Projections.exclude("image_data"));

        try {
            List<Parlamentarier> members = retrieveAllMembersOfParliament(filter, projection);
            ctx.json(members);
            return members;
        } catch (IOException e) {
            ctx.status(500);
            ctx.result("Server error occurred");
            return null;
        }
    }

    public static List<Parlamentarier> retrieveAllMembersOfParliament(Bson filter, Bson projection) throws IOException {
        List<Document> speeches = getSpeakerCollection().find(filter).projection(projection).into(new ArrayList<>());
        List<Parlamentarier> result = new ArrayList<>();
        for (Document speech : speeches) {
            Parlamentarier parlamentarier = new Parlamentarier();
            parlamentarier.setId(speech.getString("_id"));
            parlamentarier.setVorname(speech.getString("firstName"));
            parlamentarier.setNachname(speech.getString("name"));
            parlamentarier.setPartei(speech.getString("party"));
            result.add(parlamentarier);
        }

        return result;
    }


    /**
     * Liest einen Parlamentarier von der MongoDB
     * @param doc - MongoDB Dokument eines Parlamentariers
     * @return Parlamentarier
     */
    private static Parlamentarier readParlamentarierFromSpeaker(Document doc) {
        Parlamentarier p = new Parlamentarier();

        p.setId((String) doc.get("_id"));
        p.setNachname((String) doc.get("name"));
        p.setVorname((String) doc.get("firstName"));
        String partei = (String) doc.get("party");
        p.setPartei(Objects.requireNonNullElse(partei, "(parteilos)"));

        return p;
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    // Parlamentarier - Picture

    /**
     *
     * @param id : ID des Parlamentariers
     * @return Das Foto (als Base64-encoded String)
     */
    public static String getParlamentarierPictureByID(String id) {
        Document doc = MongoDBHandler.findFirstDocumentInCollection(getPicturesCollection(), "memberId", id);
        if (doc == null) {
            return null;
        } else return doc.getString("base64");
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    // ParlamentarierDetails


    /**
     * Holt die Details eines Parlamentariers
     * @param id Parlamentarier-ID (String)
     * @return ParlamentarierDetails
     */
    public static ParlamentarierDetails getParlamentarierDetailsByID(String id) {
        Document doc = MongoDBHandler.findFirstDocumentInCollection(getSpeakerCollection(), "_id", id);
        return readParlamentarierDetailsFromSpeaker(doc);
    }


    /**
     * Holt die Details eines Parlamentariers
     * @param id Parlamentarier-ID (Integer)
     * @return ParlamentarierDetails
     */
    public static ParlamentarierDetails getParlamentarierDetailsByID(Integer id) {
        Document doc = MongoDBHandler.findFirstDocumentInCollection(getSpeakerCollection(), "_id", id.toString());
        return readParlamentarierDetailsFromSpeaker(doc);
    }

    /**
     * Liest die Details eines Parlamentariers aus MongoDB, reiche diese mit Bildinformationen aus dem "pictures"-Collection, falls vorhanden, an
     * @param doc - MongoDB Dokument eines Parlamentariers
     * @return ParlamentarierDetails
     */
    private static ParlamentarierDetails readParlamentarierDetailsFromSpeaker(Document doc) {
        ParlamentarierDetails p = new ParlamentarierDetails();

        String pid = (String) doc.get("_id");
        p.setId(pid);
        p.setNachname((String) doc.get("name"));
        p.setVorname((String) doc.get("firstName"));
        String partei = (String) doc.get("party");
        p.setPartei(Objects.requireNonNullElse(partei, "(parteilos)"));
        p.setTitle((String) doc.get("title"));
        p.setGeburtsort((String) doc.get("geburtsort"));
        p.setGeschlecht((String) doc.get("geschlecht"));
        p.setBeruf((String) doc.get("beruf"));
        p.setAkademischerTitel((String) doc.get("akademischertitel"));
        p.setFamilienstand((String) doc.get("familienstand"));
        p.setReligion((String) doc.get("religion"));
        p.setVita((String) doc.get("vita"));
        p.setPrimaryFoto((String) doc.get("primaryFoto"));

        Date geburtsdatum = (Date) doc.get("geburtsdatum");
        if (geburtsdatum != null) {
            p.setGeburtsdatum(LocalDate.ofInstant(geburtsdatum.toInstant(), ZoneId.systemDefault()));
        }
        Date sterbedatum = (Date) doc.get("sterbedatum");
        if (sterbedatum != null) {
            p.setSterbedatum(LocalDate.ofInstant(sterbedatum.toInstant(), ZoneId.systemDefault()));
        }

        List<Document> membershipDocList = doc.get("memberships", MongoDBHandler.DOC_LIST_CLASS);
        if (membershipDocList == null) {
            p.setMemberships(new ArrayList<>());
        } else {
            p.setMemberships(getMemberships(membershipDocList));
        }


        /* - TODO
        List<Speech> reden = readSpeechesOfParlamentarier(pid);
        PPRUtils.sortSpeechByDate(reden);
        p.setReden(reden);

         */

        return p;
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    // ParlamentarierDetails - Memberships

    /**
     * Liest die Memberships eines Parlamentariers
     * @param doclist Liste von MongoDB-Dokumente für die Memberships
     * @return List<Membership>
     */
    private static List<Membership> getMemberships(List<Document> doclist) {
        List<Membership> memberships = new ArrayList<>();
        for (Document doc : doclist) {
            memberships.add(getMembership(doc));
        }
        return memberships;
    }

    /**
     * Liest eine Memberships
     * @param doc MongoDB-Dokument für eine Membership
     * @return Membership
     */
    private static Membership getMembership(Document doc) {
        Membership m = new Membership();

        Date begin =  (Date) doc.get("begin");
        if (begin != null) {
            m.setBegin(LocalDate.ofInstant(begin.toInstant(), ZoneId.systemDefault()));
        }
        Date end =  (Date) doc.get("end");
        if (end != null) {
            m.setEnd(LocalDate.ofInstant(end.toInstant(), ZoneId.systemDefault()));
        }

        m.setRole((String) doc.get("role"));
        m.setMember((String) doc.get("member"));
        m.setLabel((String) doc.get("label"));
        m.setWp((Integer) doc.get("wp"));

        return m;
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    // Speech

    // TODO: kopiere die Speech-Sachen von Übung 4 hierher!

    /**
     * Aufzählen, wie viele Reden eines bestimmten Redners gespeichert sind
     * @param speakerId
     * @return Anzahl Reden
     */
    public static Long countSpeechesOfSpeaker(String speakerId) {
        return getSpeechCollection().countDocuments(new Document("speakerId", Integer.parseInt(speakerId)));
    }


    /**
     * Liefert alle Reden eines Redners zurück
     * @param speakerId
     * @return Alle Reden eines Redners
     */
    public static List<Speech> getSpeechesOfSpeaker(String speakerId) {
        List<Speech> speeches = new ArrayList<>();

        Document filter = new Document("speakerId", Integer.parseInt(speakerId));
        List<Document> docs = getSpeechCollection().find(filter).into(new ArrayList<>());

        for (Document doc : docs) {
            speeches.add(new Speech_MongoDB_Impl(doc, true));
        }

        return speeches;
    }

    /**
     * Liefert alle Reden zurück
     * Die Auswahl kann durch einen (textuellen) Filter eingeschränkt werden
     * @param filter
     * @return
     */
    public static List<Speech> getSpeeches(String filter) {
        List<Speech> speeches = new ArrayList<>();

        MongoCursor<Document> cursor;

        if (filter== null || filter.isBlank()) {
            cursor = getSpeechCollection().find().iterator();
        } else {
            String pattern = ".*" + filter + ".*";
            Document searchDocument = new Document("$or", List.of(
                    new Document("speakerName", new Document("$regex", pattern).append("$options", "i")),
                    new Document("fraction", new Document("$regex", pattern).append("$options", "i")),
                    new Document("speechKey", new Document("$regex", pattern).append("$options", "i"))
            ));
            cursor = getSpeechCollection().find(searchDocument).cursor();
        }

        try {
            while (cursor.hasNext()) {
                speeches.add(new Speech_MongoDB_Impl(cursor.next(), false));
            }
        } catch (Throwable t) {
            Logger.error(String.valueOf(t));
        } finally {
            cursor.close();
        }

        return speeches;
    }

    /**
     * Liefert Metadaten (aber keine Inhalte!) für alle Reden eines Redners zurück.
     * Als Metadaten zählen das Datum, Agenda-ID etc.
     * @param speakerId
     * @return
     */
    public static List<SpeechMetaData> getSpeechesMetadataForSpeaker(String speakerId) {

        List<SpeechMetaData> speechMetaDataList = new ArrayList<>();
        List<Speech> speeches = MongoPprUtils.getSpeechesOfSpeaker(speakerId);
        for (Speech speech : speeches) {
            SpeechMetaData md = new SpeechMetaData();

            md.setSpeechKey(speech.getSpeechKey());
            md.setSpeechId(speech.getSpeechId());
            md.setSpeakerId(speech.getSpeakerId());
            int sessionId = speech.getSessionId();
            md.setSessionId(sessionId);


            // aus "sessions" Collection
            augmentSpeechMetaDataFromSession(sessionId, md);

            // aus "agendaItems" Collection
            int agendaItemId = speech.getAgendaItemId();
            String agendaTitel = getAgendaTitle(sessionId, agendaItemId);
            md.setAgendaTitle(agendaTitel);

            speechMetaDataList.add(md);

        }

        // Sortiere nach Datum, absteigend
        speechMetaDataList.sort((md1, md2) -> {
            if ((md2.getDateTime() == null) && (md1.getDateTime()) == null) return 0;
            if (md2.getDateTime() == null)  return -1;
            if (md1.getDateTime() == null)  return 1;
            return md2.getDateTime().compareTo(md1.getDateTime());
        });

        return speechMetaDataList;
    }

    /**
     * Liefert Metadaten (aber keine Inhalte!) für alle Reden zurück.
     * Die Auswahl kann durch einen (textuellen) Filter eingeschränkt werden
     * Als Metadaten zählen das Datum, Agenda-ID etc.
     * @param filter
     * @return
     */
    public static List<SpeechMetaData> getSpeechesMetadata(String filter) {

        List<SpeechMetaData> speechMetaDataList = new ArrayList<>();
        List<Speech> speeches = MongoPprUtils.getSpeeches(filter);
        for (Speech speech : speeches) {
            SpeechMetaData md = new SpeechMetaData();

            md.setSpeechKey(speech.getSpeechKey());
            md.setSpeechId(speech.getSpeechId());
            md.setSpeakerId(speech.getSpeakerId());
            md.setSpeakerName(speech.getSpeakerName());
            md.setFraktion(speech.getFraction());
            if (md.getFraktion() == null) {md.setFraktion(PPRUtils.PARTEILOS_KUERZEL);}
            int sessionId = speech.getSessionId();
            md.setSessionId(sessionId);

            // aus "sessions" Collection
            augmentSpeechMetaDataFromSession(sessionId, md);

            // aus "agendaItems" Collection
            int agendaItemId = speech.getAgendaItemId();
            String agendaTitel = getAgendaTitle(sessionId, agendaItemId);
            md.setAgendaTitle(agendaTitel);

            speechMetaDataList.add(md);
        }

        // Sortiere nach Datum, absteigend
        speechMetaDataList.sort((md1, md2) -> {
            if ((md2.getDateTime() == null) && (md1.getDateTime()) == null) return 0;
            if (md2.getDateTime() == null)  return -1;
            if (md1.getDateTime() == null)  return 1;
            return md2.getDateTime().compareTo(md1.getDateTime());
        });

        return speechMetaDataList;
    }

    /**
     * Füge Rede-Metadaten (welche in der Session-Collection stehen) der Rede hinzu.
     * Achtung: Redezeit ist in der Datenbank in unterschiedlichen Formaten vorhanden.
     * @param sessionId
     * @param md
     */
    public static void augmentSpeechMetaDataFromSession(int sessionId, SpeechMetaData md) {
        String dateTimeString = getSessionDateTime(sessionId);
        if (dateTimeString != null) {
            md.setDateTimeString(dateTimeString);

            for (String format : Arrays.asList("dd.MM.yyyy HH:mm",
                    "dd.MM.yyyy H:mm",
                    "dd.MM.yyyy HH.mm",
                    "dd.MM.yyyy H.mm")) {
                LocalDateTime tmp = GeneralUtils.parseDateTime(dateTimeString,format);
                if (tmp != null) {
                    md.setDateTime(tmp);
                    return;
                }
            }
            Logger.error(dateTimeString + " could not be parsed");
        }
    }

    /**
     * Liefert das Datum und die Uhrzeit einer Sitzung zurück
     * @param sessionId
     * @return
     */
    public static String getSessionDateTime(int sessionId) {
        Document filter = new Document("sessionId", sessionId);
        Object dateTime = getSessionCollection().find(filter).first().get("dateTime");
        if (dateTime == null) {
            return null;
        } else {
            return (String) dateTime;
        }
    }

    /**
     * Liefert den Agenda-Titel zurück
     * @param sessionId
     * @return
     */
    public static String getAgendaTitle(int sessionId, int agendaItemId) {
        Document filter = new Document("sessionId", sessionId).append("id", agendaItemId);
        FindIterable<Document> iter = getAgendaItemsCollection().find(filter);
        if ((iter == null || (iter.first() == null))) {
            return "(kein Agendatitel)";
        } else {
            return (String) iter.first().get("title");
        }
    }

    /**
     * Liefert die Rede-Informationen für die Anzeige einer Rede:
     * - die Rede-ID
     * - Name und Fraktion des Redners
     * - Die Inhalte der Rede
     * @param key: Rede ID
     * @return
     */
    public static HtmlSpeech getSpeechByKey(String key) {
        Document filter = new Document("speechKey", key);
        Document speechDoc = getSpeechCollection().find(filter).first();
        if (speechDoc == null) {
            Logger.error("Rede " + key + " nicht gefunden");
            return null;
        }

        return new HtmlSpeech(speechDoc);
    }

    // getMemberPhoto

    public static String getMemberPhoto(String id) {
        Document doc = MongoDBHandler.findFirstDocumentInCollection(getPicturesCollection(), "memberId", id);
        if (doc == null) {
            return null;
        } else return doc.getString("base64");
    }
}
