// Copyright 2026 Kushview, LLC <info@kushview.net>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "auth.hpp"

#include <element/settings.hpp>

#include <juce_cryptography/juce_cryptography.h>

#define AUTH_LOG(o)
// #define AUTH_LOG(o) Logger::writeToLog(o)

namespace element::auth {
using namespace juce;

static String base64UrlEncode (const void* data, size_t numBytes)
{
    MemoryOutputStream output;
    Base64::convertToBase64 (output, data, numBytes);
    auto encoded = output.toString().trim();
    encoded = encoded.replaceCharacter ('+', '-')
                  .replaceCharacter ('/', '_')
                  .upToLastOccurrenceOf ("=", false, false);
    while (encoded.endsWithChar ('='))
        encoded = encoded.dropLastCharacters (1);
    return encoded;
}

static String createCodeChallenge (const String& verifier, String& method)
{
    const auto utf8 = verifier.toUTF8();
    // sizeInBytes() includes the null terminator; drop it so we hash the same
    // bytes that a strlen-based digest would (i.e. the verifier text only).
    const SHA256 hash (utf8.getAddress(), (size_t) utf8.sizeInBytes() - 1);
    const auto digest = hash.getRawData();

    method = "S256";
    return base64UrlEncode (digest.getData(), digest.getSize());
}

static TokenResponse parseTokenPayload (const String& payload, const String& errorPrefix)
{
    TokenResponse response;

    const auto parsed = JSON::parse (payload);
    if (parsed.isVoid() || ! parsed.isObject())
    {
        response.error = errorPrefix + ": invalid JSON response";
        return response;
    }

    auto* object = parsed.getDynamicObject();
    if (object == nullptr)
    {
        response.error = errorPrefix + ": malformed response object";
        return response;
    }

    response.accessToken = object->getProperty ("access_token").toString().trim();
    response.refreshToken = object->getProperty ("refresh_token").toString().trim();
    response.expiresInSeconds = static_cast<int> (object->getProperty ("expires_in"));

    if (auto user = object->getProperty ("user"); user.isObject())
    {
        if (auto* userObj = user.getDynamicObject())
        {
            response.userEmail = userObj->getProperty ("email").toString().trim();
            response.userDisplay = userObj->getProperty ("display_name").toString().trim();
        }
    }

    if (response.userEmail.isEmpty())
        response.userEmail = object->getProperty ("email").toString().trim();
    if (response.userDisplay.isEmpty())
        response.userDisplay = object->getProperty ("display_name").toString().trim();
    if (response.userDisplay.isEmpty())
        response.userDisplay = response.userEmail;

    if (response.accessToken.isEmpty())
    {
        response.error = errorPrefix + ": access token missing in response";
        return response;
    }

    if (auto entitlements = object->getProperty ("entitlements"); entitlements.isObject())
        if (auto* ent = entitlements.getDynamicObject())
            response.previewUpdates = static_cast<bool> (ent->getProperty ("preview_updates"));

    response.success = true;
    return response;
}

String generateState()
{
    return Uuid().toString().replaceCharacters ("{}-", "");
}

String generateCodeVerifier()
{
    static constexpr auto alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
    static constexpr int alphabetLen = 66;
    static constexpr int verifierLength = 64;

    String verifier;
    verifier.preallocateBytes (verifierLength);

    auto& random = Random::getSystemRandom();
    for (int i = 0; i < verifierLength; ++i)
        verifier << String::charToString (alphabet[random.nextInt (alphabetLen)]);

    return verifier;
}

String buildAuthorizationURL (const String& state,
                              const String& codeVerifier)
{
    String challengeMethod;
    const auto codeChallenge = createCodeChallenge (codeVerifier, challengeMethod);

    String authUrl (authorizeEndpoint);
    authUrl << "?response_type=code"
            << "&client_id=" << URL::addEscapeChars (clientId, true)
            << "&redirect_uri=" << URL::addEscapeChars (redirectUri, true)
            << "&scope=" << URL::addEscapeChars ("basic", true)
            << "&state=" << URL::addEscapeChars (state, true)
            << "&code_challenge=" << URL::addEscapeChars (codeChallenge, true)
            << "&code_challenge_method=" << URL::addEscapeChars (challengeMethod, true);

    return authUrl;
}

bool storePendingPKCE (element::Settings& settings,
                       const String& state,
                       const String& verifier)
{
    if (auto* props = settings.getUserSettings())
    {
        props->setValue (pkceStateKey, state.trim());
        props->setValue (pkceVerifierKey, verifier.trim());
        return true;
    }

    return false;
}

StringPairArray parseQueryParameters (const String& urlString)
{
    StringPairArray parameters;
    const String query = urlString.fromFirstOccurrenceOf ("?", false, false);
    const StringArray pairs = StringArray::fromTokens (query, "&", "");
    for (const auto& pair : pairs)
    {
        const int equalsPos = pair.indexOfChar ('=');
        if (equalsPos <= 0)
            continue;

        const auto key = pair.substring (0, equalsPos);
        const auto value = URL::removeEscapeChars (pair.substring (equalsPos + 1));
        parameters.set (key, value);
    }

    return parameters;
}

TokenResponse exchangeAuthorizationCode (const String& authCode,
                                         const String& codeVerifier)
{
    TokenResponse response;
    if (authCode.isEmpty())
    {
        response.error = "Auth token exchange failed: missing authorization code";
        return response;
    }

    if (codeVerifier.isEmpty())
    {
        response.error = "Auth token exchange failed: missing PKCE code_verifier";
        return response;
    }

    auto body = String ("grant_type=authorization_code&code=");
    body << URL::addEscapeChars (authCode, true)
         << "&client_id=" << URL::addEscapeChars (clientId, true)
         << "&redirect_uri=" << URL::addEscapeChars (redirectUri, true)
         << "&code_verifier=" << URL::addEscapeChars (codeVerifier, true);

    URL tokenUrl (tokenEndpoint);
    tokenUrl = tokenUrl.withPOSTData (body);

    int statusCode = -1;
    StringPairArray responseHeaders;
    auto options = URL::InputStreamOptions (URL::ParameterHandling::inAddress)
                       .withHttpRequestCmd ("POST")
                       .withExtraHeaders ("Content-Type: application/x-www-form-urlencoded\r\n"
                                          "Accept: application/json\r\n")
                       .withConnectionTimeoutMs (15000)
                       .withStatusCode (&statusCode)
                       .withResponseHeaders (&responseHeaders);

    std::unique_ptr<InputStream> stream (tokenUrl.createInputStream (options));
    if (statusCode == -1 || stream == nullptr)
    {
        response.error = "Auth token exchange failed: unable to contact token endpoint";
        return response;
    }

    const auto payload = stream->readEntireStreamAsString().trim();

    if (statusCode < 200 || statusCode >= 300)
    {
        response.error = String ("Auth token exchange failed: HTTP ")
                         + String (statusCode)
                         + (payload.isNotEmpty() ? " " + payload : String());
        return response;
    }

    return parseTokenPayload (payload, "Auth token exchange failed");
}

TokenResponse refreshAccessToken (const String& refreshToken)
{
    TokenResponse response;
    if (refreshToken.isEmpty())
    {
        response.error = "Auth token refresh failed: missing refresh token";
        return response;
    }

    auto body = String ("grant_type=refresh_token&refresh_token=");
    body << URL::addEscapeChars (refreshToken, true)
         << "&client_id=" << URL::addEscapeChars (clientId, true);

    URL tokenUrl (refreshEndpoint);
    tokenUrl = tokenUrl.withPOSTData (body);

    int statusCode = -1;
    StringPairArray responseHeaders;
    auto options = URL::InputStreamOptions (URL::ParameterHandling::inAddress)
                       .withHttpRequestCmd ("POST")
                       .withExtraHeaders ("Content-Type: application/x-www-form-urlencoded\r\n"
                                          "Accept: application/json\r\n")
                       .withConnectionTimeoutMs (15000)
                       .withStatusCode (&statusCode)
                       .withResponseHeaders (&responseHeaders);

    std::unique_ptr<InputStream> stream (tokenUrl.createInputStream (options));
    if (statusCode == -1 || stream == nullptr)
    {
        response.error = "Auth token refresh failed: unable to contact token endpoint";
        return response;
    }

    const auto payload = stream->readEntireStreamAsString().trim();

    if (statusCode < 200 || statusCode >= 300)
    {
        response.error = String ("Auth token refresh failed: HTTP ")
                         + String (statusCode)
                         + (payload.isNotEmpty() ? " " + payload : String());
        return response;
    }

    return parseTokenPayload (payload, "Auth token refresh failed");
}

void persistTokens (element::Settings& settings, const TokenResponse& tokenResponse)
{
    if (! tokenResponse.success)
        return;

    settings.setUpdateKeyType ("member");
    settings.setUpdateKey (tokenResponse.accessToken);
    settings.setUpdateKeyUser (tokenResponse.userDisplay);
    settings.setAuthPreviewUpdates (tokenResponse.previewUpdates);

    if (auto* props = settings.getUserSettings())
        props->setValue (refreshTokenKey, tokenResponse.refreshToken);

    // Fetch a short-lived signed appcast URL and cache it so the updater
    // can point at the correct channel without needing a token at feed time.
    const auto appcastUrl = fetchSignedAppcastUrl (tokenResponse.accessToken);
    settings.setAuthAppcastUrl (appcastUrl);
}

bool consumePendingPKCE (element::Settings& settings, String& state, String& verifier)
{
    if (auto* props = settings.getUserSettings())
    {
        state = props->getValue (pkceStateKey).trim();
        verifier = props->getValue (pkceVerifierKey).trim();
        props->setValue (pkceStateKey, "");
        props->setValue (pkceVerifierKey, "");
        return state.isNotEmpty() && verifier.isNotEmpty();
    }

    return false;
}

String fetchSignedAppcastUrl (const String& accessToken)
{
    if (accessToken.isEmpty())
        return {};

#if JUCE_MAC
    constexpr const char* plat = "macos";
#elif JUCE_WINDOWS
    constexpr const char* plat = "windows";
#else
    constexpr const char* plat = "unknown";
#endif

    const String endpoint = String (apiBaseEndpoint) + "/appcast-url?plat=" + plat;
    URL url (endpoint);

    int statusCode = -1;
    auto options = URL::InputStreamOptions (URL::ParameterHandling::inAddress)
                       .withHttpRequestCmd ("GET")
                       .withExtraHeaders ("Authorization: Bearer " + accessToken + "\r\n"
                                                                                   "Accept: application/json\r\n")
                       .withConnectionTimeoutMs (10000)
                       .withStatusCode (&statusCode);

    std::unique_ptr<InputStream> stream (url.createInputStream (options));
    if (statusCode != 200 || stream == nullptr)
    {
        AUTH_LOG ("Auth: appcast-url request failed (HTTP " + String (statusCode) + ")");
        return {};
    }

    const auto body = stream->readEntireStreamAsString();
    auto json = JSON::parse (body);
    const auto appcastUrl = json["url"].toString();
    if (appcastUrl.isNotEmpty())
        AUTH_LOG ("Auth: fetched signed appcast URL");
    else
        AUTH_LOG ("Auth: appcast-url response missing 'url' field");

    return appcastUrl;
}

void revokeRefreshToken (const String& refreshToken)
{
    if (refreshToken.isEmpty())
        return;

    const String revokeEndpoint = String (apiBaseEndpoint) + "/token/revoke";
    auto body = String ("refresh_token=") + URL::addEscapeChars (refreshToken, true);

    URL url (revokeEndpoint);
    url = url.withPOSTData (body);

    int statusCode = -1;
    auto options = URL::InputStreamOptions (URL::ParameterHandling::inAddress)
                       .withHttpRequestCmd ("POST")
                       .withExtraHeaders ("Content-Type: application/x-www-form-urlencoded\r\n"
                                          "Accept: application/json\r\n")
                       .withConnectionTimeoutMs (10000)
                       .withStatusCode (&statusCode);

    std::unique_ptr<InputStream> stream (url.createInputStream (options));
    if (statusCode >= 200 && statusCode < 300)
        AUTH_LOG ("Auth: refresh token revoked successfully");
    else
        AUTH_LOG ("Auth: revoke request failed (HTTP " + String (statusCode) + ")");
}

bool isAppcastUrlExpired (const String& cachedUrl)
{
    if (cachedUrl.isEmpty())
        return true;
    const auto params = parseQueryParameters (cachedUrl);
    const auto expStr = params["exp"];
    if (expStr.isEmpty())
        return true;
    return expStr.getLargeIntValue() < (int64) (Time::currentTimeMillis() / 1000);
}

void maybeRefreshOnStartup (element::Settings& settings)
{
    const auto storedRefreshToken = [&]() -> String {
        if (auto* props = settings.getUserSettings())
            return props->getValue (refreshTokenKey).trim();
        return {};
    }();

    if (storedRefreshToken.isEmpty())
    {
        AUTH_LOG ("Auth: no stored refresh token, skipping startup refresh");
        return;
    }

    AUTH_LOG ("Auth: attempting token refresh on startup");
    const auto response = refreshAccessToken (storedRefreshToken);

    if (! response.success)
    {
        AUTH_LOG ("Auth: startup refresh failed: " + response.error);
        // Clear stale credentials so the UI reflects signed-out state.
        settings.setUpdateKey ({});
        settings.setUpdateKeyUser ({});
        settings.setUpdateKeyType ("element-v1");
        settings.setAuthPreviewUpdates (false);
        settings.setAuthAppcastUrl ({});
        if (auto* props = settings.getUserSettings())
        {
            props->setValue (refreshTokenKey, String());
            props->save();
        }
        return;
    }

    persistTokens (settings, response);
    AUTH_LOG ("Auth: startup token refresh succeeded");
}

void handleCallback (const String& urlString, element::Settings& settings)
{
    const URL url (urlString);
    if (url.getDomain() != "auth" || url.getSubPath() != "callback")
        return;

    const auto parameters = parseQueryParameters (url.toString (true));

    const auto authError = parameters["error"];
    if (authError.isNotEmpty())
    {
        AUTH_LOG ("Auth callback error: " + authError);
        return;
    }

    const auto authCode = parameters["code"];
    if (authCode.isEmpty())
        return;

    String expectedState, codeVerifier;
    if (! consumePendingPKCE (settings, expectedState, codeVerifier))
    {
        AUTH_LOG ("Auth: missing PKCE session — restart sign-in");
        return;
    }

    const auto callbackState = parameters["state"].trim();
    if (callbackState.isEmpty() || callbackState != expectedState)
    {
        AUTH_LOG ("Auth: callback rejected — invalid state");
        return;
    }

    const auto response = exchangeAuthorizationCode (authCode, codeVerifier);
    if (! response.success)
    {
        AUTH_LOG ("Auth: token exchange failed: " + response.error);
        return;
    }

    persistTokens (settings, response);
    AUTH_LOG ("Auth: token exchange completed successfully");
}

String beginAuthorizationFlow (element::Settings& settings)
{
    const auto state = generateState();
    const auto codeVerifier = generateCodeVerifier();
    if (! storePendingPKCE (settings, state, codeVerifier))
    {
        AUTH_LOG ("Auth: failed to store PKCE state");
        return {};
    }

    AUTH_LOG ("Auth: authorization flow started (PKCE)");
    return buildAuthorizationURL (state, codeVerifier);
}

void signOut (element::Settings& settings)
{
    AUTH_LOG ("Auth: signing out");

    const auto refreshToken = [&]() -> String {
        if (auto* props = settings.getUserSettings())
            return props->getValue (refreshTokenKey).trim();
        return {};
    }();

    if (refreshToken.isNotEmpty())
    {
        std::thread ([refreshToken]() {
            revokeRefreshToken (refreshToken);
        }).detach();
    }

    settings.setUpdateKey ({});
    settings.setUpdateKeyUser ({});
    settings.setUpdateKeyType ("element-v1");
    settings.setAuthPreviewUpdates (false);
    settings.setAuthAppcastUrl ({});
    settings.setUpdateChannel ("stable");

    if (auto* props = settings.getUserSettings())
    {
        props->setValue (refreshTokenKey, String());
        props->save();
    }
}

} // namespace element::auth
