package org.jabref.gui.mergeentries;

import java.util.List;
import java.util.Optional;

import javax.swing.undo.UndoManager;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.mergeentries.threewaymerge.EntriesMergeResult;
import org.jabref.gui.mergeentries.threewaymerge.MergeEntriesDialog;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.importer.EntryBasedFetcher;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherServerException;
import org.jabref.logic.importer.ImportCleanup;
import org.jabref.logic.importer.WebFetcher;
import org.jabref.logic.importer.WebFetchers;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldTextMapper;
import org.jabref.model.entry.field.StandardField;

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

/// Class for fetching and merging bibliographic information
public class FetchAndMergeEntry {

    // All identifiers listed here should also appear at {@link org.jabref.logic.importer.WebFetchers#getIdBasedFetcherFoIdentifier}
    public static List<Field> SUPPORTED_FIELDS =
            List.of(
                    StandardField.EPRINT, // arXiv
                    StandardField.DOI,
                    StandardField.ISBN,
                    StandardField.ISSN
            );

    private static final Logger LOGGER = LoggerFactory.getLogger(FetchAndMergeEntry.class);

    private final DialogService dialogService;
    private final UndoManager undoManager;
    private final StateManager stateManager;
    private final BibDatabaseContext bibDatabaseContext;
    private final TaskExecutor taskExecutor;
    private final GuiPreferences preferences;

    public FetchAndMergeEntry(BibDatabaseContext bibDatabaseContext,
                              TaskExecutor taskExecutor,
                              GuiPreferences preferences,
                              DialogService dialogService,
                              UndoManager undoManager,
                              StateManager stateManager) {
        this.bibDatabaseContext = bibDatabaseContext;
        this.taskExecutor = taskExecutor;
        this.preferences = preferences;
        this.dialogService = dialogService;
        this.undoManager = undoManager;
        this.stateManager = stateManager;
    }

    public void fetchAndMerge(BibEntry entry) {
        fetchAndMerge(entry, SUPPORTED_FIELDS);
    }

    public void fetchAndMerge(BibEntry entry, Field field) {
        fetchAndMerge(entry, List.of(field));
    }

    public void fetchAndMerge(BibEntry entry, List<Field> fields) {
        for (Field field : fields) {
            Optional<String> fieldContent = entry.getField(field);
            if (fieldContent.isPresent()) {
                WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences())
                           .ifPresentOrElse(idBasedFetcher ->
                                           BackgroundTask.wrap(() -> idBasedFetcher.performSearchById(fieldContent.get()))
                                                         .onSuccess(fetchedEntry -> {
                                                             ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences());
                                                             if (fetchedEntry.isPresent()) {
                                                                 cleanup.doPostCleanup(fetchedEntry.get());
                                                                 showMergeDialog(entry, fetchedEntry.get(), idBasedFetcher);
                                                             } else {
                                                                 dialogService.notify(Localization.lang("Cannot get info based on given %0: %1", FieldTextMapper.getDisplayName(field), fieldContent.get()));
                                                             }
                                                         })
                                                         .onFailure(exception -> {
                                                             LOGGER.error("Error while fetching bibliographic information", exception);
                                                             if (exception instanceof FetcherClientException) {
                                                                 dialogService.notify(Localization.lang("No data was found for the identifier"));
                                                             } else if (exception instanceof FetcherServerException) {
                                                                 dialogService.notify(Localization.lang("Server not available"));
                                                             } else {
                                                                 dialogService.notify(Localization.lang("Error while fetching from %0", idBasedFetcher.getName()));
                                                             }
                                                         })
                                                         .executeWith(taskExecutor),
                                   () -> LOGGER.warn("No fetcher available for '{}'", FieldTextMapper.getDisplayName(field)));
            } else {
                dialogService.notify(Localization.lang("No %0 found", FieldTextMapper.getDisplayName(field)));
            }
        }
    }

    private void showMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) {
        MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferences, stateManager);
        dialog.setTitle(Localization.lang("Merge entry with %0 information", fetcher.getName()));
        dialog.setLeftHeaderText(Localization.lang("Original entry"));
        dialog.setRightHeaderText(Localization.lang("Entry from %0", fetcher.getName()));
        dialog.autoSelectBetterFields();
        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(dialog).map(EntriesMergeResult::mergedEntry);

        new UpdateOriginalEntry(
                originalEntry,
                mergedEntry,
                dialogService,
                undoManager,
                Localization.lang("Merge entry with %0 information", fetcher.getName()),
                Localization.lang("Updated entry with info from %0", fetcher.getName()))
                .update();
    }

    public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) {
        BackgroundTask.wrap(() -> fetcher.performSearch(entry).stream().findFirst())
                      .onSuccess(fetchedEntry -> {
                          if (fetchedEntry.isPresent()) {
                              ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences());
                              cleanup.doPostCleanup(fetchedEntry.get());
                              showMergeDialog(entry, fetchedEntry.get(), fetcher);
                          } else {
                              dialogService.notify(Localization.lang("Could not find any bibliographic information."));
                          }
                      })
                      .onFailure(exception -> {
                          LOGGER.error("Error while fetching entry with {} ", fetcher.getName(), exception);
                          dialogService.notify(Localization.lang("Error while fetching from %0", fetcher.getName()));
                      })
                      .executeWith(taskExecutor);
    }
}
