package org.jabref.logic.importer.fetcher.citation.crossref;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;

import org.jabref.logic.ai.chatting.ChatModel;
import org.jabref.logic.ai.preferences.AiPreferences;
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.CrossRef;
import org.jabref.logic.importer.fetcher.citation.CitationFetcher;
import org.jabref.logic.importer.plaincitation.PlainCitationParser;
import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice;
import org.jabref.logic.importer.plaincitation.PlainCitationParserFactory;
import org.jabref.logic.importer.util.GrobidPreferences;
import org.jabref.logic.net.URLDownload;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.InternalField;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.identifier.DOI;
import org.jabref.model.entry.types.StandardEntryType;

import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

/// Uses [CrossRef's REST API](https://www.crossref.org/documentation/retrieve-metadata/rest-api/) for getting paper information
///
/// Example URL: <https://api.crossref.org/works/10.47397/tb/44-3/tb138kopp-jabref>
public class CrossRefCitationFetcher implements CitationFetcher {
    public static final String FETCHER_NAME = "CrossRef";

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

    private static final String API_URL = "https://api.crossref.org/works/";

    private final ImporterPreferences importerPreferences;
    private final ImportFormatPreferences importFormatPreferences;
    private final CitationKeyPatternPreferences citationKeyPatternPreferences;
    private final GrobidPreferences grobidPreferences;
    private final AiPreferences aiPreferences;
    private final ChatModel chatModel;

    private final ObjectMapper mapper = new ObjectMapper();

    private final CrossRef crossRefForDoi;

    public CrossRefCitationFetcher(
            ImporterPreferences importerPreferences,
            ImportFormatPreferences importFormatPreferences,
            CitationKeyPatternPreferences citationKeyPatternPreferences,
            GrobidPreferences grobidPreferences,
            AiPreferences aiPreferences,
            ChatModel chatModel) {
        this.importerPreferences = importerPreferences;
        this.importFormatPreferences = importFormatPreferences;
        this.citationKeyPatternPreferences = citationKeyPatternPreferences;
        this.grobidPreferences = grobidPreferences;
        this.aiPreferences = aiPreferences;
        this.chatModel = chatModel;
        this.crossRefForDoi = new CrossRef(importerPreferences);
    }

    @Override
    public String getName() {
        return FETCHER_NAME;
    }

    @Override
    public List<BibEntry> getReferences(BibEntry entry) throws FetcherException {
        BibEntry clonedEntry = new BibEntry(entry);
        Optional<DOI> doi = clonedEntry.getField(StandardField.DOI).flatMap(DOI::parse);
        if (doi.isEmpty()) {
            findDoiForEntry(clonedEntry);
        }

        Optional<URI> uri = getReferencesApiUri(clonedEntry);
        if (uri.isEmpty()) {
            return List.of();
        }

        PlainCitationParserChoice parserChoice = importerPreferences.getDefaultPlainCitationParser();
        final PlainCitationParser parser = parserChoice == PlainCitationParserChoice.LLM
                                           ? PlainCitationParserFactory.getLlmPlainCitationParser(importFormatPreferences, aiPreferences, chatModel)
                                           : PlainCitationParserFactory.getPlainCitationParser(parserChoice, citationKeyPatternPreferences, grobidPreferences, importFormatPreferences);

        try (InputStream stream = new URLDownload(uri.get().toString()).asInputStream()) {
            JsonNode node = mapper.readTree(stream);
            LOGGER.atDebug()
                  .addKeyValue("payload", node)
                  .log("Received JSON");
            JsonNode references = node.at("/message/reference");
            return references.valueStream()
                             .map(Unchecked.function(reference -> {
                                 JsonNode doiNode = reference.at("/DOI");
                                 if (doiNode.isMissingNode() && !reference.at("/key").isMissingNode()) {
                                     return getBibEntryFromData(reference);
                                 }
                                 JsonNode unstructured = reference.at("/unstructured");
                                 if (doiNode.isMissingNode()) {
                                     if (unstructured.isMissingNode()) {
                                         LOGGER.error("Reference has neither DOI nor unstructured text: {}", reference);
                                         return new BibEntry()
                                                 .withField(StandardField.NOTE, "Could not retrieve reference information from CrossRef")
                                                 .withChanged(true);
                                     }
                                     return getBibEntryFromText(parser, unstructured.asString());
                                 } else {
                                     return getBibEntryFromDoi(doiNode.asString(), unstructured);
                                 }
                             }))
                             .toList();
        } catch (MalformedURLException e) {
            throw new FetcherException("Could not construct correct URL", e);
        } catch (IOException e) {
            throw new FetcherException("Could not read from crossref", e);
        }
    }

