// Go 1.21+. Set SPENDKIT_CLIENT_ID, SPENDKIT_CLIENT_SECRET, SPENDKIT_RECIPIENT.
// Default run only reads connection, Policy, and a payment preview.
// Payment requires SPENDKIT_ENABLE_PAYMENT=1 and your wallet adapter below.
// 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.
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"
)

const protocol = "2026-07-28"

var httpClient = &http.Client{Timeout: 15 * time.Second}

func environment(name, fallback string) string {
    if value := os.Getenv(name); value != "" { return value }
    return fallback
}

func object(value any) map[string]any {
    result, ok := value.(map[string]any)
    if !ok { return map[string]any{} }
    return result
}

func field(value map[string]any, key string) string {
    result, _ := value[key].(string)
    return result
}

func decode(response *http.Response) (map[string]any, error) {
    defer response.Body.Close()
    var value map[string]any
    decoder := json.NewDecoder(response.Body)
    decoder.UseNumber()
    if err := decoder.Decode(&value); err != nil { return nil, err }
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        return nil, fmt.Errorf("HTTP %d: %v", response.StatusCode, value)
    }
    return value, nil
}

func token(baseURL string) (string, error) {
    id, secret := os.Getenv("SPENDKIT_CLIENT_ID"), os.Getenv("SPENDKIT_CLIENT_SECRET")
    if id == "" || secret == "" { return "", errors.New("set SPENDKIT_CLIENT_ID and SPENDKIT_CLIENT_SECRET") }
    form := url.Values{"grant_type": {"client_credentials"}, "client_id": {id}, "client_secret": {secret}}
    request, err := http.NewRequest("POST", baseURL+"/oauth/token", strings.NewReader(form.Encode()))
    if err != nil { return "", err }
    request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    response, err := httpClient.Do(request)
    if err != nil { return "", err }
    data, err := decode(response)
    if err != nil { return "", err }
    accessToken := field(data, "access_token")
    if accessToken == "" { return "", errors.New("OAuth response has no access_token") }
    return accessToken, nil
}

type MCP struct {
    baseURL string
    token string
    id int
}

func (client *MCP) rpc(method string, params map[string]any) (map[string]any, error) {
    client.id++
    if params == nil { params = map[string]any{} }
    params["_meta"] = map[string]any{
        "io.modelcontextprotocol/protocolVersion": protocol,
        "io.modelcontextprotocol/clientInfo": map[string]any{"name": "spendkit-go-example", "version": "1.0.0"},
        "io.modelcontextprotocol/clientCapabilities": map[string]any{},
    }
    body, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": client.id, "method": method, "params": params})
    if err != nil { return nil, err }
    request, err := http.NewRequest("POST", client.baseURL+"/mcp", strings.NewReader(string(body)))
    if err != nil { return nil, err }
    request.Header.Set("Authorization", "Bearer "+client.token)
    request.Header.Set("Content-Type", "application/json")
    request.Header.Set("Accept", "application/json")
    request.Header.Set("MCP-Protocol-Version", protocol)
    request.Header.Set("Mcp-Method", method)
    if method == "tools/call" { request.Header.Set("Mcp-Name", field(params, "name")) }
    response, err := httpClient.Do(request)
    if err != nil { return nil, err }
    message, err := decode(response)
    if err != nil { return nil, err }
    if message["error"] != nil { return nil, fmt.Errorf("MCP %s: %v", method, message["error"]) }
    return object(message["result"]), nil
}

func (client *MCP) tool(name string, args map[string]any) (map[string]any, error) {
    if args == nil { args = map[string]any{} }
    result, err := client.rpc("tools/call", map[string]any{"name": name, "arguments": args})
    if err != nil { return nil, err }
    data := object(result["structuredContent"])
    if len(data) == 0 {
        for _, entry := range result["content"].([]any) {
            content := object(entry)
            if field(content, "type") == "text" {
                if err := json.Unmarshal([]byte(field(content, "text")), &data); err != nil { return nil, err }
                break
            }
        }
    }
    if result["isError"] == true && data["allowed"] != false && data["accepted"] != false {
        return nil, fmt.Errorf("%s failed: %v", name, data)
    }
    return data, nil
}

