#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"

#include "ccm/util/Rfc3986.hpp"

#include <nlohmann/json.hpp>

#include <cctype>
#include <string>
#include <unordered_set>

namespace ccm {

namespace {

std::string trim(std::string s) {
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front()))) {
        s.erase(s.begin());
    }
    while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back()))) {
        s.pop_back();
    }
    return s;
}

std::string asciiLower(std::string s) {
    for (char& ch : s) {
        ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
    }
    return s;
}

std::string stripLeadingZeros(std::string_view s) {
    std::size_t i = 0;
    while (i + 1 < s.size() && s[i] == '0') ++i;
    return std::string(s.substr(i));
}

bool localIdsMatch(std::string_view a, std::string_view b) {
    if (a == b) return true;
    return stripLeadingZeros(a) == stripLeadingZeros(b);
}

bool catalogPrintMatchesRow(const JapanesePokemonPrintEnInfo& print,
                            const JapanesePokemonCardPreviewSource::SetCardRow& row) {
    // Reject stale catalog rows whose Japanese name disagrees with TCGdex.
    // Seed data historically mapped Charmander→001 / Charizard→004; those
    // localIds are Bulbasaur / Weedle on PMCG1.
    if (print.nameJa.empty()) return true;
    return asciiLower(print.nameJa) == asciiLower(row.nameJa);
}

bool nameMatchesRow(std::string_view wantedLower,
                    const JapanesePokemonCardPreviewSource::SetCardRow& row,
                    std::string_view setId,
                    const JapanesePokemonEnCatalog& catalog) {
    if (wantedLower.empty()) return true;
    if (asciiLower(row.nameJa) == wantedLower) return true;
    if (auto print = catalog.findPrint(setId, row.localId)) {
        if (!catalogPrintMatchesRow(*print, row)) return false;
        if (asciiLower(print->nameEn) == wantedLower) return true;
        if (asciiLower(print->nameJa) == wantedLower) return true;
    }
    return false;
}

}  // namespace

JapanesePokemonCardPreviewSource::JapanesePokemonCardPreviewSource(
    IHttpClient& http, const JapanesePokemonEnCatalog& catalog)
    : http_(http), catalog_(catalog) {}

std::string JapanesePokemonCardPreviewSource::normalizeLocalId(std::string_view setNo) {
    std::string s = trim(std::string(setNo));
    const auto slash = s.find('/');
    if (slash != std::string::npos) s.erase(slash);
    return s;
}

std::string JapanesePokemonCardPreviewSource::buildSetDetailUrl(std::string_view setId) {
    return std::string("https://api.tcgdex.net/v2/ja/sets/") +
           rfc3986PercentEncode(setId);
}

std::string JapanesePokemonCardPreviewSource::buildCardUrl(std::string_view setId,
                                                           std::string_view localId) {
    std::string id = std::string(setId) + "-" + std::string(localId);
    return std::string("https://api.tcgdex.net/v2/ja/cards/") +
           rfc3986PercentEncode(id);
}

std::string JapanesePokemonCardPreviewSource::imageUrlFromBase(std::string_view imageBase) {
    if (imageBase.empty()) return {};
    std::string url(imageBase);
    while (!url.empty() && (url.back() == '/' || url.back() == ' ')) url.pop_back();
    return url + "/high.png";
}

Result<std::vector<JapanesePokemonCardPreviewSource::SetCardRow>, PreviewLookupError>
JapanesePokemonCardPreviewSource::parseSetCards(const std::string& body) {
    using R = Result<std::vector<SetCardRow>, PreviewLookupError>;
    using K = PreviewLookupError::Kind;
    try {
        const auto j = nlohmann::json::parse(body);
        if (!j.is_object() || !j.contains("cards") || !j.at("cards").is_array()) {
            return R::err({K::Transient,
                           "TCGdex JA set detail missing 'cards' array."});
        }
        std::vector<SetCardRow> out;
        out.reserve(j.at("cards").size());
        for (const auto& card : j.at("cards")) {
            SetCardRow row;
            row.localId = card.value("localId", "");
            if (row.localId.empty() && card.contains("id") && card.at("id").is_string()) {
                // Fallback: take suffix after last '-' from card id.
                const std::string id = card.at("id").get<std::string>();
                const auto dash = id.rfind('-');
                if (dash != std::string::npos) row.localId = id.substr(dash + 1);
            }
            row.nameJa = card.value("name", "");
            row.rarity = card.value("rarity", "");
            if (card.contains("image") && card.at("image").is_string()) {
                row.imageBase = card.at("image").get<std::string>();
            }
            if (row.localId.empty()) continue;
            out.push_back(std::move(row));
        }
        return R::ok(std::move(out));
    } catch (const std::exception& e) {
        return R::err({K::Transient,
                       std::string("TCGdex JA set detail JSON parse error: ") + e.what()});
    }
}

