package de.duehl.vocabulary.japanese.website.update.ownlists;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.SwingUtilities;

import de.duehl.basics.io.FileHelper;
import de.duehl.basics.logic.ErrorHandler;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;
import de.duehl.html.download.binary.SimpleBinaryFileDownloader;
import de.duehl.swing.logic.LongTimeProcessInformer;
import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.elements.progress.MultipleProgressDialog;
import de.duehl.vocabulary.japanese.common.persistence.Options;
import de.duehl.vocabulary.japanese.common.website.VocaluaryTrainerWebsiteConstants;
import de.duehl.vocabulary.japanese.data.FumikoDataStructures;
import de.duehl.vocabulary.japanese.data.OwnList;
import de.duehl.vocabulary.japanese.logic.ownlists.OwnLists;
import de.duehl.vocabulary.japanese.logic.ownlists.groups.OwnListGroup;
import de.duehl.vocabulary.japanese.ui.components.data.OwnListsPartActualizer;
import de.duehl.vocabulary.japanese.ui.data.FumikoUiObjects;
import de.duehl.vocabulary.japanese.ui.dialog.update.OwnListInterestSelectionAndDownloadDialog;
import de.duehl.vocabulary.japanese.website.update.ownlists.data.NewestOwnListVersionListEntry;
import de.duehl.vocabulary.japanese.website.update.ownlists.data.OwnListInterestAndVersionEntry;
import de.duehl.vocabulary.japanese.website.update.ownlists.groupfile.GroupFileIo;
import de.duehl.vocabulary.japanese.website.update.ownlists.groupfile.data.GroupFileEntry;
import de.duehl.vocabulary.japanese.website.update.ownlists.interest.OwnListVersionAndInterestIo;
import de.duehl.zipwithzip4j.unzip.UnzipWithZip4J2;

import static de.duehl.vocabulary.japanese.website.update.ownlists.groupfile.GroupFileIo.GROUP_BARE_FILENAME;

/**
 * Diese Klasse zeigt einen Dialog zur Auswahl der herunterzuladenden Gruppen von eigenen Dateien
 * an und lädt diese herunter und importiert sie.
 *
 * @version 1.01     2025-11-20
 * @author Christian Dühl
 */

public class GroupsDownloader {

    private static final boolean DEBUG = false;


    /** Die Datenstrukturen des Vokabeltrainers. */
    private final FumikoDataStructures dataStructures;

    /** Die häufig verwendeten Funktionen der grafischen Oberfläche des Vokabeltrainers. */
    private final FumikoUiObjects uiObjects;

    /** Die Liste der Einträge in der Datei Neuste_Version_Listen.txt auf dem Server. */
    private final List<NewestOwnListVersionListEntry> newestOwnListVersionEntries;

    /**
     * Die Liste der Einträge in der Datei Eigene_Listen_Interessen_und_Versionen.txt auf dem
     * Rechner des Benutzers.
     */
    private final List<OwnListInterestAndVersionEntry> interestAndVersionEntries;

    /**
     * Die Liste derjenigen Gruppennamen, die der Benutzer bisher noch nicht oder nur in einer
     * älteren Version kannte. Ist diese Liste leer, werden keine Informationen zu Neuerungen
     * angezeigt. In dem Fall wurde aus der Klasse GroupsOfOwnListsFromWebsiteImporter aufgerufen,
     * anderenfalls aus der Klasse OwnListGroupsUpdater.
     */
    private final List<String> newerOrUnseenGroupNames;

    /** Die Liste der Gruppennamen, an denen der Benutzer nach Auswahl im Dialog Interesse hat. */
    private List<String> groupNamesInterestedIn;

    /**
     * Die Liste der Gruppennamen, die der Benutzer nach Auswahl im Dialog herunterladen und
     * importieren möchte.
     */
    private List<String> groupNamesToDownload;

    /**
     * Das Verzeichnis zum Herunterladen und entpacken sowie importieren der Gruppen von eigenen
     * Listen.
     */
    private String ownListImportExportDirectory;

    /**
     * Gibt an, ob der Benutzer ein Verzeichnis zum Herunterladen, Auspacken und Importieren
     * gewählt hat.
     */
    private boolean ownListImportExportDirectorySelected;

