// Package api01 is a Api01 client generated by mimicplus from captured traffic of 127.0.0.1
// (spec v4). Standard library only; do not edit by hand, regenerate.
package api01

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"mime/multipart"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

const defaultBaseURL = "http://127.0.0.1:8765"

var defaultHeaders = map[string]string{"accept": "application/json"}

var authHeaders = []string{"authorization"}

var flow *authFlow = &authFlow{tokenPath: "access_token", tokenIn: "header", tokenName: "authorization", tokenFormat: "Bearer {token}", expiresInPath: "expires_in", refreshTokenPath: "refresh_token"}

var csrf *csrfConfig = nil

// ---- mimicplus Go runtime (MIT; derived from mimicplus). Standard library only. ----

// APIError is returned for HTTP status >= 400.
type APIError struct {
	Status int
	Body   []byte
	URL    string
}

func (e *APIError) Error() string {
	b := string(e.Body)
	if len(b) > 200 {
		b = b[:200]
	}
	return fmt.Sprintf("HTTP %d for %s: %s", e.Status, e.URL, b)
}

// Options configure a Client; zero values pick sensible defaults.
type Options struct {
	BaseURL    string
	Token      string
	Headers    map[string]string
	Retries    int
	Timeout    time.Duration
	HTTPClient *http.Client
	UserAgent  string
	// OnRequest is called after every HTTP attempt (observability hook).
	OnRequest func(method, url string, status int, elapsed time.Duration, attempt int)
}

// Event is one Server-Sent Event; Data is JSON-decoded when it parses.
type Event struct {
	Event string
	ID    string
	Data  any
	Raw   string
}

type authFlow struct {
	tokenPath, tokenIn, tokenName, tokenFormat, expiresInPath, refreshTokenPath string
}

type csrfConfig struct {
	header, source, name, method, path string
}

type pageRecipe struct {
	style, itemsPath, cursorPath, nextPath, totalPath string
	start                                             int
}

// Client is safe for concurrent use once configured.
type Client struct {
	BaseURL      string
	Headers      map[string]string
	HTTP         *http.Client
	Retries      int
	CSRF         string
	RefreshToken string
	TokenExpiry  time.Time
	LastStatus   int
	onRequest    func(method, url string, status int, elapsed time.Duration, attempt int)
	authQuery    url.Values
	mu           sync.Mutex
	refreshing   bool
	refresher    func(ctx context.Context, token string) ([]byte, error)
}

func newClient(o Options) *Client {
	jar, _ := cookiejar.New(nil)
	hc := o.HTTPClient
	if hc == nil {
		t := o.Timeout
		if t == 0 {
			t = 30 * time.Second
		}
		hc = &http.Client{Timeout: t, Jar: jar}
	} else if hc.Jar == nil {
		hc.Jar = jar
	}
	base := o.BaseURL
	if base == "" {
		base = defaultBaseURL
	}
	c := &Client{BaseURL: strings.TrimRight(base, "/"), Headers: map[string]string{}, HTTP: hc, Retries: 3, onRequest: o.OnRequest}
	if o.Retries > 0 {
		c.Retries = o.Retries
	}
	for k, v := range defaultHeaders {
		c.Headers[k] = v
	}
	ua := o.UserAgent
	if ua == "" {
		ua = "mimicplus-go"
	}
	c.Headers["user-agent"] = ua
	for k, v := range o.Headers {
		c.Headers[strings.ToLower(k)] = v
	}
	if o.Token != "" {
		c.SetToken(o.Token)
	}
	return c
}

// SetToken installs an API token the learned way (header format or query parameter).
func (c *Client) SetToken(token string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if flow != nil {
		v := strings.ReplaceAll(flow.tokenFormat, "{token}", token)
		if flow.tokenIn == "query" {
			c.authQuery = url.Values{flow.tokenName: {v}}
		} else {
			c.Headers[flow.tokenName] = v
		}
		return
	}
	h := "authorization"
	if len(authHeaders) > 0 {
		h = authHeaders[0]
	}
	if h == "authorization" && !strings.Contains(token, " ") {
		token = "Bearer " + token
	}
	c.Headers[h] = token
}

