// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "lite_connection_service.h"

#include <algorithm>
#include <cctype>
#include <utility>

namespace dragonx {
namespace wallet {

namespace {

bool startsWith(const std::string& value, const char* prefix)
{
    const std::string prefixValue(prefix);
    return value.size() >= prefixValue.size() &&
           value.compare(0, prefixValue.size(), prefixValue) == 0;
}

LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint)
{
    LiteServerEndpoint normalized = endpoint;
    normalized.url = liteTrimCopy(normalized.url);
    return normalized;
}

std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
    const LiteConnectionSettings& settings)
{
    std::vector<std::pair<std::size_t, LiteServerEndpoint>> servers;
    for (std::size_t index = 0; index < settings.servers.size(); ++index) {
        auto endpoint = trimmedEndpoint(settings.servers[index]);
        if (!endpoint.enabled || !isLiteServerUrlUsable(endpoint.url)) continue;
        servers.push_back({index, std::move(endpoint)});
    }
    return servers;
}

std::string normalizedChainName(const std::string& chainName)
{
    const std::string normalized = liteTrimCopy(chainName);
    return normalized.empty() ? kDragonXLiteChainName : normalized;
}

} // namespace

std::string liteTrimCopy(const std::string& value)
{
    auto begin = value.begin();
    while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;

    auto end = value.end();
    while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;

    return std::string(begin, end);
}

std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
{
    return {
        {"https://lite.dragonx.is", "DragonX Lite", true},
        {"https://lite1.dragonx.is", "DragonX Lite 1", true},
        {"https://lite2.dragonx.is", "DragonX Lite 2", true},
        {"https://lite3.dragonx.is", "DragonX Lite 3", true},
        {"https://lite4.dragonx.is", "DragonX Lite 4", true},
        {"https://lite5.dragonx.is", "DragonX Lite 5", true},
        {"https://lite6.dragonx.is", "DragonX Lite 6", true},
        {"https://lite7.dragonx.is", "DragonX Lite 7", true}
    };
}

LiteConnectionSettings defaultLiteConnectionSettings()
{
    LiteConnectionSettings settings;
    settings.servers = defaultDragonXLiteServers();
    settings.selectionMode = LiteServerSelectionMode::Sticky;
    settings.stickyServerUrl = kDefaultDragonXLiteServer;
    settings.chainName = kDragonXLiteChainName;
    settings.randomSelectionSeed = 0;
    return settings;
}

// Strict dotted-decimal check for the 127.0.0.0/8 loopback block: exactly four numeric octets
// (each 0-255) with the first equal to 127. This must NOT be a prefix match — "127.0.0.1.evil.com"
// and "127.evil.com" are attacker-controlled DNS names that a startsWith("127.") test would wrongly
// treat as loopback, reopening the plaintext-downgrade hole.
static bool isNumericIpv4Loopback(const std::string& h)
{
    int parts = 0;
    size_t start = 0;
    while (true) {
        const size_t dot = h.find('.', start);
        const std::string seg = h.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
        if (seg.empty() || seg.size() > 3) return false;
        int v = 0;
        for (char c : seg) { if (c < '0' || c > '9') return false; v = v * 10 + (c - '0'); }
        if (v > 255) return false;
        if (parts == 0 && v != 127) return false;   // 127.0.0.0/8 only
        ++parts;
        if (dot == std::string::npos) break;
        start = dot + 1;
    }
    return parts == 4;
}

static bool isLoopbackLiteHostSpec(const std::string& hostPort)
{
    std::string h = hostPort;
    const size_t term = h.find_first_of("/?#");   // strip path/query/fragment
    if (term != std::string::npos) h = h.substr(0, term);
    const size_t at = h.rfind('@');               // strip userinfo (user:pass@host)
    if (at != std::string::npos) h = h.substr(at + 1);
    if (!h.empty() && h.front() == '[') {         // [::1]:port  (bracketed IPv6)
        const size_t close = h.find(']');
        h = (close == std::string::npos) ? h : h.substr(1, close - 1);
    } else {                                      // host or host:port
        const size_t colon = h.find(':');
        if (colon != std::string::npos) h = h.substr(0, colon);
    }
    return h == "localhost" || h == "::1" || isNumericIpv4Loopback(h);
}