Result<std::string, PreviewLookupError>
JapanesePokemonCardPreviewSource::parseCardImageUrl(const std::string& body) {
    using R = Result<std::string, PreviewLookupError>;
    using K = PreviewLookupError::Kind;
    try {
        const auto j = nlohmann::json::parse(body);
        if (!j.is_object()) {
            return R::err({K::Transient, "TCGdex JA card response is not a JSON object."});
        }
        if (!j.contains("image") || j.at("image").is_null()) {
            return R::err({K::NotFound, "TCGdex JA card has no image."});
        }
        if (!j.at("image").is_string()) {
            return R::err({K::Transient, "TCGdex JA card image field is not a string."});
        }
        const std::string base = j.at("image").get<std::string>();
        if (base.empty()) {
            return R::err({K::NotFound, "TCGdex JA card has no image."});
        }
        return R::ok(imageUrlFromBase(base));
    } catch (const std::exception& e) {
        return R::err({K::Transient,
                       std::string("TCGdex JA card JSON parse error: ") + e.what()});
    }
}

Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::parsePrintVariants(
    const std::string& body,
    std::string_view setId,
    std::string_view wantedCardName,
    const JapanesePokemonEnCatalog& catalog) {
    using R = Result<std::vector<AutoDetectedPrint>>;
    auto rows = parseSetCards(body);
    if (!rows) {
        return R::err(rows.error().message);
    }

    const std::string wantedLower = asciiLower(trim(std::string(wantedCardName)));
    std::vector<AutoDetectedPrint> out;
    std::unordered_set<std::string> seen;
    std::unordered_set<std::string> seenCatalogUrls;

    // Prefer catalog EN matches first so typed English names resolve — but
    // only when the catalog localId exists in the set and name_ja agrees
    // with TCGdex (guards against stale seed mappings).
    std::vector<AutoDetectedPrint> withPreview;
    std::vector<AutoDetectedPrint> withoutPreview;
    if (!wantedLower.empty()) {
        for (const auto& p : catalog.findPrintsByName(setId, wantedCardName)) {
            const SetCardRow* row = nullptr;
            for (const auto& r : rows.value()) {
                if (localIdsMatch(r.localId, p.localId)) {
                    row = &r;
                    break;
                }
            }
            const std::string previewUrl =
                JapanesePokemonEnCatalog::previewImageUrlFromPrint(p);
            if (row == nullptr) {
                // Set detail sometimes omits cards[]; keep catalog-only hits
                // (UnnumberedPromo). Dedupe only among non-empty preview URLs
                // so empty-image prints still appear in the Next ring.
                if (!rows.value().empty()) continue;
                if (!previewUrl.empty() &&
                    !seenCatalogUrls.insert(previewUrl).second) {
                    continue;
                }
            } else if (!catalogPrintMatchesRow(p, *row)) {
                continue;
            }
            if (!seen.insert(p.localId).second) continue;
            AutoDetectedPrint print;
            print.setNo = p.localId;
            if (row != nullptr) print.rarity = row->rarity;
            if (previewUrl.empty()) {
                withoutPreview.push_back(std::move(print));
            } else {
                withPreview.push_back(std::move(print));
            }
        }
    }

    for (auto& print : withPreview) out.push_back(std::move(print));
    for (auto& print : withoutPreview) out.push_back(std::move(print));

    for (const auto& row : rows.value()) {
        if (!nameMatchesRow(wantedLower, row, setId, catalog)) continue;
        if (!seen.insert(row.localId).second) continue;
        AutoDetectedPrint print;
        print.setNo = row.localId;
        print.rarity = row.rarity;
        out.push_back(std::move(print));
    }

    if (out.empty() && !wantedLower.empty()) {
        return R::err("No matching Japanese Pokemon prints for that name in the set.");
    }
    return R::ok(std::move(out));
}

Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectPrintVariantsFromCatalog(
    std::string_view setId,
    std::string_view wantedCardName,
    const JapanesePokemonEnCatalog& catalog) {
    using R = Result<std::vector<AutoDetectedPrint>>;
    if (!catalog.hasPrintsForSet(setId)) {
        return R::err("No matching Japanese Pokemon prints for that name in the set.");
    }
    const std::string wantedLower = asciiLower(trim(std::string(wantedCardName)));
    std::vector<AutoDetectedPrint> out;
    std::unordered_set<std::string> seen;
    std::unordered_set<std::string> seenUrls;
    if (wantedLower.empty()) {
        return R::ok(std::move(out));
    }
    std::vector<AutoDetectedPrint> withPreview;
    std::vector<AutoDetectedPrint> withoutPreview;
    for (const auto& p : catalog.findPrintsByName(setId, wantedCardName)) {
        if (!seen.insert(p.localId).second) continue;
        // Dedupe only among non-empty preview URLs so identical art is not
        // cycled; empty-image prints still join the Next ring (card-back).
        // Emit imaged prints first so Auto-detect lands on real art.
        const std::string previewUrl =
            JapanesePokemonEnCatalog::previewImageUrlFromPrint(p);
        if (!previewUrl.empty() && !seenUrls.insert(previewUrl).second) {
            continue;
        }
        AutoDetectedPrint print;
        print.setNo = p.localId;
        if (previewUrl.empty()) {
            withoutPreview.push_back(std::move(print));
        } else {
            withPreview.push_back(std::move(print));
        }
    }
    for (auto& print : withPreview) out.push_back(std::move(print));
    for (auto& print : withoutPreview) out.push_back(std::move(print));
    if (out.empty()) {
        return R::err("No matching Japanese Pokemon prints for that name in the set.");
    }
    return R::ok(std::move(out));
}