    /**
     * Das Verzeichnis der Dateinamen ohne Pfad der Zipdateien (wie sie auf dem Webserver liegen
     * oder auch heruntergeladen lokal) nach dem Gruppennamen.
     */
    private Map<String, String> zipBareFilenamesByGroupName;

    /**
     * Das Verzeichnis der lokalen Dateinamen mit Pfad der heruntergeladenen Zipdateien nach dem
     * Gruppennamen.
     */
    private Map<String, String> localZipFilenamesByGroupName;

    /**
     * Das Verzeichnis der lokalen Verzeichnisnamen mit Pfad der ausgepackten Zipdateien nach dem
     * Gruppennamen.
     */
    private Map<String, String> localUnzippedDirnamesByGroupName;

    /** Das Verzeichnis der Versionen nach dem Gruppennamen. */
    private Map<String, Integer> versionByGroupName;

    /** Der Name der Gruppe, welche gerade importiert wird. */
    private String groupName;

    /**
     * Der Dialog zum Anzeigen des Fortschritts beim Download einer Gruppe.
     *
     * Beim Import einer Gruppe.
     */
    private MultipleProgressDialog progressDialog;

    /**
     * Das Verzeichnis der Listennamen (mit Leerzeichen etc.) nach den Dateinamen der Listen (ohne
     * Pfad), das aus den Einträgen in der Gruppendatei erstellt wurde.
     *
     * Beim Import einer Gruppe.
     */
    private Map<String, String> listNameByBareListFilenameMap;

    /**
     * Die Liste der Dateinamen von Dateien mit eigenen Listen im Archiv.
     *
     * Beim Import einer Gruppe.
     */
    private List<String> ownListFilenamesInArchive;

    /**
     * Konstruktor.
     *
     * @param dataStructures
     *            Die Datenstrukturen des Vokabeltrainers.
     * @param uiObjects
     *            Die häufig verwendeten Funktionen der grafischen Oberfläche des Vokabeltrainers.
     * @param newestOwnListVersionEntries
     *            Die Liste der Einträge in der Datei Neuste_Version_Listen.txt auf dem Server.
     * @param interestAndVersionEntries
     *            Die Liste der Einträge in der Datei Eigene_Listen_Interessen_und_Versionen.txt
     *            auf dem Rechner des Benutzers.
     * @param newerOrUnseenGroupNames
     *            Die Liste derjenigen Gruppennamen, die der Benutzer bisher noch nicht oder nur in
     *            einer älteren Version kannte. Ist diese Liste leer, werden keine Informationen zu
     *            Neuerungen angezeigt. In dem Fall wurde aus der Klasse
     *            GroupsOfOwnListsFromWebsiteImporter aufgerufen, anderenfalls aus der Klasse
     *            OwnListGroupsUpdater.
     */
    public GroupsDownloader(FumikoDataStructures dataStructures, FumikoUiObjects uiObjects,
            List<NewestOwnListVersionListEntry> newestOwnListVersionEntries,
            List<OwnListInterestAndVersionEntry> interestAndVersionEntries,
            List<String> newerOrUnseenGroupNames) {
        this.dataStructures = dataStructures;
        this.uiObjects = uiObjects;
        this.newestOwnListVersionEntries = newestOwnListVersionEntries;
        this.interestAndVersionEntries = interestAndVersionEntries;
        this.newerOrUnseenGroupNames = newerOrUnseenGroupNames;
    }

    /**
     * Zeigt einen Dialog an und lädt ggf. Gruppen von eigenen Listen herunter und installiert sie.
     */
    public void download() {
        boolean goOn = showOwnListInterestSelectionAndDownloadDialog();

        if (goOn) {
            workWithUsersSelection();
        }
    }

    private boolean showOwnListInterestSelectionAndDownloadDialog() {
        OwnListInterestSelectionAndDownloadDialog dialog =
                new OwnListInterestSelectionAndDownloadDialog(newestOwnListVersionEntries,
                        interestAndVersionEntries, newerOrUnseenGroupNames,
                        uiObjects.getGuiLocation(), uiObjects.getProgramImage());
        dialog.setVisible(true);

        if (dialog.isApplied()) {
            groupNamesInterestedIn = dialog.getGroupNamesInterestedIn();
            groupNamesToDownload = dialog.getGroupNamesToDownload();
            return true;
        }
        else {
            return false;
        }
    }