// Cookies returns the session cookies the client currently holds for its base URL.
func (c *Client) Cookies() map[string]string {
	out := map[string]string{}
	u, err := url.Parse(c.BaseURL)
	if err != nil || c.HTTP.Jar == nil {
		return out
	}
	for _, ck := range c.HTTP.Jar.Cookies(u) {
		out[ck.Name] = ck.Value
	}
	return out
}

func getIn(v any, path string) any {
	if path == "" {
		return v
	}
	for _, k := range strings.Split(path, ".") {
		m, ok := v.(map[string]any)
		if !ok {
			return nil
		}
		v = m[k]
	}
	return v
}

func setPath(m map[string]any, path []string, v any) {
	for _, k := range path[:len(path)-1] {
		next, ok := m[k].(map[string]any)
		if !ok {
			next = map[string]any{}
			m[k] = next
		}
		m = next
	}
	m[path[len(path)-1]] = v
}

func addQ[T any](q url.Values, k string, v *T) {
	if v != nil {
		q.Add(k, fmt.Sprint(*v))
	}
}

func ptrOr[T any](v *T, d T) *T {
	if v != nil {
		return v
	}
	return &d
}

// Ptr returns a pointer to v (handy for optional parameters).
func Ptr[T any](v T) *T { return &v }

func (c *Client) applyAuth(raw []byte) error {
	if flow == nil {
		return nil
	}
	var m any
	if err := json.Unmarshal(raw, &m); err != nil {
		return err
	}
	tok, _ := getIn(m, flow.tokenPath).(string)
	if tok == "" {
		return fmt.Errorf("auth: no token at %q in login response", flow.tokenPath)
	}
	c.SetToken(tok)
	c.mu.Lock()
	defer c.mu.Unlock()
	if rt, _ := getIn(m, flow.refreshTokenPath).(string); flow.refreshTokenPath != "" && rt != "" {
		c.RefreshToken = rt
	}
	if exp, ok := getIn(m, flow.expiresInPath).(float64); flow.expiresInPath != "" && ok {
		c.TokenExpiry = time.Now().Add(time.Duration(exp) * time.Second)
	} else {
		c.TokenExpiry = time.Time{}
	}
	return nil
}

func (c *Client) canRefresh() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.refresher != nil && c.RefreshToken != "" && !c.refreshing
}

// RefreshAuth exchanges the refresh token for a new access token.
func (c *Client) RefreshAuth(ctx context.Context) error {
	if !c.canRefresh() {
		return errors.New("no refresh token / refresh endpoint")
	}
	c.mu.Lock()
	c.refreshing = true
	tok := c.RefreshToken
	c.mu.Unlock()
	defer func() { c.mu.Lock(); c.refreshing = false; c.mu.Unlock() }()
	raw, err := c.refresher(ctx, tok)
	if err != nil {
		return err
	}
	return c.applyAuth(raw)
}

var csrfMeta = regexp.MustCompile(`(?i)<(?:meta|input)[^>]*(?:name)=["']([^"']+)["'][^>]*(?:content|value)=["']([^"']+)["']`)

// FetchCSRF (re)loads the anti-CSRF token the learned way.
func (c *Client) FetchCSRF(ctx context.Context) (string, error) {
	if csrf == nil {
		return "", nil
	}
	if csrf.source == "cookie" {
		c.CSRF = c.Cookies()[csrf.name]
		return c.CSRF, nil
	}
	path := csrf.path
	if path == "" {
		path = "/"
	}
	body, hdr, err := c.do(ctx, csrf.method, path, nil, nil, "", true, true)
	if err != nil {
		return "", err
	}
	switch csrf.source {
	case "body":
		var m any
		_ = json.Unmarshal(body, &m)
		c.CSRF, _ = getIn(m, csrf.name).(string)
	case "header":
		c.CSRF = hdr.Get(csrf.name)
	default:
		for _, mm := range csrfMeta.FindAllStringSubmatch(string(body), -1) {
			if mm[1] == csrf.name {
				c.CSRF = mm[2]
			}
		}
	}
	return c.CSRF, nil
}