Result<std::string, PreviewLookupError>
JapanesePokemonCardPreviewSource::fetchImageUrl(std::string_view name,
                                                std::string_view setId,
                                                std::string_view setNo) {
    using R = Result<std::string, PreviewLookupError>;
    using K = PreviewLookupError::Kind;

    const std::string localId = normalizeLocalId(setNo);
    if (setId.empty()) {
        return R::err({K::NotFound, "Japanese Pokemon preview requires a set id."});
    }

    auto catalogPreviewFor = [&](std::string_view lid) -> Result<std::string, PreviewLookupError> {
        if (lid.empty()) return R::err({K::NotFound, "No catalog preview for print."});
        if (auto print = catalog_.findPrint(setId, lid)) {
            const std::string catalogUrl =
                JapanesePokemonEnCatalog::previewImageUrlFromPrint(*print);
            if (!catalogUrl.empty()) return R::ok(catalogUrl);
        }
        return R::err({K::NotFound, "TCGdex JA card has no image."});
    };

    // Prefer direct card fetch when we have a localId.
    if (!localId.empty()) {
        auto cardResp = http_.get(buildCardUrl(setId, localId));
        if (cardResp) {
            auto img = parseCardImageUrl(cardResp.value());
            if (img) return img;
            // NotFound from card object: fall through to set list / catalog.
            if (img.error().kind == K::Transient) return img;
        } else {
            // Synthetic / classic products: try catalog gap-fill before set detail.
            auto catalogImg = catalogPreviewFor(localId);
            if (catalogImg) return catalogImg;
            // Known catalog print with no preview URL: honest miss (do not
            // borrow a sibling print's art via name match).
            if (catalog_.findPrint(setId, localId)) {
                return R::err({K::NotFound, "TCGdex JA card has no image."});
            }
        }
    }

    auto setResp = http_.get(buildSetDetailUrl(setId));
    if (!setResp) {
        // Catalog-only products (City Gym theme decks, etc.) are not on TCGdex.
        if (!localId.empty()) {
            auto catalogImg = catalogPreviewFor(localId);
            if (catalogImg) return catalogImg;
            if (catalog_.findPrint(setId, localId)) {
                return R::err({K::NotFound, "TCGdex JA card has no image."});
            }
            // Network failure and no catalog entry: Transient so a brief outage
            // is not negative-cached as a permanent miss.
            return R::err({K::Transient, setResp.error()});
        }
        if (catalog_.hasPrintsForSet(setId)) {
            const std::string wantedLower = asciiLower(trim(std::string(name)));
            if (!wantedLower.empty()) {
                for (const auto& p : catalog_.findPrintsByName(setId, name)) {
                    auto catalogImg = catalogPreviewFor(p.localId);
                    if (catalogImg) return catalogImg;
                }
            }
            return R::err({K::NotFound, "No matching Japanese Pokemon card for preview."});
        }
        return R::err({K::Transient, setResp.error()});
    }
    auto rows = parseSetCards(setResp.value());
    if (!rows) return R::err(rows.error());

    const std::string wantedLower = asciiLower(trim(std::string(name)));
    const SetCardRow* chosen = nullptr;
    for (const auto& row : rows.value()) {
        if (!localId.empty() && localIdsMatch(row.localId, localId)) {
            chosen = &row;
            break;
        }
    }
    // Name match only when setNo was not provided — never borrow a sibling
    // print's art for a concrete localId.
    if (chosen == nullptr && localId.empty() && !wantedLower.empty()) {
        for (const auto& row : rows.value()) {
            if (nameMatchesRow(wantedLower, row, setId, catalog_)) {
                chosen = &row;
                break;
            }
        }
    }
    if (chosen == nullptr) {
        // Empty cards[] with catalog prints: resolve from catalog.
        if (rows.value().empty() && catalog_.hasPrintsForSet(setId)) {
            if (!localId.empty()) {
                auto catalogImg = catalogPreviewFor(localId);
                if (catalogImg) return catalogImg;
                if (catalog_.findPrint(setId, localId)) {
                    return R::err({K::NotFound, "TCGdex JA card has no image."});
                }
                return R::err({K::NotFound, "No matching Japanese Pokemon card for preview."});
            }
            if (!wantedLower.empty()) {
                for (const auto& p : catalog_.findPrintsByName(setId, name)) {
                    auto catalogImg = catalogPreviewFor(p.localId);
                    if (catalogImg) return catalogImg;
                }
            }
        }
        return R::err({K::NotFound, "No matching Japanese Pokemon card for preview."});
    }
    if (!chosen->imageBase.empty()) {
        return R::ok(imageUrlFromBase(chosen->imageBase));
    }

    // Try full card object — set résumé sometimes omits image.
    auto cardResp = http_.get(buildCardUrl(setId, chosen->localId));
    if (cardResp) {
        auto img = parseCardImageUrl(cardResp.value());
        if (img) return img;
        if (img.error().kind == K::Transient) return img;
    } else {
        // Odd localId padding can 404; still try catalog gap-fill below.
    }

    // Classic JA sets often have image:null on TCGdex. Prefer a catalog
    // printing-accurate TCGPlayer product image for this exact setId+localId
    // (never search other printings by Pokémon name).
    return catalogPreviewFor(chosen->localId);
}