    private void workWithUsersSelection() {
        if (groupNamesToDownload.isEmpty()) {
            GuiTools.informUser(uiObjects.getWindowAsComponent(), "Hinweis",
                    "Es wurden keine Gruppen zum Herunterladen, Auspacken und importieren "
                            + "ausgewählt.");
            storeInteresstedGroupsWithoutAnyDownload();
        }
        else {
            reallyWorkWithUsersSelection();
        }
    }

    private void reallyWorkWithUsersSelection() {
        LongTimeProcessInformer informer = uiObjects.getInformer();
        informer.startLongTimeProcess("Download etc. läuft");
        new Thread(() -> workWithUsersSelectionInOwnThread()).start();
    }

    private void workWithUsersSelectionInOwnThread() {
        try {
            tryToWorkWithUsersSelectionInOwnThread();
        }
        catch (Exception exception) {
            SwingUtilities.invokeLater(() -> showError(exception));
        }
    }

    private void tryToWorkWithUsersSelectionInOwnThread() {
        updateNewerOwnListVersionAndInterestEntriesWithGuiSelection();
        storeInterestAndVersionFile();
        determineOwnListImportExportDirectory();
        if (ownListImportExportDirectorySelected) {
            createVersionByGroupNameMap();
            determineZipNames();
            downloadWantedGroups();
            unzipWantedGroups();
            boolean success = checkUnzipIsOk();
            if (success) {
                importWantedGroups();
            }
            removeTemporarilyFiles();
        }

        SwingUtilities.invokeLater(() -> actualizeInEdt());
    }

    /**
     * Aktualisiert die Einträge aus der Datei "Eigene_Listen_Interessen_und_Versionen.txt" auf dem
     * Rechner des Benutzer durch die im Dialog getroffenen Einstellungen bezüglich seiner
     * Interessen.
     */
    private void updateNewerOwnListVersionAndInterestEntriesWithGuiSelection() {
        for (OwnListInterestAndVersionEntry entry : interestAndVersionEntries) {
            String groupName = entry.getGroupName();
            boolean isInterested = groupNamesInterestedIn.contains(groupName);
            entry.setInterested(isInterested);
        }
    }

    /**
     * Speichert die aktualisierte Datei "Eigene_Listen_Interessen_und_Versionen.txt" auf dem
     * Rechner des Benutzer.
     */
    private void storeInterestAndVersionFile() {
        OwnListVersionAndInterestIo.storeInterestAndVersionFile(interestAndVersionEntries);
    }

    private void determineOwnListImportExportDirectory() {
        Options options = dataStructures.getOptions();
        ownListImportExportDirectory = options.getLastUsedOwnListImportExportDirectory();
        boolean ok = GuiTools.askUser(uiObjects.getWindowAsComponent(),
                "Verzeichnis zum Herunterladen, entpacken und Importieren der Gruppen",
                ""
                        + "Das Verzeichnis zum Herunterladen, entpacken und Importieren der "
                        + "Gruppen ist auf\n" + ownListImportExportDirectory + "\n"
                        + "eingestellt. Ist das In Ordnung?\n"
                        + "Anderenfalls kann ein anderes Verzeichnis gewählt werden.");
        if (ok) {
            ownListImportExportDirectorySelected = true;
        }
        else {
            String dir = GuiTools.openDirectory(ownListImportExportDirectory,
                    "Verzeichnis zum Herunterladen, entpacken und Importieren der Gruppen wählen",
                    uiObjects.getWindowAsComponent());
            dir = dir.strip();
            if (!dir.isEmpty() && FileHelper.isDirectory(dir)) {
                ownListImportExportDirectory = dir;
                options.setLastUsedOwnListImportExportDirectory(ownListImportExportDirectory);
                ownListImportExportDirectorySelected = true;
            }
            else {
                ownListImportExportDirectorySelected = false;
            }
        }
    }