func (c *Client) url(path string, q url.Values) string {
	u := path
	if !strings.HasPrefix(path, "http://") && !strings.HasPrefix(path, "https://") {
		u = c.BaseURL + path
	}
	all := url.Values{}
	for k, vs := range q {
		all[k] = append(all[k], vs...)
	}
	c.mu.Lock()
	for k, vs := range c.authQuery {
		all[k] = vs
	}
	c.mu.Unlock()
	if len(all) > 0 {
		sep := "?"
		if strings.Contains(u, "?") {
			sep = "&"
		}
		u += sep + all.Encode()
	}
	return u
}

func encodeBody(body any, kind string) (io.Reader, string, error) {
	switch b := body.(type) {
	case nil:
		return nil, "", nil
	case string:
		return strings.NewReader(b), "", nil
	case map[string]any:
		switch kind {
		case "form":
			f := url.Values{}
			for k, v := range b {
				if v != nil {
					f.Set(k, fmt.Sprint(v))
				}
			}
			return strings.NewReader(f.Encode()), "application/x-www-form-urlencoded", nil
		case "multipart":
			var buf bytes.Buffer
			w := multipart.NewWriter(&buf)
			for k, v := range b {
				switch x := v.(type) {
				case nil:
				case []byte:
					fw, err := w.CreateFormFile(k, k)
					if err != nil {
						return nil, "", err
					}
					_, _ = fw.Write(x)
				default:
					_ = w.WriteField(k, fmt.Sprint(x))
				}
			}
			_ = w.Close()
			return &buf, w.FormDataContentType(), nil
		}
	}
	raw, err := json.Marshal(body)
	if err != nil {
		return nil, "", err
	}
	return bytes.NewReader(raw), "application/json", nil
}

func (c *Client) do(ctx context.Context, method, path string, q url.Values, body any, kind string, safe, bare bool) ([]byte, http.Header, error) {
	unsafe := method != "GET" && method != "HEAD" && method != "OPTIONS"
	if !bare && unsafe && csrf != nil && (c.CSRF == "" || csrf.source == "cookie") {
		if _, err := c.FetchCSRF(ctx); err != nil {
			return nil, nil, err
		}
	}
	if !bare && !c.TokenExpiry.IsZero() && time.Until(c.TokenExpiry) < 5*time.Second && c.canRefresh() {
		_ = c.RefreshAuth(ctx)
	}
	refreshed, csrfRetried := bare, bare
	for attempt := 0; ; attempt++ {
		rd, ctype, err := encodeBody(body, kind)
		if err != nil {
			return nil, nil, err
		}
		u := c.url(path, q)
		req, err := http.NewRequestWithContext(ctx, method, u, rd)
		if err != nil {
			return nil, nil, err
		}
		c.mu.Lock()
		for k, v := range c.Headers {
			req.Header.Set(k, v)
		}
		c.mu.Unlock()
		if ctype != "" && req.Header.Get("content-type") == "" {
			req.Header.Set("content-type", ctype)
		}
		if unsafe && csrf != nil && c.CSRF != "" {
			req.Header.Set(csrf.header, c.CSRF)
		}
		t0 := time.Now()
		resp, err := c.HTTP.Do(req)
		if err != nil {
			if safe && attempt < c.Retries && ctx.Err() == nil {
				sleepCtx(ctx, backoff(attempt, ""))
				continue
			}
			return nil, nil, err
		}
		raw, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, nil, err
		}
		c.LastStatus = resp.StatusCode
		if c.onRequest != nil {
			c.onRequest(method, u, resp.StatusCode, time.Since(t0), attempt)
		}
		if resp.StatusCode == 401 && !refreshed && c.canRefresh() {
			refreshed = true
			if c.RefreshAuth(ctx) == nil {
				continue
			}
		}
		if (resp.StatusCode == 403 || resp.StatusCode == 419) && unsafe && csrf != nil && !csrfRetried {
			csrfRetried = true
			c.CSRF = ""
			if _, err := c.FetchCSRF(ctx); err == nil {
				continue
			}
		}
		if safe && (resp.StatusCode == 429 || resp.StatusCode >= 500) && attempt < c.Retries {
			sleepCtx(ctx, backoff(attempt, resp.Header.Get("retry-after")))
			continue
		}
		if resp.StatusCode >= 400 {
			return raw, resp.Header, &APIError{Status: resp.StatusCode, Body: raw, URL: u}
		}
		return raw, resp.Header, nil
	}
}

