/*
 * @Copyright 2018-2026 HardBackNutter
 * @License GNU General Public License
 *
 * This file is part of NeverTooManyBooks.
 *
 * NeverTooManyBooks is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * NeverTooManyBooks is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NeverTooManyBooks. If not, see <http://www.gnu.org/licenses/>.
 */

package com.hardbacknutter.nevertoomanybooks.searchengines.isfdb;

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

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

import com.hardbacknutter.nevertoomanybooks.core.network.CredentialsException;
import com.hardbacknutter.nevertoomanybooks.core.parsers.DateParser;
import com.hardbacknutter.nevertoomanybooks.core.parsers.PartialDateParser;
import com.hardbacknutter.nevertoomanybooks.core.storage.StorageException;
import com.hardbacknutter.nevertoomanybooks.core.tasks.Cancellable;
import com.hardbacknutter.nevertoomanybooks.core.utils.PartialDate;
import com.hardbacknutter.nevertoomanybooks.entities.Author;
import com.hardbacknutter.nevertoomanybooks.entities.Identifier;
import com.hardbacknutter.nevertoomanybooks.searchengines.AuthorResolver;
import com.hardbacknutter.nevertoomanybooks.searchengines.AuthorResolverHelper;
import com.hardbacknutter.nevertoomanybooks.searchengines.EngineId;
import com.hardbacknutter.nevertoomanybooks.searchengines.SearchEngine;
import com.hardbacknutter.nevertoomanybooks.searchengines.SearchException;
import com.hardbacknutter.util.logger.LoggerFactory;

import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.select.Elements;

/**
 * Available fields.
 * - Birthdate
 * - Deathdate
 * - picture
 * - Birthplace
 * - Language
 * - Webpages
 */