bool isLiteServerUrlUsable(const std::string& serverUrl)
{
    const std::string normalized = liteTrimCopy(serverUrl);
    if (startsWith(normalized, "https://")) return true;
    // SECURITY: plaintext http:// is a TLS downgrade for lightwalletd traffic (view keys,
    // transactions, addresses). Permit it only for loopback (a local dev lightwalletd);
    // reject remote plaintext servers instead of silently accepting the downgrade.
    if (startsWith(normalized, "http://"))
        return isLoopbackLiteHostSpec(normalized.substr(sizeof("http://") - 1));
    return false;
}

bool isOfficialLiteServer(const std::string& serverUrl)
{
    const std::string url = liteTrimCopy(serverUrl);
    for (const auto& s : defaultDragonXLiteServers())
        if (s.url == url) return true;
    return false;
}

const char* liteServerSelectionModeName(LiteServerSelectionMode mode)
{
    switch (mode) {
    case LiteServerSelectionMode::Sticky:
        return "Sticky";
    case LiteServerSelectionMode::Random:
        return "Random";
    }
    return "Unknown";
}

const char* liteConnectionAvailabilityName(LiteConnectionAvailability availability)
{
    switch (availability) {
    case LiteConnectionAvailability::Ready:
        return "Ready";
    case LiteConnectionAvailability::UnsupportedBuild:
        return "UnsupportedBuild";
    case LiteConnectionAvailability::BackendUnavailable:
        return "BackendUnavailable";
    case LiteConnectionAvailability::BridgeUnavailable:
        return "BridgeUnavailable";
    case LiteConnectionAvailability::NoUsableServer:
        return "NoUsableServer";
    }
    return "Unknown";
}

LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
{
    auto usableServers = usableConfiguredServers(settings);
    const std::string stickyServerUrl = liteTrimCopy(settings.stickyServerUrl);

    if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
        isLiteServerUrlUsable(stickyServerUrl)) {
        auto match = std::find_if(usableServers.begin(), usableServers.end(), [&](const auto& candidate) {
            return candidate.second.url == stickyServerUrl;
        });
        if (match != usableServers.end()) {
            return LiteServerSelectionResult{true, match->second, match->first, false, {}};
        }
        return LiteServerSelectionResult{
            true,
            LiteServerEndpoint{stickyServerUrl, "Custom", true},
            0,
            true,
            {}
        };
    }

    if (usableServers.empty()) {
        return LiteServerSelectionResult{false, {}, 0, false, "no usable lite servers are configured"};
    }

    if (settings.selectionMode == LiteServerSelectionMode::Random) {
        const std::size_t selectedIndex = settings.randomSelectionSeed % usableServers.size();
        return LiteServerSelectionResult{
            true,
            usableServers[selectedIndex].second,
            usableServers[selectedIndex].first,
            false,
            {}
        };
    }

    return LiteServerSelectionResult{true, usableServers.front().second, usableServers.front().first, false, {}};
}

LiteConnectionService::LiteConnectionService(WalletCapabilities capabilities,
                                             LiteConnectionSettings settings,
                                             LiteClientBridge bridge)
    : capabilities_(capabilities), settings_(std::move(settings)), bridge_(std::move(bridge))
{
}

LiteConnectionAvailability LiteConnectionService::availability() const
{
    if (!isLiteBuild(capabilities_)) return LiteConnectionAvailability::UnsupportedBuild;
    if (!supportsLiteBackend(capabilities_)) return LiteConnectionAvailability::BackendUnavailable;
    if (!bridge_.available()) return LiteConnectionAvailability::BridgeUnavailable;
    if (!selectedServer().ok) return LiteConnectionAvailability::NoUsableServer;
    return LiteConnectionAvailability::Ready;
}