func backoff(attempt int, retryAfter string) time.Duration {
	if s, err := strconv.Atoi(strings.TrimSpace(retryAfter)); err == nil {
		return time.Duration(min(s, 60)) * time.Second
	}
	d := time.Duration(250*(1<<attempt)) * time.Millisecond
	if d > 8*time.Second {
		d = 8 * time.Second
	}
	return d/2 + time.Duration(rand.Int63n(int64(d/2)+1))
}

func sleepCtx(ctx context.Context, d time.Duration) {
	select {
	case <-ctx.Done():
	case <-time.After(d):
	}
}

func decode[T any](raw []byte, err error) (*T, error) {
	if err != nil {
		return nil, err
	}
	var out T
	if len(bytes.TrimSpace(raw)) == 0 {
		return &out, nil
	}
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	return &out, nil
}

// paginate walks pages with a learned recipe; fetch gets the page position (nil = first page as given),
// or a next-page URL for the "next" style. Items are handed to sink as raw JSON.
func (c *Client) paginate(ctx context.Context, r pageRecipe, maxPages int, fetch func(pos any, nextURL string) ([]byte, error), sink func([]json.RawMessage) error) error {
	if maxPages <= 0 {
		maxPages = 10
	}
	var pos any
	next := ""
	seen := map[string]bool{}
	for page := 0; page < maxPages; page++ {
		raw, err := fetch(pos, next)
		if err != nil {
			return err
		}
		var doc any
		if err := json.Unmarshal(raw, &doc); err != nil {
			return err
		}
		var items []json.RawMessage
		if r.itemsPath == "" {
			_ = json.Unmarshal(raw, &items)
		} else {
			sub, _ := json.Marshal(getIn(doc, r.itemsPath))
			_ = json.Unmarshal(sub, &items)
		}
		key := fmt.Sprint(len(items))
		if len(items) > 0 {
			key += string(items[0])
		}
		if len(items) == 0 || seen[key] {
			return nil
		}
		seen[key] = true
		if err := sink(items); err != nil {
			return err
		}
		switch r.style {
		case "page":
			if pos == nil {
				pos = r.start
			}
			pos = pos.(int) + 1
		case "offset":
			if pos == nil {
				pos = r.start
			}
			pos = pos.(int) + len(items)
		case "cursor":
			pos = getIn(doc, r.cursorPath)
			if pos == nil || pos == "" {
				return nil
			}
		default:
			n, _ := getIn(doc, r.nextPath).(string)
			if n == "" {
				return nil
			}
			next = n
		}
		if r.totalPath != "" && r.style == "offset" {
			if t, ok := getIn(doc, r.totalPath).(float64); ok && float64(pos.(int)) >= t {
				return nil
			}
		}
	}
	return nil
}

// sse streams Server-Sent Events to fn until it returns false, maxEvents is hit, or the stream ends.
func (c *Client) sse(ctx context.Context, path string, q url.Values, maxEvents int, fn func(Event) bool) error {
	u := c.url(path, q)
	req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
	if err != nil {
		return err
	}
	c.mu.Lock()
	for k, v := range c.Headers {
		req.Header.Set(k, v)
	}
	c.mu.Unlock()
	req.Header.Set("accept", "text/event-stream")
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		raw, _ := io.ReadAll(resp.Body)
		return &APIError{Status: resp.StatusCode, Body: raw, URL: u}
	}
	sc := bufio.NewScanner(resp.Body)
	sc.Buffer(make([]byte, 64*1024), 4*1024*1024)
	var ev Event
	var data []string
	n := 0
	flush := func() bool {
		if len(data) == 0 && ev.Event == "" && ev.ID == "" {
			return true
		}
		ev.Raw = strings.Join(data, "\n")
		var v any
		if json.Unmarshal([]byte(ev.Raw), &v) == nil {
			ev.Data = v
		} else {
			ev.Data = ev.Raw
		}
		if ev.Event == "" {
			ev.Event = "message"
		}
		ok := fn(ev)
		n++
		ev, data = Event{}, nil
		return ok && (maxEvents <= 0 || n < maxEvents)
	}
	for sc.Scan() {
		line := strings.TrimRight(sc.Text(), "\r")
		if line == "" {
			if !flush() {
				return nil
			}
			continue
		}
		if strings.HasPrefix(line, ":") {
			continue
		}
		field, value, _ := strings.Cut(line, ":")
		value = strings.TrimPrefix(value, " ")
		switch field {
		case "data":
			data = append(data, value)
		case "event":
			ev.Event = value
		case "id":
			ev.ID = value
		}
	}
	flush()
	return sc.Err()
}