type WalletSender func(transaction map[string]any) (string, error)

func createPaymentWalletSender() (WalletSender, error) {
    // YOUR WALLET CODE: connect your EVM wallet signer, validate the exact
    // Router transaction and submit it from the wallet bound to this Agent.
    // Return the real tx hash. Keep wallet keys outside the AI model.
    return nil, errors.New("implement the wallet sender before enabling payment")
}

func run() error {
    baseURL := strings.TrimRight(environment("SPENDKIT_BASE_URL", "https://spendkit-alpha.vercel.app"), "/")
    accessToken, err := token(baseURL)
    if err != nil { return err }
    mcp := &MCP{baseURL: baseURL, token: accessToken}
    if _, err = mcp.rpc("server/discover", nil); err != nil { return err }
    if _, err = mcp.rpc("tools/list", nil); err != nil { return err }
    connection, err := mcp.tool("spendkit_get_connection", nil)
    if err != nil { return err }
    policy, err := mcp.tool("spendkit_get_policy", nil)
    if err != nil { return err }
    agent := object(connection["agent"])
    fmt.Println("Agent:", field(agent, "name"), "Policy ready:", policy["ready"])

    // YOUR BUSINESS CODE: replace environment values with the payment intent
    // chosen by your own application or AI Agent.
    payment := map[string]any{
        "amount": environment("SPENDKIT_AMOUNT", "0.01"),
        "recipient": os.Getenv("SPENDKIT_RECIPIENT"),
        "token": "USDG",
    }
    if payment["recipient"] == "" { fmt.Println("Set SPENDKIT_RECIPIENT to preview a payment"); return nil }
    preview, err := mcp.tool("spendkit_preview_payment", payment)
    if err != nil { return err }
    fmt.Println("Preview:", preview["allowed"], preview["reason"])
    if preview["allowed"] != true || os.Getenv("SPENDKIT_ENABLE_PAYMENT") != "1" { return nil }

    sender, err := createPaymentWalletSender() // Fails before authorization until implemented.
    if err != nil { return err }
    key := os.Getenv("SPENDKIT_IDEMPOTENCY_KEY")
    if key == "" { return errors.New("set one stable SPENDKIT_IDEMPOTENCY_KEY per logical payment") }
    arguments := map[string]any{"amount": payment["amount"], "recipient": payment["recipient"], "token": "USDG", "idempotencyKey": key}
    authorized, err := mcp.tool("spendkit_authorize_payment", arguments)
    if err != nil { return err }
    transaction := object(authorized["transaction"])
    if authorized["allowed"] != true || len(transaction) == 0 { return errors.New("no new transaction authorized; inspect intent status") }
    network := object(connection["network"])
    wallet := field(object(agent["paymentWallet"]), "address")
    auth := object(authorized["authorization"])
    if wallet == "" || !strings.EqualFold(field(transaction, "from"), wallet) ||
        !strings.EqualFold(field(auth, "paymentWallet"), wallet) ||
        !strings.EqualFold(field(auth, "recipient"), field(payment, "recipient")) ||
        field(auth, "amount") != field(preview, "amountAtomic") ||
        !strings.EqualFold(field(auth, "token"), field(object(preview["policy"]), "tokenAddress")) ||
        !strings.EqualFold(field(transaction, "to"), field(network, "routerAddress")) ||
        transaction["chainId"] != network["chainId"] {
        return errors.New("authorization does not match intended payment or bound wallet")
    }
    txHash, err := sender(transaction)
    if err != nil { return err }
    intentID := field(authorized, "intentId")
    // 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.
    recorded, err := mcp.tool("spendkit_record_payment", map[string]any{"intentId": intentID, "txHash": txHash})
    if err != nil { return err }
    // If recording fails, retry with this intentID/txHash; never resend payment.
    if recorded["accepted"] != true { return fmt.Errorf("record failed: %v", recorded["error"]) }
    // KEEP THIS CALL to reconcile the receipt; submitted is not final success.
    status, err := mcp.tool("spendkit_get_payment_status", map[string]any{"intentId": intentID})
    if err != nil { return err }
    fmt.Println("Payment status:", status["status"], "Transaction:", status["txHash"])
    return nil
}

func main() {
    if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
}
