// C++17 example. Requires libcurl and nlohmann/json.hpp.
// Set SPENDKIT_CLIENT_ID, SPENDKIT_CLIENT_SECRET, SPENDKIT_RECIPIENT.
// Default run is read-only. Implement the wallet callback before enabling pay.
// The base URL defaults below. Dashboard > AI Agents > your Agent > Connect Agent
// provides Client ID/Secret. Your own wallet signer supplies the payment key;
// never enter a private key in the dashboard or expose it to the AI model.
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

using json = nlohmann::json;
const std::string protocol = "2026-07-28";

std::string env(const char* key, const std::string& fallback = "") {
  const char* value = std::getenv(key);
  return value && *value ? std::string(value) : fallback;
}

std::string encode(const std::string& value) {
  std::ostringstream out;
  for (unsigned char ch : value) {
    if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') out << ch;
    else out << '%' << std::uppercase << std::hex << std::setw(2)
             << std::setfill('0') << static_cast<int>(ch) << std::dec;
  }
  return out.str();
}

size_t appendBody(char* data, size_t size, size_t count, void* target) {
  const size_t length = size * count;
  static_cast<std::string*>(target)->append(data, length);
  return length;
}

json post(const std::string& url, const std::string& body,
          const std::vector<std::string>& headers) {
  CURL* curl = curl_easy_init();
  if (!curl) throw std::runtime_error("Cannot initialize libcurl");
  curl_slist* list = nullptr;
  for (const auto& header : headers) list = curl_slist_append(list, header.c_str());
  std::string responseBody;
  curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
  curl_easy_setopt(curl, CURLOPT_POST, 1L);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, appendBody);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
  const CURLcode result = curl_easy_perform(curl);
  long status = 0;
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
  curl_slist_free_all(list);
  curl_easy_cleanup(curl);
  if (result != CURLE_OK) throw std::runtime_error(curl_easy_strerror(result));
  if (status < 200 || status >= 300)
    throw std::runtime_error("HTTP " + std::to_string(status) + ": " + responseBody);
  return json::parse(responseBody);
}

class SpendkitMcp {
 public:
  SpendkitMcp(std::string base, std::string token)
      : base_(std::move(base)), token_(std::move(token)) {}

  json rpc(const std::string& method, json params = json::object()) {
    const json meta = {
      {"io.modelcontextprotocol/protocolVersion", protocol},
      {"io.modelcontextprotocol/clientInfo", {{"name", "spendkit-cpp-example"}, {"version", "1.0.0"}}},
      {"io.modelcontextprotocol/clientCapabilities", json::object()},
    };
    params["_meta"] = meta;
    const json request = {{"jsonrpc", "2.0"}, {"id", ++id_}, {"method", method}, {"params", params}};
    std::vector<std::string> headers = {
      "Authorization: Bearer " + token_, "Content-Type: application/json",
      "Accept: application/json", "MCP-Protocol-Version: " + protocol,
      "Mcp-Method: " + method,
    };
    if (method == "tools/call") headers.push_back("Mcp-Name: " + params.at("name").get<std::string>());
    const json message = post(base_ + "/mcp", request.dump(), headers);
    if (message.contains("error")) throw std::runtime_error("MCP " + method + ": " + message.at("error").dump());
    return message.at("result");
  }

  json tool(const std::string& name, const json& args = json::object()) {
    const json result = rpc("tools/call", {{"name", name}, {"arguments", args}});
    json data = result.contains("structuredContent")
      ? result.at("structuredContent")
      : json::parse(result.at("content").at(0).at("text").get<std::string>());
    if (result.value("isError", false) && data.value("allowed", true) && data.value("accepted", true))
      throw std::runtime_error(name + " failed: " + data.dump());
    return data;
  }

 private:
  std::string base_;
  std::string token_;
  int id_ = 0;
};

std::function<std::string(const json&)> createPaymentWalletSender() {
  // YOUR WALLET CODE: return a function that verifies the exact Router
  // transaction, submits it from the bound wallet, and returns its tx hash.
  // Keep the private key out of the model and Spendkit Server.
  throw std::runtime_error("Implement the wallet sender before enabling payment");
}

bool sameAddress(std::string left, std::string right) {
  auto lower = [](unsigned char c) { return static_cast<char>(std::tolower(c)); };
  std::transform(left.begin(), left.end(), left.begin(), lower);
  std::transform(right.begin(), right.end(), right.begin(), lower);
  return left == right;
}