// ---- end mimicplus runtime ----

// New returns a Api01 client.
func New(o Options) *Client {
	c := newClient(o)
	c.refresher = func(ctx context.Context, token string) ([]byte, error) {
		return c.refreshRaw(ctx, RefreshParams{RefreshToken: &token})
	}
	return c
}

type LoginResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
}

type GetMeResponse struct {
	Id       int64  `json:"id"`
	Username string `json:"username"`
	Email    string `json:"email"`
	Plan     string `json:"plan"`
}

type ListProductsResponseItemsItem struct {
	Id      int64    `json:"id"`
	InStock bool     `json:"in_stock"`
	Name    string   `json:"name"`
	Price   float64  `json:"price"`
	Tags    []string `json:"tags"`
}

type ListProductsResponse struct {
	Items    []ListProductsResponseItemsItem `json:"items"`
	NextPage *int64                          `json:"next_page"`
	Page     int64                           `json:"page"`
	PerPage  int64                           `json:"per_page"`
	Total    int64                           `json:"total"`
}

type CreateItemResponseItem struct {
	ProductId int64 `json:"product_id"`
	Qty       int64 `json:"qty"`
}

type CreateItemResponse struct {
	CartSize int64                  `json:"cart_size"`
	Item     CreateItemResponseItem `json:"item"`
}

type SearchProductsResponseDataSearchProductsNodesItem struct {
	Id    string  `json:"id"`
	Name  string  `json:"name"`
	Price float64 `json:"price"`
}

type SearchProductsResponseDataSearchProducts struct {
	Nodes      []SearchProductsResponseDataSearchProductsNodesItem `json:"nodes"`
	TotalCount int64                                               `json:"totalCount"`
}

type SearchProductsResponseData struct {
	SearchProducts SearchProductsResponseDataSearchProducts `json:"searchProducts"`
}

type SearchProductsResponse struct {
	Data SearchProductsResponseData `json:"data"`
}

type GetOrderResponseDataOrder struct {
	Id     string  `json:"id"`
	Status string  `json:"status"`
	Total  float64 `json:"total"`
	Items  []int64 `json:"items"`
}

type GetOrderResponseData struct {
	Order GetOrderResponseDataOrder `json:"order"`
}

type GetOrderResponse struct {
	Data GetOrderResponseData `json:"data"`
}

type AddReviewResponseDataAddReview struct {
	Id     string `json:"id"`
	Rating int64  `json:"rating"`
	Ok     bool   `json:"ok"`
}

type AddReviewResponseData struct {
	AddReview AddReviewResponseDataAddReview `json:"addReview"`
}

type AddReviewResponse struct {
	Data AddReviewResponseData `json:"data"`
}

// LoginParams are the inputs of Login; nil fields use the recorded defaults.
type LoginParams struct {
	Username *string        // body username
	Password *string        // body password
	Extra    map[string]any // extra top-level body fields
}

func (c *Client) loginRaw(ctx context.Context, p LoginParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"username\": \"\", \"password\": \"\"}"), &b)
	if p.Username != nil {
		setPath(b, []string{"username"}, *p.Username)
	} else {
		delete(b, "username")
	}
	if p.Password != nil {
		setPath(b, []string{"password"}, *p.Password)
	} else {
		delete(b, "password")
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/api/auth/login", q, b, "json", false, false)
	return raw, err
}

// Login: POST /api/auth/login -> 200 (WRITES).
func (c *Client) Login(ctx context.Context, p LoginParams) (*LoginResponse, error) {
	return decode[LoginResponse](c.loginRaw(ctx, p))
}

func (c *Client) getMeRaw(ctx context.Context) ([]byte, error) {
	q := url.Values{}
	raw, _, err := c.do(ctx, "GET", "/api/me", q, nil, "json", true, false)
	return raw, err
}