public final class IsfdbAuthorResolver
        implements AuthorResolver {

    private static final String TAG = "IsfdbAuthorResolver";
    // part of a url
    private static final String EA_CGI = "ea.cgi";

    @NonNull
    private final IsfdbSearchEngine searchEngine;
    @NonNull
    private final String authorSearchUrl;
    private final DateParser<PartialDate> dateParser = new PartialDateParser();

    /**
     * Private Constructor.
     *
     * @param context      Current context
     * @param searchEngine the engine
     */
    @VisibleForTesting
    IsfdbAuthorResolver(@NonNull final Context context,
                        @NonNull final IsfdbSearchEngine searchEngine) {
        this.searchEngine = searchEngine;

        authorSearchUrl = this.searchEngine.getHostUrl()
                          + "/cgi-bin/se.cgi?arg=%s&type=Name";
    }

    /**
     * Private Constructor.
     *
     * @param context Current context
     * @param caller  a {@link Cancellable} which can forward requests
     *                to the (internal) {@link IsfdbSearchEngine}
     */
    private IsfdbAuthorResolver(@NonNull final Context context,
                                @Nullable final Cancellable caller) {
        this(context, (IsfdbSearchEngine) EngineId.Isfdb.createSearchEngine(context));
        searchEngine.setCaller(caller);
    }

    /**
     * Constructor.
     *
     * @param context      Current context
     * @param searchEngine the engine which is requesting this resolver
     *
     * @return new instance
     */
    @NonNull
    public static List<AuthorResolver> create(@NonNull final Context context,
                                              @NonNull final SearchEngine searchEngine) {

        if (AuthorResolverHelper.isEnabled(EngineId.Isfdb)) {
            final AuthorResolver ar;
            if (searchEngine instanceof IsfdbSearchEngine) {
                ar = new IsfdbAuthorResolver(context, (IsfdbSearchEngine) searchEngine);
            } else {
                ar = new IsfdbAuthorResolver(context, searchEngine);
            }
            return List.of(ar);
        }
        return List.of();
    }

    @Override
    public boolean resolve(@NonNull final Context context,
                           @NonNull final Author author)
            throws SearchException, CredentialsException {

        final Author found;
        final Optional<String> oIv = author.getIdentifierValue(Identifier.SID_ISFDB);
        if (oIv.isPresent()) {
            found = searchBySid(context, oIv.get());
            if (found != null) {
                boolean modified = author.merge(found, true);
                if (author.isSameName(found) && !author.isIdenticalName(found)) {
                    // correct diacritics difference
                    author.setName(found.getFamilyName(), found.getGivenNames());
                    modified = true;
                }
                return modified;
            }
        } else {
            found = searchByName(context, author);
            // 2025-05-10: insist on case-sensitive name equality for now.
            // If this proves problematic, we'll change it later...
            if (found != null && author.isSameName(found)) {
                return author.merge(found, true);
            }
        }

        return false;
    }

    @Nullable
    private Author searchBySid(@NonNull final Context context,
                               @NonNull final String sid)
            throws SearchException, CredentialsException {
        // the user can delete it...
        final String url = String.format(IsfdbSearchEngine.AUTHOR_URL, sid);
        final Document document = searchEngine.loadDocument(context, url, null);
        if (!searchEngine.isCancelled()) {
            // sanity check, we SHOULD always have a single result page
            final String pageUrl = document.location();
            if (pageUrl.contains(EA_CGI)) {
                return parse(context, document, sid);
            } else {
                LoggerFactory.getLogger().w(TAG, "parse|pageUrl=" + pageUrl);
            }
        }
        return null;
    }


    @Nullable
    private Author searchByName(@NonNull final Context context,
                                @NonNull final Author author)
            throws SearchException, CredentialsException {

        final String formattedName = author.getFormattedName(true);
        final String url = String.format(authorSearchUrl, formattedName);
        final Document document = searchEngine.loadDocument(context, url, null);
        if (!searchEngine.isCancelled()) {
            final String pageUrl = document.location();
            if (pageUrl.contains(EA_CGI)) {
                final String sid = getIdFromUrl(pageUrl);
                if (sid != null) {
                    return parse(context, document, sid);
                }
            } else if (pageUrl.contains("se.cgi")) {
                return parseMultiResult(context, document, formattedName);
            } else {
                // dunno, let's log it
                LoggerFactory.getLogger().w(TAG, "parse|pageUrl=" + pageUrl);
            }
        }
        return null;
    }

    @Nullable
    private Author parseMultiResult(@NonNull final Context context,
                                    @NonNull final Document document,
                                    @NonNull final String formattedName)
            throws SearchException, CredentialsException {
        // <a href="https://www.isfdb.org/cgi-bin/ea.cgi?276149" dir="ltr">Брайан Олдисс</a>
        final Elements aas = document.select("a[href^='https://www.isfdb.org/cgi-bin/ea.cgi']");
        // try to find an exact match
        final Optional<String> url = aas.stream()
                                        .filter(a -> a.text().equals(formattedName))
                                        .map(a -> a.attr("href"))
                                        .findFirst();
        if (url.isPresent()) {
            final String sid = getIdFromUrl(url.get());
            if (sid != null) {
                return searchBySid(context, sid);
            }
        }
        return null;
    }

    /**
     * Parse the document.
     * This method will detect any "Alternate Name" pages automatically and use those
     * as the 'real' author.
     * <p>
     * Dev. Note: following the "Alternate Name" has two outcomes:
     * <ul>
     *     <li>If this is a pen-name, then we'll get the real-author. GOOD</li>
     *     <li>If this is a variant of the name, e.g. "Brian Aldiss",
     *         we end up with "Brian W. Aldiss" as the real-author.
     *         Technically this is 'good', but users might find this annoying (including myself)
     *         but there is not much we can do about it.
     *         We could add yet another option in the settings to let the user
     *         decide: redirect as the real-author, or overwrite.
     *         But that would complicate things even more for actual pen-name handling.
     *         Bottom-line: the site does not differentiate between an "alternative name"
     *         and a "pen-name".
     *         2025-05-21: Leaving as-is for now.
     *     </li>
     * </ul>
     *
     * @param context  Current context
     * @param document to parse
     * @param sid      of the author we searched for
     *
     * @return author, or {@code null} on failure
     *
     * @throws SearchException      on generic exceptions (wrapped) during search
     * @throws CredentialsException on authentication/login failures
     */
    @VisibleForTesting
    @Nullable
    Author parse(@NonNull final Context context,
                 @NonNull final Document document,
                 @NonNull final String sid)
            throws SearchException, CredentialsException {
        final Element root = document.selectFirst("div.ContentBox");
        if (root == null) {
            return null;
        }

        Element element;

        // We are disregarding the "b:contains(Legal Name:)".
        // This typically spells out any (middle) initials which
        // do not appear on the books.
        // While nice to have, it complicates name comparison far too much.
        // TODO: add a specific author field for the full/legal name
        element = root.selectFirst("b:contains(Author:)");
        if (element == null) {
            return null;
        }

        final Node node = element.nextSibling();
        if (node == null) {
            return null;
        }
        final Author author = Author.from(node.toString().strip());
        author.setIdentifierValue(Identifier.SID_ISFDB, sid);

        // test for an "Alternate Name" or a "real name" result.
        final Element altNameElement = root.selectFirst(
                "b:contains(Used As Alternate Name By:)");
        if (altNameElement != null) {
            // we're on an "Alternate Name." result page
            element = altNameElement.nextElementSibling();
            // extract the sid of the 'real' author name
            if (element != null && "a".equals(element.tag().getName())) {
                final String realAuthorSid = getIdFromUrl(element.attr("href"));
                if (realAuthorSid != null) {
                    // and follow the link to get the real author details
                    final Author realAuthor = searchBySid(context, realAuthorSid);
                    if (realAuthor != null) {
                        author.setRealAuthor(realAuthor);
                    }
                }
            }
        } else {
            // we're on a "real name" result page.
            parseExtraData(context, root, author, sid);
        }

        return author;
    }

    @Nullable
    private String getIdFromUrl(@Nullable final String url) {
        if (url == null) {
            return null;
        }
        final String[] split = url.split("\\?");
        if (split.length > 1) {
            final String sid = split[1];
            if (!sid.isEmpty()) {
                return sid;
            }
        }
        return null;
    }

    private void parseExtraData(@NonNull final Context context,
                                @NonNull final Element root,
                                @NonNull final Author author,
                                @NonNull final String sid) {

        String birthPlace = null;

        // 2025-06-03: if the image is hosted elsewhere, e.g. Fantascienza,
        // the download might fail due to that site blocking direct url requests.
        // There are too many 3rd party sites to try fixing this.
        // Only direct ISFDB downloads are considered "supported"
        final Element image = root.selectFirst("img[alt='Author Picture']");
        if (image != null) {
            final String imageUrl = image.attr("src");
            try {
                searchEngine.saveImage(context, imageUrl, null, sid, 0, null)
                            .ifPresent(author::setTmpPictureFileSpec);
            } catch (@NonNull final StorageException ignore) {
                // ignore
            }
        }

        final Elements lis = root.select("li");
        for (final Element li : lis) {
            final Element label = li.selectFirst("b");
            if (label != null) {
                final String lblText = label.text();
                switch (lblText) {
                    case "Birthplace:": {
                        final Node node = label.nextSibling();
                        if (node != null) {
                            birthPlace = node.toString().strip();
                        }
                        break;
                    }
                    case "Birthdate:": {
                        final Node node = label.nextSibling();
                        if (node != null) {
                            dateParser.parse(node.toString().strip())
                                      .map(PartialDate::getIsoString)
                                      .ifPresent(author::setBirthDate);
                        }
                        break;
                    }
                    case "Deathdate:": {
                        final Node node = label.nextSibling();
                        if (node != null) {
                            dateParser.parse(node.toString().strip())
                                      .map(PartialDate::getIsoString)
                                      .ifPresent(author::setDeathDate);
                        }
                        break;
                    }
                    case "Language:":
                        break;
                    case "Webpages:":
                        break;
                    case "Used These Alternate Names:":
                        break;
                    case "Note:":
                        break;
                    case "Additional Bibliographic Comments:":
                        break;
                    case "Author Tags:":
                        // these come from the books, skip them.
                        break;
                }
            }
        }
    }
}