WalletBackendStatus LiteConnectionService::status() const
{
    const auto currentAvailability = availability();
    if (currentAvailability == LiteConnectionAvailability::NoUsableServer) {
        return statusFor(currentAvailability, selectedServer().error);
    }
    return statusFor(currentAvailability);
}

LiteServerSelectionResult LiteConnectionService::selectedServer() const
{
    return selectLiteServer(settings_);
}

LiteWalletExistsRequest LiteConnectionService::walletExistsRequest() const
{
    return LiteWalletExistsRequest{normalizedChainName(settings_.chainName)};
}

LiteServerHealthRequest LiteConnectionService::serverHealthRequest() const
{
    auto selection = selectedServer();
    if (!selection.ok) return {};
    return LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
}

LiteWalletExistsCheckResult LiteConnectionService::checkWalletExists()
{
    LiteWalletExistsCheckResult result;
    result.request = walletExistsRequest();

    const auto readiness = bridgeReadinessStatus();
    if (readiness.state != WalletBackendState::Disconnected) {
        result.status = readiness;
        result.error = readiness.message;
        return result;
    }
    if (result.request.chainName.empty()) {
        result.status = statusFor(LiteConnectionAvailability::BackendUnavailable, "lite chain name is empty");
        result.error = result.status.message;
        return result;
    }

    result.attempted = true;
    result.walletExists = bridge_.walletExists(result.request.chainName);
    result.ok = true;
    result.status = readiness;
    return result;
}

LiteServerHealthCheckResult LiteConnectionService::checkSelectedServerHealth()
{
    LiteServerHealthCheckResult result;
    result.request = serverHealthRequest();

    const auto readiness = bridgeReadinessStatus();
    if (readiness.state != WalletBackendState::Disconnected) {
        result.status = readiness;
        result.error = readiness.message;
        return result;
    }

    auto selection = selectedServer();
    if (!selection.ok) {
        result.status = statusFor(LiteConnectionAvailability::NoUsableServer, selection.error);
        result.error = result.status.message;
        return result;
    }

    result.request = LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
    result.attempted = true;
    result.serverOnline = bridge_.checkServerOnline(result.request.server.url);
    result.ok = true;
    result.status = readiness;
    return result;
}

WalletBackendStatus LiteConnectionService::statusFor(LiteConnectionAvailability availability,
                                                     const std::string& detail) const
{
    switch (availability) {
    case LiteConnectionAvailability::Ready:
        return WalletBackendStatus{
            WalletBackendState::Disconnected,
            detail.empty() ? "lite connection scaffold ready; wallet lifecycle is not implemented" : detail,
            {},
            {},
            0.0
        };
    case LiteConnectionAvailability::UnsupportedBuild:
        return WalletBackendStatus{
            WalletBackendState::Unavailable,
            "lite connection service is unsupported in full-node builds",
            {},
            {},
            0.0
        };
    case LiteConnectionAvailability::BackendUnavailable:
        return WalletBackendStatus{
            WalletBackendState::Unavailable,
            detail.empty() ? "lite backend is not linked" : detail,
            {},
            {},
            0.0
        };
    case LiteConnectionAvailability::BridgeUnavailable:
        return WalletBackendStatus{
            WalletBackendState::Unavailable,
            detail.empty() ? bridge_.unavailableReason() : detail,
            {},
            {},
            0.0
        };
    case LiteConnectionAvailability::NoUsableServer:
        return WalletBackendStatus{
            WalletBackendState::Error,
            detail.empty() ? "no usable lite servers are configured" : detail,
            {},
            {},
            0.0
        };
    }

    return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite connection state", {}, {}, 0.0};
}

WalletBackendStatus LiteConnectionService::bridgeReadinessStatus() const
{
    if (!isLiteBuild(capabilities_)) return statusFor(LiteConnectionAvailability::UnsupportedBuild);
    if (!supportsLiteBackend(capabilities_)) return statusFor(LiteConnectionAvailability::BackendUnavailable);
    if (!bridge_.available()) return statusFor(LiteConnectionAvailability::BridgeUnavailable);
    return statusFor(LiteConnectionAvailability::Ready);
}

} // namespace wallet
} // namespace dragonx