// GetMe: GET /api/me -> 200 (read-only).
func (c *Client) GetMe(ctx context.Context) (*GetMeResponse, error) {
	return decode[GetMeResponse](c.getMeRaw(ctx))
}

// ListProductsParams are the inputs of ListProducts; nil fields use the recorded defaults.
type ListProductsParams struct {
	Page    *int // query page
	PerPage *int // query per_page
}

func (c *Client) listProductsRaw(ctx context.Context, p ListProductsParams) ([]byte, error) {
	q := url.Values{}
	addQ(q, "page", ptrOr(p.Page, 1))
	addQ(q, "per_page", ptrOr(p.PerPage, 2))
	raw, _, err := c.do(ctx, "GET", "/api/products", q, nil, "json", true, false)
	return raw, err
}

// ListProducts: GET /api/products -> 200 (read-only).
func (c *Client) ListProducts(ctx context.Context, p ListProductsParams) (*ListProductsResponse, error) {
	return decode[ListProductsResponse](c.listProductsRaw(ctx, p))
}

// ListProductsAll collects every item across pages (learned page pagination).
func (c *Client) ListProductsAll(ctx context.Context, p ListProductsParams, maxPages int) ([]ListProductsResponseItemsItem, error) {
	r := pageRecipe{style: "page", itemsPath: "items", cursorPath: "", nextPath: "", totalPath: "total", start: 1}
	if p.Page != nil {
		r.start = *p.Page
	}
	var all []ListProductsResponseItemsItem
	err := c.paginate(ctx, r, maxPages, func(pos any, next string) ([]byte, error) {
		if next != "" {
			raw, _, err := c.do(ctx, "GET", next, nil, nil, "", true, false)
			return raw, err
		}
		q := p
		if pos != nil {
			v := pos.(int)
			q.Page = &v
		}
		return c.listProductsRaw(ctx, q)
	}, func(items []json.RawMessage) error {
		for _, it := range items {
			var x ListProductsResponseItemsItem
			if err := json.Unmarshal(it, &x); err != nil {
				return err
			}
			all = append(all, x)
		}
		return nil
	})
	return all, err
}

func (c *Client) getProductRaw(ctx context.Context, productId int) ([]byte, error) {
	q := url.Values{}
	raw, _, err := c.do(ctx, "GET", fmt.Sprintf("/api/products/%s", url.PathEscape(fmt.Sprint(productId))), q, nil, "json", true, false)
	return raw, err
}

// GetProduct: GET /api/products/{product_id} -> 200 (read-only).
func (c *Client) GetProduct(ctx context.Context, productId int) (*ListProductsResponseItemsItem, error) {
	return decode[ListProductsResponseItemsItem](c.getProductRaw(ctx, productId))
}

// CreateItemParams are the inputs of CreateItem; nil fields use the recorded defaults.
type CreateItemParams struct {
	ProductId *int           // body product_id
	Qty       *int           // body qty
	Extra     map[string]any // extra top-level body fields
}

func (c *Client) createItemRaw(ctx context.Context, p CreateItemParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"product_id\": 104, \"qty\": 1}"), &b)
	if p.ProductId != nil {
		setPath(b, []string{"product_id"}, *p.ProductId)
	}
	if p.Qty != nil {
		setPath(b, []string{"qty"}, *p.Qty)
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/api/cart/items", q, b, "json", false, false)
	return raw, err
}

// CreateItem: POST /api/cart/items -> 201 (WRITES).
func (c *Client) CreateItem(ctx context.Context, p CreateItemParams) (*CreateItemResponse, error) {
	return decode[CreateItemResponse](c.createItemRaw(ctx, p))
}

// SearchProductsParams are the inputs of SearchProducts; nil fields use the recorded defaults.
type SearchProductsParams struct {
	Q     *string        // body variables.q
	First *int           // body variables.first
	Extra map[string]any // extra top-level body fields
}

func (c *Client) searchProductsRaw(ctx context.Context, p SearchProductsParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"operationName\": \"SearchProducts\", \"query\": \"query SearchProducts($q: String!, $first: Int) { searchProducts(q: $q, first: $first) { totalCount nodes { id name price } } }\", \"variables\": {\"q\": \"lamp\", \"first\": 5}}"), &b)
	if p.Q != nil {
		setPath(b, []string{"variables", "q"}, *p.Q)
	}
	if p.First != nil {
		setPath(b, []string{"variables", "first"}, *p.First)
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/graphql", q, b, "json", true, false)
	return raw, err
}

