package x402

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"sync"
)

// Payer produces a payment payload for the given requirements. A real
// implementation signs a stablecoin authorization with a wallet; tests
// and local development can return a fixed token. It is the consumer
// counterpart to the Facilitator (which verifies on the server side).
type Payer interface {
	Pay(ctx context.Context, req Requirements) (payment string, err error)
}

// Client is an HTTP client that automatically settles x402 payment
// challenges, up to an optional spend Budget. It is the consumer
// counterpart to Middleware: point it at a paid tool, and a 402 is paid
// and retried transparently — unless paying would exceed the budget, the
// spend cap that keeps an autonomous, paying caller in bounds.
type Client struct {
	// HTTP is the underlying client (defaults to http.DefaultClient).
	HTTP *http.Client
	// Payer constructs payment payloads. Required to pay.
	Payer Payer
	// Budget caps total spend across all calls, in the asset's smallest
	// unit (0 = unlimited). A call that would exceed it is refused before
	// any payment is made.
	Budget int64

	mu    sync.Mutex
	spent int64
}

// Spent returns the total amount paid so far, in the asset's smallest unit.
func (c *Client) Spent() int64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.spent
}

func (c *Client) httpClient() *http.Client {
	if c.HTTP != nil {
		return c.HTTP
	}
	return http.DefaultClient
}

// Do performs req. If the server answers 402, Do reads the requirements,
// checks the budget, pays via Payer, and retries once with the payment
// attached. It returns an error (without paying) if the payment would
// exceed the budget.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
	// Buffer the body so the request can be replayed after paying.
	var body []byte
	if req.Body != nil {
		b, err := io.ReadAll(req.Body)
		req.Body.Close()
		if err != nil {
			return nil, err
		}
		body = b
		req.Body = io.NopCloser(bytes.NewReader(body))
	}

	resp, err := c.httpClient().Do(req)
	if err != nil || resp.StatusCode != http.StatusPaymentRequired {
		return resp, err
	}

	var ch challenge
	_ = json.NewDecoder(resp.Body).Decode(&ch)
	resp.Body.Close()
	if len(ch.Accepts) == 0 {
		return resp, fmt.Errorf("x402: 402 response carried no requirements")
	}
	reqd := ch.Accepts[0]
	// The amount governs the whole spend cap, so it must be a real positive
	// integer. A swallowed parse error (non-decimal, overflow, empty) would
	// yield 0 and pass the budget check trivially, and a negative amount would
	// inflate the remaining allowance — either way the cap is defeated. Refuse
	// before signing anything.
	amount, err := strconv.ParseInt(strings.TrimSpace(reqd.MaxAmountRequired), 10, 64)
	if err != nil || amount <= 0 {
		return resp, fmt.Errorf("x402: refusing to pay %s: invalid maxAmountRequired %q",
			reqd.Resource, reqd.MaxAmountRequired)
	}

	// Spend cap: reserve before paying so concurrent calls cannot all pass
	// the check and overspend the caller's allowance. Roll the reservation
	// back if payment construction, replay, or verification fails.
	c.mu.Lock()
	if c.Budget > 0 && c.spent+amount > c.Budget {
		spent := c.spent
		c.mu.Unlock()
		return nil, fmt.Errorf("x402: paying %d for %s would exceed budget (spent %d of %d)",
			amount, reqd.Resource, spent, c.Budget)
	}
	c.spent += amount
	c.mu.Unlock()
	reserved := true
	rollback := func() {
		if !reserved {
			return
		}
		c.mu.Lock()
		c.spent -= amount
		c.mu.Unlock()
		reserved = false
	}

	if c.Payer == nil {
		rollback()
		return nil, fmt.Errorf("x402: payment required for %s but no Payer configured", reqd.Resource)
	}
	payment, err := c.Payer.Pay(req.Context(), reqd)
	if err != nil {
		rollback()
		return nil, fmt.Errorf("x402: pay: %w", err)
	}

	// Replay the request with the payment attached.
	var rbody io.Reader
	if body != nil {
		rbody = bytes.NewReader(body)
	}
	retry, err := http.NewRequestWithContext(req.Context(), req.Method, req.URL.String(), rbody)
	if err != nil {
		rollback()
		return nil, err
	}
	for k, v := range req.Header {
		retry.Header[k] = v
	}
	retry.Header.Set(PaymentHeader, payment)

	resp2, err := c.httpClient().Do(retry)
	if err != nil {
		rollback()
		return nil, err
	}
	if resp2.StatusCode == http.StatusPaymentRequired {
		rollback()
		return resp2, fmt.Errorf("x402: payment for %s was rejected", reqd.Resource)
	}

	reserved = false
	return resp2, nil
}