    private BibEntry getBibEntryFromData(JsonNode reference) {
        BibEntry bibEntry = new BibEntry();
        setField(bibEntry, InternalField.KEY_FIELD, reference, "/key");
        setField(bibEntry, StandardField.AUTHOR, reference, "/author");
        setField(bibEntry, StandardField.TITLE, reference, "/article-title");
        setField(bibEntry, StandardField.JOURNAL, reference, "/journal-title");
        setField(bibEntry, StandardField.PAGES, reference, "/first-page");
        setField(bibEntry, StandardField.VOLUME, reference, "/volume");
        setField(bibEntry, StandardField.NUMBER, reference, "/number");

        if (!reference.at("/journal-title").isMissingNode()) {
            bibEntry.setType(StandardEntryType.Article);
        }

        bibEntry.setChanged(true);
        return bibEntry;
    }

    private void setField(BibEntry bibEntry, Field field, JsonNode reference, String path) {
        JsonNode node = reference.at(path);
        if (!node.isMissingNode()) {
            bibEntry.setField(field, node.asString());
        }
    }

    /// Not supported by CrossRef. Therefore, always returning an empty list.
    @Override
    public List<BibEntry> getCitations(BibEntry entry) throws FetcherException {
        return List.of();
    }

    private BibEntry getBibEntryFromText(PlainCitationParser parser, String unstructured) {
        try {
            return parser.parsePlainCitation(unstructured)
                         .orElseGet(() -> new BibEntry()
                                 .withField(StandardField.NOTE, unstructured)
                                 .withChanged(true));
        } catch (FetcherException e) {
            LOGGER.warn("Could not get bib entry from text {}", unstructured, e);
            return new BibEntry()
                    .withField(StandardField.NOTE, unstructured)
                    .withChanged(true);
        }
    }

    private BibEntry getBibEntryFromDoi(String referenceDoi, JsonNode unstructured) throws FetcherException {
        // Determine the doi using CrossRef
        // In case no BibEntry found for the doi, just return a BibEntry using the doi
        return crossRefForDoi.performSearchById(referenceDoi)
                             .orElseGet(() -> {
                                 BibEntry bibEntry = new BibEntry()
                                         .withField(StandardField.DOI, referenceDoi)
                                         .withChanged(true);
                                 if (!unstructured.isMissingNode()) {
                                     bibEntry.setField(StandardField.NOTE, unstructured.asText());
                                 }
                                 return bibEntry;
                             });
    }

    /// Crossref has no citation count feature
    @Override
    public Optional<Integer> getCitationCount(BibEntry entry) throws FetcherException {
        return Optional.empty();
    }

    // Clone of org.jabref.logic.importer.FulltextFetchers.findDoiForEntry
    private void findDoiForEntry(BibEntry clonedEntry) {
        try {
            crossRefForDoi.findIdentifier(clonedEntry)
                          .ifPresent(e -> clonedEntry.setField(StandardField.DOI, e.asString()));
        } catch (FetcherException e) {
            LOGGER.debug("Failed to find DOI", e);
        }
    }

    @Override
    public Optional<URI> getReferencesApiUri(BibEntry entry) {
        Optional<DOI> doi = entry.getField(StandardField.DOI).flatMap(DOI::parse);
        if (doi.isEmpty()) {
            return Optional.empty();
        }

        try {
            String apiUrl = API_URL + doi.get().asString();
            return Optional.of(new URI(apiUrl));
        } catch (URISyntaxException e) {
            LOGGER.debug("Could not create references API URI", e);
            return Optional.empty();
        }
    }

    /// CrossRef does not support fetching citations for a given entry.
    @Override
    public Optional<URI> getCitationsApiUri(BibEntry entry) {
        return Optional.empty();
    }
}