    private void createVersionByGroupNameMap() {
        versionByGroupName = new HashMap<>();

        for (NewestOwnListVersionListEntry entry : newestOwnListVersionEntries) {
            versionByGroupName.put(entry.getGroupName(), entry.getVersion());
        }
    }

    private void determineZipNames() {
        zipBareFilenamesByGroupName = new HashMap<>();

        for (String groupName : groupNamesToDownload) {
            int version = versionByGroupName.get(groupName);
            String zipBareFilename = OwnListGroup.createGroupBareFilename(groupName, version);
            zipBareFilenamesByGroupName.put(groupName, zipBareFilename);
        }

        if (DEBUG) {
            for (String groupName : groupNamesToDownload) {
                System.out.println(groupName + " -> " + zipBareFilenamesByGroupName.get(groupName));
            }
        }
    }

    /** Lädt die gewünschten Gruppen herunter, die der Benutzer im Dialog ausgewählt hat. */
    private void downloadWantedGroups() {
        localZipFilenamesByGroupName = new HashMap<>();

        for (String groupName : groupNamesToDownload) {
            String zipBareFilename = zipBareFilenamesByGroupName.get(groupName);
            String zipFileUrl = VocaluaryTrainerWebsiteConstants.BASE_URL + zipBareFilename;
            String localZipFilename = FileHelper.concatPathes(ownListImportExportDirectory,
                    zipBareFilename);
            SimpleBinaryFileDownloader.downloadBinaryFile(zipFileUrl, localZipFilename);
            localZipFilenamesByGroupName.put(groupName, localZipFilename);
            if (DEBUG) {
                System.out.println("Heruntergeladen: " + localZipFilename);
            }
        }
    }

    /**
     * Packt die Gruppen aus, die der Benutzer im Dialog ausgewählt hat und die zuvor
     * heruntergeladen wurden.
     */
    private void unzipWantedGroups() {
        localUnzippedDirnamesByGroupName = new HashMap<>();

        for (String groupName : groupNamesToDownload) {
            String localZipFilename = localZipFilenamesByGroupName.get(groupName);
            int version = versionByGroupName.get(groupName);
            String unzipDirectory = FileHelper.concatPathes(ownListImportExportDirectory,
                    OwnListGroup.createGroupBareFilenameWithoutExtension(groupName, version));
            if (FileHelper.isDirectory(unzipDirectory)) {
                FileHelper.deleteTree(unzipDirectory);
                /*
                 * Anderenfalls friert er ein, wenn es das Verzeichnis schon gibt!
                 *
                 * Vermutlich weil 7zip auf der Kommandozeile nachfragt.
                 */
            }


            UnzipWithZip4J2 unzip = new UnzipWithZip4J2(localZipFilename,
                    ownListImportExportDirectory);
            unzip.unzip();

            localUnzippedDirnamesByGroupName.put(groupName, unzipDirectory);
        }
    }

    private boolean checkUnzipIsOk() {
        List<String> groupsWithMissingZipDir = new ArrayList<>();

        for (String groupName : groupNamesToDownload) {
            if (!localUnzippedDirnamesByGroupName.containsKey(groupName)) {
                groupsWithMissingZipDir.add(groupName);
            }
        }

        if (groupsWithMissingZipDir.isEmpty()) {
            return true;
        }
        else {
            String title = "Achtung";
            String message = ""
                    + "Kein ausgepacktes Gruppenverzeichnis "
                    + NumberString.germanPlural(groupsWithMissingZipDir.size(), "zu den Gruppen",
                            "zur Gruppe")
                    + " " + Text.joinWithCommaAndBlank(groupsWithMissingZipDir)
                    + " vorhanden.\n\n"
                    + "Daher wird nichts importiert.";
            GuiTools.informUser(uiObjects.getWindowAsComponent(), title, message);
            return false;
        }
    }