Result<AutoDetectedPrint>
JapanesePokemonCardPreviewSource::detectFirstPrint(std::string_view name,
                                                   std::string_view setId) {
    auto variants = detectPrintVariants(name, setId);
    if (!variants) return Result<AutoDetectedPrint>::err(variants.error());
    if (variants.value().empty()) {
        return Result<AutoDetectedPrint>::err(
            "No matching Japanese Pokemon prints for that name in the set.");
    }
    return Result<AutoDetectedPrint>::ok(variants.value().front());
}

Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectPrintVariants(std::string_view name,
                                                      std::string_view setId) {
    if (setId.empty()) {
        return Result<std::vector<AutoDetectedPrint>>::err(
            "Select a set before auto-detecting Japanese Pokemon prints.");
    }
    auto setResp = http_.get(buildSetDetailUrl(setId));
    if (!setResp) {
        if (catalog_.hasPrintsForSet(setId)) {
            return detectPrintVariantsFromCatalog(setId, name, catalog_);
        }
        return Result<std::vector<AutoDetectedPrint>>::err(setResp.error());
    }
    auto parsed = parsePrintVariants(setResp.value(), setId, name, catalog_);
    if (parsed) return parsed;
    // Empty/unusable TCGdex detail: fall back to catalog prints when present.
    if (catalog_.hasPrintsForSet(setId)) {
        return detectPrintVariantsFromCatalog(setId, name, catalog_);
    }
    return parsed;
}

