package org.jabref.logic.citation;

import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;

import javafx.beans.property.ObjectProperty;

import org.jabref.logic.ai.chatting.ChatModel;
import org.jabref.logic.ai.preferences.AiPreferences;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.citation.repository.BibEntryCitationsAndReferencesRepository;
import org.jabref.logic.citation.repository.BibEntryCitationsAndReferencesRepositoryShell;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.ImporterPreferences;
import org.jabref.logic.importer.fetcher.citation.CitationCountFetcher;
import org.jabref.logic.importer.fetcher.citation.CitationCountFetcherType;
import org.jabref.logic.importer.fetcher.citation.CitationFetcher;
import org.jabref.logic.importer.fetcher.citation.CitationFetcherType;
import org.jabref.logic.importer.util.GrobidPreferences;
import org.jabref.logic.util.Directories;
import org.jabref.logic.util.NotificationService;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;

import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SearchCitationsRelationsService {

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

    private volatile CitationFetcher citationFetcher;
    private volatile CitationCountFetcher citationCountFetcher;
    private final BibEntryCitationsAndReferencesRepository relationsRepository;

    public SearchCitationsRelationsService(ImporterPreferences importerPreferences,
                                           ImportFormatPreferences importFormatPreferences,
                                           FieldPreferences fieldPreferences,
                                           ObjectProperty<CitationFetcherType> citationFetcherTypeProperty,
                                           ObjectProperty<CitationCountFetcherType> citationCountFetcherTypeProperty,
                                           CitationKeyPatternPreferences citationKeyPatternPreferences,
                                           GrobidPreferences grobidPreferences,
                                           AiPreferences aiPreferences,
                                           ChatModel chatModel,
                                           BibEntryTypesManager entryTypesManager,
                                           NotificationService notificationService) {
        this.citationFetcher = CitationFetcherType.getCitationFetcher(
                citationFetcherTypeProperty.get(),
                importerPreferences,
                importFormatPreferences,
                citationKeyPatternPreferences,
                grobidPreferences,
                aiPreferences,
                chatModel);

        this.citationCountFetcher = CitationCountFetcherType.getCitationCountFetcher(
                citationCountFetcherTypeProperty.get(),
                importerPreferences);

        citationFetcherTypeProperty.addListener((_, _, newValue) -> {
            this.citationFetcher = CitationFetcherType.getCitationFetcher(
                    newValue,
                    importerPreferences,
                    importFormatPreferences,
                    citationKeyPatternPreferences,
                    grobidPreferences,
                    aiPreferences,
                    chatModel);
        });

        citationCountFetcherTypeProperty.addListener((_, _, newValue) -> {
            this.citationCountFetcher = CitationCountFetcherType.getCitationCountFetcher(
                    newValue,
                    importerPreferences);
        });

        this.relationsRepository = new BibEntryCitationsAndReferencesRepositoryShell(
                Directories.getCitationsRelationsDirectory(),
                importerPreferences.getCitationsRelationsStoreTTL(),
                importFormatPreferences,
                fieldPreferences,
                entryTypesManager,
                citationFetcherTypeProperty,
                notificationService
        );
    }

    @VisibleForTesting
    SearchCitationsRelationsService(CitationFetcher citationFetcher,
                                    BibEntryCitationsAndReferencesRepository repository
    ) {
        this.citationFetcher = citationFetcher;
        this.citationCountFetcher = citationFetcher;
        this.relationsRepository = repository;
    }

    public List<BibEntry> searchCites(BibEntry referencing, boolean bypassCache) throws FetcherException {
        return searchCites(referencing, bypassCache, () -> false);
    }

    public List<BibEntry> searchCites(BibEntry referencing, boolean bypassCache, BooleanSupplier isCancelled) throws FetcherException {
        boolean isFetchingAllowed =
                bypassCache
                        || !relationsRepository.containsReferences(referencing)
                        || relationsRepository.isReferencesUpdatable(referencing);
        if (isFetchingAllowed) {
            List<BibEntry> referencedBy = citationFetcher.getReferences(referencing);
            if (isCancelled.getAsBoolean()) {
                return List.of();
            }
            relationsRepository.addReferences(referencing, referencedBy);
        }
        return relationsRepository.getReferences(referencing);
    }

    /// If the store was empty and nothing was fetch in any case (empty fetch, or error) then yes => empty list
    /// If the store was not empty and nothing was fetched after a successful fetch => the store will be erased and the returned collection will be empty
    /// If the store was not empty and an error occurs while fetching => will return the content of the store
    public List<BibEntry> searchCitedBy(BibEntry cited, boolean bypassCache) throws FetcherException {
        return searchCitedBy(cited, bypassCache, () -> false);
    }

    public List<BibEntry> searchCitedBy(BibEntry cited, boolean bypassCache, BooleanSupplier isCancelled) throws FetcherException {
        boolean isFetchingAllowed =
                bypassCache
                        || !relationsRepository.containsCitations(cited)
                        || relationsRepository.isCitationsUpdatable(cited);
        if (isFetchingAllowed) {
            List<BibEntry> citedBy = citationFetcher.getCitations(cited);
            if (isCancelled.getAsBoolean()) {
                return List.of();
            }
            relationsRepository.addCitations(cited, citedBy);
        }
        return relationsRepository.getCitations(cited);
    }

    public int getCitationCount(BibEntry citationCounted, Optional<String> actualFieldValue) throws FetcherException {
        boolean isFetchingAllowed = actualFieldValue.isEmpty() ||
                relationsRepository.isCitationsUpdatable(citationCounted);
        if (isFetchingAllowed) {
            Optional<Integer> citationCountResult = citationCountFetcher.getCitationCount(citationCounted);
            return citationCountResult.orElse(0);
        }
        assert actualFieldValue.isPresent();
        return Integer.parseInt(actualFieldValue.get());
    }

    public boolean isReferencesUpdatable(BibEntry entry) {
        return relationsRepository.isReferencesUpdatable(entry);
    }

    public boolean isCitationsUpdatable(BibEntry entry) {
        return relationsRepository.isCitationsUpdatable(entry);
    }

    public void close() {
        relationsRepository.close();
    }

    public Optional<URI> getReferencesApiUri(BibEntry entry) {
        return citationFetcher.getReferencesApiUri(entry);
    }

    public Optional<URI> getCitationsApiUri(BibEntry entry) {
        return citationFetcher.getCitationsApiUri(entry);
    }
}