    /**
     * Importiert die Gruppen, die der Benutzer im Dialog ausgewählt hat und die zuvor
     * heruntergeladen und ausgepackt wurden.
     *
     * Der Import ist so gemacht, dass die Listen auch ohne Neustart übernommen werden.
     *
     * Ich pflege die Listennamen so, dass sie im Normalfall keine Listen der Benutzer
     * überschrieben können.
     *
     * Ganz sauber wäre es, wenn ich die eigenen Listen irgendwann auch in Unterverzeichnissen nach
     * Kategorie und Unterkategorie vorhalten würde. Aber das ist eine größere Umstellung. Erstmal
     * geht das auch so.
     */
    private void importWantedGroups() {
        createProgressDialog();

        for (String groupName : groupNamesToDownload) {
            this.groupName = groupName;
            importWantedGroup();
        }
    }

    private void createProgressDialog() {
        String title = "Fortschritt beim Import der "
                + NumberString.germanPlural(groupNamesToDownload.size(), "Gruppen", "Gruppe")
                + " '" + Text.join("', '", groupNamesToDownload) + "'";

        progressDialog = new MultipleProgressDialog(title, uiObjects.getGuiLocation(),
                uiObjects.getProgramImage());

        for (String groupName : groupNamesToDownload) {
            progressDialog
                    .addProgress(groupName)
                    .setProgressTitle(groupName,
                            "Fortschritt beim Import der Gruppe '" + groupName + "':")
                    .setCountPrefix(groupName,
                            "Anzahl importierter eigener Listen der Gruppe '" + groupName + "': ")
                    .setTimerPrefix(groupName, "Laufzeit: ")
                    .setActualElementPrefix(groupName, "Importiere: ")
                    .setActualElementPrefixBeforeStart(groupName, "Noch nichts importiert.")
                    .setActualElementWhenDone(groupName,
                            "Alle eigener Listen der Gruppe '" + groupName + "'wurden importiert.")
                    .createProgressPanel(groupName);
        }

        progressDialog.createUi();
    }

    private void importWantedGroup() {

        List<String> allFilenamesInArchive = getAllFilenamesInArchive();

        List<String> groupFilenames = FileHelper.getFilesWithGivenBareFilename(
                allFilenamesInArchive, GROUP_BARE_FILENAME);
        if (groupFilenames.size() != 1) {
            informUserAboutGroupFileFailure(groupName);
            progressDialog.closeUi(groupName);
            return;
        }
        createListNameByBareListFilenameMap(groupFilenames.get(0));

        ownListFilenamesInArchive = FileHelper.getFilesWithGivenExtension(
                allFilenamesInArchive, OwnList.OWN_LISTS_EXTENSION);
        if (ownListFilenamesInArchive.size() != listNameByBareListFilenameMap.size()) {
            informUserAboutOwnListFilesInArchiveNotLikeInGroupFileFailure(groupName);
            progressDialog.closeUi(groupName);
            return;
        }

        initProgressGui();
        removeOldOwnListsOfGroup();
        importOwnListFilenamesInArchive();
        progressDialog.closeUi(groupName);
    }

    private List<String> getAllFilenamesInArchive() {
        if (!localUnzippedDirnamesByGroupName.containsKey(groupName)) {
            throw new RuntimeException("Zu einer Gruppe ist kein Verzeichnis bekannt:\n"
                    + "\t" + "groupName = '" + groupName + "'\n");
        }

        String unzipDirectory = localUnzippedDirnamesByGroupName.get(groupName);

        if (!FileHelper.isDirectory(unzipDirectory)) {
            throw new RuntimeException("Das Verzeichnis zu einer Gruppe existiert nicht:\n"
                    + "\t" + "groupName = '" + groupName + "'\n"
                    + "\t" + "unzipDirectory = '" + unzipDirectory + "'\n");
        }

        List<String> allFilenamesInArchive =
                FileHelper.findAllFilesInMainDirectoryNio2(unzipDirectory);
        return allFilenamesInArchive;
    }

    private void createListNameByBareListFilenameMap(String groupFilename) {
        List<GroupFileEntry> entries = GroupFileIo.readGroupFile(groupFilename);
        listNameByBareListFilenameMap =
                GroupFileIo.createListNameByBareListFilenameMap(entries);
    }

    private void initProgressGui() {
        progressDialog.initNumberOfTasksToDo(groupName, ownListFilenamesInArchive.size());
    }