int main() {
  curl_global_init(CURL_GLOBAL_DEFAULT);
  try {
    const std::string id = env("SPENDKIT_CLIENT_ID"), secret = env("SPENDKIT_CLIENT_SECRET");
    if (id.empty() || secret.empty()) throw std::runtime_error("Set SPENDKIT_CLIENT_ID and SPENDKIT_CLIENT_SECRET");
    std::string base = env("SPENDKIT_BASE_URL", "https://spendkit-alpha.vercel.app");
    if (!base.empty() && base.back() == '/') base.pop_back();
    const std::string form = "grant_type=client_credentials&client_id=" + encode(id)
      + "&client_secret=" + encode(secret);
    const json oauth = post(base + "/oauth/token", form,
      {"Content-Type: application/x-www-form-urlencoded", "Accept: application/json"});
    SpendkitMcp mcp(base, oauth.at("access_token").get<std::string>());
    mcp.rpc("server/discover");
    mcp.rpc("tools/list");
    const json connection = mcp.tool("spendkit_get_connection");
    const json policy = mcp.tool("spendkit_get_policy");
    std::cout << "Agent: " << connection.at("agent").at("name")
              << " Policy ready: " << policy.at("ready") << '\n';

    // YOUR BUSINESS CODE: replace environment values with the payment intent
    // selected by your own application or AI Agent.
    const json payment = {{"amount", env("SPENDKIT_AMOUNT", "0.01")},
                          {"recipient", env("SPENDKIT_RECIPIENT")}, {"token", "USDG"}};
    if (payment.at("recipient").get<std::string>().empty()) {
      std::cout << "Set SPENDKIT_RECIPIENT to preview a payment\n";
      curl_global_cleanup();
      return 0;
    }
    const json preview = mcp.tool("spendkit_preview_payment", payment);
    std::cout << "Preview: " << preview.at("allowed") << " " << preview.value("reason", "") << '\n';
    if (preview.at("allowed") != true || env("SPENDKIT_ENABLE_PAYMENT") != "1") {
      curl_global_cleanup();
      return 0;
    }

    auto sender = createPaymentWalletSender(); // Fail before authorization until implemented.
    const std::string key = env("SPENDKIT_IDEMPOTENCY_KEY");
    if (key.empty()) throw std::runtime_error("Set one stable SPENDKIT_IDEMPOTENCY_KEY per logical payment");
    json request = payment;
    request["idempotencyKey"] = key;
    const json auth = mcp.tool("spendkit_authorize_payment", request);
    if (!auth.value("allowed", false) || !auth.contains("transaction"))
      throw std::runtime_error("No new transaction authorized; inspect intent status");
    const json transaction = auth.at("transaction");
    const std::string wallet = connection.at("agent").at("paymentWallet").at("address").get<std::string>();
    if (!sameAddress(transaction.at("from").get<std::string>(), wallet)
        || !sameAddress(auth.at("authorization").at("paymentWallet").get<std::string>(), wallet)
        || !sameAddress(auth.at("authorization").at("recipient").get<std::string>(), payment.at("recipient").get<std::string>())
        || auth.at("authorization").at("amount") != preview.at("amountAtomic")
        || !sameAddress(auth.at("authorization").at("token").get<std::string>(), preview.at("policy").at("tokenAddress").get<std::string>())
        || !sameAddress(transaction.at("to").get<std::string>(), connection.at("network").at("routerAddress").get<std::string>())
        || transaction.at("chainId") != connection.at("network").at("chainId"))
      throw std::runtime_error("Authorization does not match intended payment or bound wallet");
    const std::string hash = sender(transaction);
    const std::string intent = auth.at("intentId").get<std::string>();
    // KEEP THIS CALL after wallet submission: intentId + txHash lets Spendkit
    // track payment promptly. Without it, Activity/status can lag until onchain
    // reconciliation; the Router still enforces spending limits.
    const json recorded = mcp.tool("spendkit_record_payment", {{"intentId", intent}, {"txHash", hash}});
    // If recording fails, retry with this intent/hash; never resend payment.
    if (!recorded.value("accepted", false)) throw std::runtime_error("Record failed: " + recorded.dump());
    // KEEP THIS CALL to reconcile the receipt; submitted is not final success.
    const json status = mcp.tool("spendkit_get_payment_status", {{"intentId", intent}});
    std::cout << "Payment status: " << status.at("status") << '\n';
    curl_global_cleanup();
    return 0;
  } catch (const std::exception& error) {
    std::cerr << error.what() << '\n';
    curl_global_cleanup();
    return 1;
  }
}