// SearchProducts: POST /graphql -> 200 (read-only). GraphQL query SearchProducts.
func (c *Client) SearchProducts(ctx context.Context, p SearchProductsParams) (*SearchProductsResponse, error) {
	return decode[SearchProductsResponse](c.searchProductsRaw(ctx, p))
}

// GetOrderParams are the inputs of GetOrder; nil fields use the recorded defaults.
type GetOrderParams struct {
	Id    *string        // body variables.id
	Extra map[string]any // extra top-level body fields
}

func (c *Client) getOrderRaw(ctx context.Context, p GetOrderParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"operationName\": \"GetOrder\", \"query\": \"query GetOrder($id: ID!) { order(id: $id) { id status total items } }\", \"variables\": {\"id\": \"o-1\"}}"), &b)
	if p.Id != nil {
		setPath(b, []string{"variables", "id"}, *p.Id)
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/graphql", q, b, "json", true, false)
	return raw, err
}

// GetOrder: POST /graphql -> 200 (read-only). GraphQL query GetOrder.
func (c *Client) GetOrder(ctx context.Context, p GetOrderParams) (*GetOrderResponse, error) {
	return decode[GetOrderResponse](c.getOrderRaw(ctx, p))
}

// AddReviewParams are the inputs of AddReview; nil fields use the recorded defaults.
type AddReviewParams struct {
	ProductId *string        // body variables.productId
	Rating    *int           // body variables.rating
	Text      *string        // body variables.text
	Extra     map[string]any // extra top-level body fields
}

func (c *Client) addReviewRaw(ctx context.Context, p AddReviewParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"operationName\": \"AddReview\", \"query\": \"mutation AddReview($productId: ID!, $rating: Int!, $text: String) { addReview(productId: $productId, rating: $rating, text: $text) { id rating ok } }\", \"variables\": {\"productId\": \"101\", \"rating\": 5, \"text\": \"great\"}}"), &b)
	if p.ProductId != nil {
		setPath(b, []string{"variables", "productId"}, *p.ProductId)
	}
	if p.Rating != nil {
		setPath(b, []string{"variables", "rating"}, *p.Rating)
	}
	if p.Text != nil {
		setPath(b, []string{"variables", "text"}, *p.Text)
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/graphql", q, b, "json", false, false)
	return raw, err
}

// AddReview: POST /graphql -> 200 (WRITES). GraphQL mutation AddReview.
func (c *Client) AddReview(ctx context.Context, p AddReviewParams) (*AddReviewResponse, error) {
	return decode[AddReviewResponse](c.addReviewRaw(ctx, p))
}

// RefreshParams are the inputs of Refresh; nil fields use the recorded defaults.
type RefreshParams struct {
	RefreshToken *string        // body refresh_token
	Extra        map[string]any // extra top-level body fields
}

func (c *Client) refreshRaw(ctx context.Context, p RefreshParams) ([]byte, error) {
	q := url.Values{}
	var b map[string]any
	_ = json.Unmarshal([]byte("{\"refresh_token\": \"\"}"), &b)
	if p.RefreshToken != nil {
		setPath(b, []string{"refresh_token"}, *p.RefreshToken)
	} else {
		delete(b, "refresh_token")
	}
	for k, v := range p.Extra {
		b[k] = v
	}
	raw, _, err := c.do(ctx, "POST", "/api/auth/refresh", q, b, "json", false, false)
	return raw, err
}

// Refresh: POST /api/auth/refresh -> 200 (WRITES).
func (c *Client) Refresh(ctx context.Context, p RefreshParams) (*LoginResponse, error) {
	return decode[LoginResponse](c.refreshRaw(ctx, p))
}

// Authenticate logs in with the learned flow and keeps the token (credentials are never stored).
func (c *Client) Authenticate(ctx context.Context, p LoginParams) (*LoginResponse, error) {
	raw, err := c.loginRaw(ctx, p)
	if err != nil {
		return nil, err
	}
	if err := c.applyAuth(raw); err != nil {
		return nil, err
	}
	return decode[LoginResponse](raw, nil)
}