    private void informUserAboutGroupFileFailure(String groupName) {
        String title = "Achtung";
        String message = ""
                + "Im Ausgepackten Archiv der Gruppe " + groupName + " konnte keine "
                + "Gruppendatei gefunden werden\n"
                + "oder es gab ein Problem beim Einlesen der Gruppendatei.\n"
                + "\n"
                + "Daher werden keine Listen aus dieser Gruppe importiert.";
        GuiTools.informUser(uiObjects.getWindowAsComponent(), title, message);
    }

    private void informUserAboutOwnListFilesInArchiveNotLikeInGroupFileFailure(String groupName) {
        String title = "Achtung";
        String message = ""
                + "Im Ausgepackten Archiv der Gruppe " + groupName + " weichen die enthaltenen\n"
                + "eigenen Listen von denen in der Gruppendatei ab\n"
                + "\n"
                + "Daher werden keine Listen aus dieser Gruppe importiert.";
        GuiTools.informUser(uiObjects.getWindowAsComponent(), title, message);
    }

    private void removeOldOwnListsOfGroup() {
        Text.say("Entferne ggf. vorhandene, alte Listen der Gruppe '" + groupName + "' ...");
        OwnLists ownLists = dataStructures.getOwnLists();
        OwnListGroup group = OwnListGroup.createByGroupName(groupName);
        String groupStart = group.getNameStart();
        ownLists.deleteOwnListsOfGroup(groupStart);
    }

    private void importOwnList(String filename, String listName) {
        Text.say("Importiere: " + filename + " unter dem Namen '" + listName + ".");
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.importOwnListFromGroup(filename, listName);
    }

    private void importOwnListFilenamesInArchive() {
        progressDialog.startingWithTask(groupName);

        for (String filename : ownListFilenamesInArchive) {
            String bareFilename = FileHelper.getBareName(filename);
            progressDialog.aboutToExceuteOneTaskSoon(groupName, bareFilename);
            String listName = listNameByBareListFilenameMap.get(bareFilename);
            importOwnList(filename, listName);
            progressDialog.oneTaskDone(groupName, bareFilename);
        }
    }

    private void removeTemporarilyFiles() {
        removeZipFiles();
        removeUnpackedArchiveDirectories();
    }

    private void removeZipFiles() {
        for (String groupName : groupNamesToDownload) {
            String localZipFilename = localZipFilenamesByGroupName.get(groupName);
            deleteFile(localZipFilename);
        }
    }

    private void removeUnpackedArchiveDirectories() {
        for (String groupName : groupNamesToDownload) {
            String dir = localUnzippedDirnamesByGroupName.get(groupName);
            FileHelper.deleteTreeIgnoreErrors(dir);
        }
    }

    private void deleteFile(String garbageFilename) {
        try {
            FileHelper.deleteFileIfExistent(garbageFilename);
        }
        catch (Exception exeption) {
            System.out.println(exeption.getMessage());
        }
    }

    private void actualizeInEdt() {
        if (ownListImportExportDirectorySelected) {
            actualizeOwnListsInGui();
        }
        LongTimeProcessInformer informer = uiObjects.getInformer();
        informer.endLongTimeProcess();
    }

    private void actualizeOwnListsInGui() {
        OwnListsPartActualizer actualizer = uiObjects.getOwnListsPartActualizer();
        actualizer.actualizeOwnListsPart();
    }

    private void showError(Exception exception) {
        ErrorHandler error = uiObjects.getErrorHandler();

        error.error("Es trat ein Fehler beim Importieren der ausgewählten "
                + NumberString.germanPlural(groupNamesToDownload.size(), "Gruppen", "Gruppe")
                + " auf:\n" + exception.getMessage(),
                exception);
    }

    private void storeInteresstedGroupsWithoutAnyDownload() {
        new Thread(() -> storeInteresstedGroupsWithoutAnyDownloadInOwnThread()).start();
    }

    private void storeInteresstedGroupsWithoutAnyDownloadInOwnThread() {
        updateNewerOwnListVersionAndInterestEntriesWithGuiSelection();
        storeInterestAndVersionFile();
    }

}