Result<AutoDetectedPrint> JapanesePokemonCardPreviewSource::parsePrintFromCardResponse(
    const std::string& body) {
    using R = Result<AutoDetectedPrint>;
    try {
        const auto j = nlohmann::json::parse(body);
        if (!j.is_object()) {
            return R::err("TCGdex JA card response is not a JSON object.");
        }
        AutoDetectedPrint print;
        print.name = trim(j.value("name", ""));
        print.setNo = normalizeLocalId(j.value("localId", ""));
        print.rarity = trim(j.value("rarity", ""));
        if (print.name.empty()) {
            return R::err("TCGdex JA card has no name.");
        }
        if (print.setNo.empty() && j.contains("id") && j.at("id").is_string()) {
            const std::string id = j.at("id").get<std::string>();
            const auto dash = id.rfind('-');
            if (dash != std::string::npos) {
                print.setNo = normalizeLocalId(id.substr(dash + 1));
            }
        }
        return R::ok(std::move(print));
    } catch (const std::exception& e) {
        return R::err(std::string("TCGdex JA card JSON parse error: ") + e.what());
    }
}

Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectVariantsBySetNoFromCatalog(
    std::string_view setId,
    std::string_view localId,
    const JapanesePokemonEnCatalog& catalog) {
    using R = Result<std::vector<AutoDetectedPrint>>;
    const std::string id = normalizeLocalId(localId);
    if (setId.empty()) return R::err("Select a set first.");
    if (id.empty()) return R::err("Card number is empty.");

    // Prefer exact key, then leading-zero-insensitive scan ("1" ↔ "001").
    auto found = catalog.findPrint(setId, id);
    if (!found) {
        for (const auto& print : catalog.printsForSet(setId)) {
            if (localIdsMatch(print.localId, id)) {
                found = print;
                break;
            }
        }
    }
    if (!found) {
        return R::err("Could not auto-detect card name from set number.");
    }
    AutoDetectedPrint print;
    print.name = !found->nameEn.empty() ? found->nameEn : found->nameJa;
    print.setNo = found->localId.empty() ? id : found->localId;
    if (print.name.empty()) {
        return R::err("Could not auto-detect card name from set number.");
    }
    std::vector<AutoDetectedPrint> out;
    out.push_back(std::move(print));
    return R::ok(std::move(out));
}

Result<AutoDetectedPrint> JapanesePokemonCardPreviewSource::detectBySetNo(
    std::string_view setId,
    std::string_view setNo) {
    auto list = detectVariantsBySetNo(setId, setNo);
    if (!list) return Result<AutoDetectedPrint>::err(list.error());
    if (list.value().empty()) {
        return Result<AutoDetectedPrint>::err(
            "Could not auto-detect card name from set number.");
    }
    return Result<AutoDetectedPrint>::ok(list.value().front());
}

Result<std::vector<AutoDetectedPrint>>
JapanesePokemonCardPreviewSource::detectVariantsBySetNo(std::string_view setId,
                                                        std::string_view setNo) {
    using R = Result<std::vector<AutoDetectedPrint>>;
    if (setId.empty()) return R::err("Select a set first.");
    const std::string id = normalizeLocalId(setNo);
    if (id.empty()) return R::err("Card number is empty.");

    auto cardResp = http_.get(buildCardUrl(setId, id));
    if (cardResp) {
        auto parsed = parsePrintFromCardResponse(cardResp.value());
        if (parsed && localIdsMatch(parsed.value().setNo, id)) {
            // Prefer EN catalog name when available (exact or zero-insensitive).
            if (auto cat = catalog_.findPrint(setId, id); cat && !cat->nameEn.empty()) {
                parsed.value().name = cat->nameEn;
            } else {
                for (const auto& p : catalog_.printsForSet(setId)) {
                    if (localIdsMatch(p.localId, id) && !p.nameEn.empty()) {
                        parsed.value().name = p.nameEn;
                        break;
                    }
                }
            }
            std::vector<AutoDetectedPrint> out;
            out.push_back(std::move(parsed).value());
            return R::ok(std::move(out));
        }
    }

    if (catalog_.hasPrintsForSet(setId)) {
        return detectVariantsBySetNoFromCatalog(setId, id, catalog_);
    }
    return R::err("Could not auto-detect card name from set number.");
}

}  // namespace ccm
