// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package client

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"tier-routing-golden-pp-cli/internal/cliutil"
	"tier-routing-golden-pp-cli/internal/config"
	"time"
)

const BinaryResponseHeader = "X-Printing-Press-Binary-Response"

type Client struct {
	BaseURL     string
	Config      *config.Config
	HTTPClient  *http.Client
	DryRun      bool
	NoCache     bool
	cacheDir    string
	limiter     *cliutil.AdaptiveLimiter
	requestTier string
	limiters    map[string]*cliutil.AdaptiveLimiter
}

type requestAuth struct {
	Value string
	In    string
	Name  string
}

func (c *Client) WithTier(tier string) *Client {
	if c == nil || tier == "" {
		return c
	}
	next := *c
	next.requestTier = tier
	next.limiter = c.limiterForTier(tier)
	return &next
}

func newTierLimiters(rateLimit float64) map[string]*cliutil.AdaptiveLimiter {
	return map[string]*cliutil.AdaptiveLimiter{
		"enterprise": cliutil.NewAdaptiveLimiter(rateLimit),
		"free":       cliutil.NewAdaptiveLimiter(rateLimit),
		"paid":       cliutil.NewAdaptiveLimiter(rateLimit),
	}
}

func (c *Client) limiterForTier(tier string) *cliutil.AdaptiveLimiter {
	if c == nil {
		return nil
	}
	if tier == "" {
		return c.limiter
	}
	if limiter, ok := c.limiters[tier]; ok && limiter != nil {
		return limiter
	}
	return c.limiter
}

func (c *Client) baseURLForRequest() string {
	switch c.requestTier {
	case "enterprise":
		return c.BaseURL
	case "free":
		return c.BaseURL
	case "paid":
		return strings.TrimRight("https://paid.api.example.com", "/")
	default:
		return c.BaseURL
	}
}

func (c *Client) authForRequest() (requestAuth, error) {
	switch c.requestTier {
	case "enterprise":
		tierValue0 := os.Getenv("TIER_ENTERPRISE_TOKEN")
		if tierValue0 == "" {
			return requestAuth{In: "header", Name: "Authorization"}, nil
		}
		value := applyTierAuthFormat("Bearer {access_token}", map[string]string{
			"token":                 tierValue0,
			"access_token":          tierValue0,
			"enterprise_token":      tierValue0,
			"TIER_ENTERPRISE_TOKEN": tierValue0,
		})
		return requestAuth{Value: value, In: "header", Name: "Authorization"}, nil
	case "free":
		return requestAuth{}, nil
	case "paid":
		tierValue0 := os.Getenv("TIER_PAID_KEY")
		if tierValue0 == "" {
			return requestAuth{In: "query", Name: "api_key"}, nil
		}
		value := tierValue0
		return requestAuth{Value: value, In: "query", Name: "api_key"}, nil
	default:
		value, err := c.authHeader()
		if err != nil {
			return requestAuth{}, err
		}
		return requestAuth{Value: value, In: "header", Name: "Authorization"}, nil
	}
}

func applyTierAuthFormat(format string, replacements map[string]string) string {
	for key, value := range replacements {
		format = strings.ReplaceAll(format, "{"+key+"}", value)
	}
	if strings.Contains(format, "{") {
		return ""
	}
	return format
}

// APIError carries HTTP status information for structured exit codes.
type APIError struct {
	Method     string
	Path       string
	StatusCode int
	Body       string
}

func (e *APIError) Error() string {
	return fmt.Sprintf("%s %s returned HTTP %d: %s", e.Method, e.Path, e.StatusCode, e.Body)
}

func newHTTPClient(timeout time.Duration, jar http.CookieJar) *http.Client {
	return &http.Client{Timeout: timeout, Jar: jar}
}

func New(cfg *config.Config, timeout time.Duration, rateLimit float64) *Client {
	homeDir, _ := os.UserHomeDir()
	cacheDir := filepath.Join(homeDir, ".cache", "tier-routing-golden-pp-cli", "http")
	httpClient := newHTTPClient(timeout, nil)
	c := &Client{
		BaseURL:    strings.TrimRight(cfg.BaseURL, "/"),
		Config:     cfg,
		HTTPClient: httpClient,
		cacheDir:   cacheDir,
		limiter:    cliutil.NewAdaptiveLimiter(rateLimit),
		limiters:   newTierLimiters(rateLimit),
	}
	// CheckRedirect re-derives auth on each hop. Go's default replays the
	// original Authorization header verbatim, which breaks nonce-bound
	// schemes (OAuth 1.0a PLAINTEXT, SigV4, Hawk): the duplicate nonce
	// trips the server's replay detector with a 401. c.authHeader()
	// returns a fresh value for those schemes and the same static value
	// for Bearer/api_key, so post-redirect headers are byte-identical for
	// static auth and freshly-signed for nonce-bound auth.
	httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		if len(via) >= 10 {
			// Match Go's defaultCheckRedirect: a plain error so Client.Do
			// returns it through do()'s err != nil branch. ErrUseLastResponse
			// would cause Do to return the 3xx with nil error, which do()
			// would then classify as a successful response and hand the HTML
			// "Moved Permanently" body back to the caller.
			return errors.New("stopped after 10 redirects")
		}
		// Tier routing picks header vs query at runtime via authForRequest;
		// re-deriving here would duplicate that selection logic. Cap depth
		// only and let Go's default header replay handle static credentials.
		return nil
	}
	return c
}

// RateLimit returns the current effective rate limit in req/s. Returns 0 if disabled.
func (c *Client) RateLimit() float64 {
	return c.limiter.Rate()
}

func (c *Client) Get(path string, params map[string]string) (json.RawMessage, error) {
	return c.GetWithHeaders(path, params, nil)
}

func (c *Client) GetWithHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, error) {
	// Check cache for GET requests
	if !c.NoCache && !c.DryRun && c.cacheDir != "" {
		if cached, ok := c.readCache(path, params); ok {
			return cached, nil
		}
	}
	result, _, err := c.do("GET", path, params, nil, headers)
	if err == nil && !c.NoCache && !c.DryRun && c.cacheDir != "" {
		c.writeCache(path, params, result)
	}
	return result, err
}

// GetNoCache issues a GET that bypasses the cache read for this call only,
// then refreshes the cache with the fresh response on success. Use for
// polling-until-terminal patterns where every call must reflect current
// server state; the same (path, params) pair returning a stale
// "in-progress" snapshot from cache would lock the poll loop on the
// initial response. Writing-back on success means subsequent c.Get calls
// (e.g. a follow-up `... get <id>` after WaitForJob returns) see the
// terminal value, not the stale non-terminal snapshot left behind by the
// first poll.
func (c *Client) GetNoCache(path string, params map[string]string) (json.RawMessage, error) {
	return c.GetWithHeadersNoCache(path, params, nil)
}

// GetWithHeadersNoCache is GetWithHeaders that skips the cache read but
// still writes the fresh response on success. See GetNoCache for when to
// prefer this over Get/GetWithHeaders.
func (c *Client) GetWithHeadersNoCache(path string, params map[string]string, headers map[string]string) (json.RawMessage, error) {
	result, _, err := c.do("GET", path, params, nil, headers)
	if err == nil && !c.NoCache && !c.DryRun && c.cacheDir != "" {
		c.writeCache(path, params, result)
	}
	return result, err
}

func (c *Client) ProbeGet(path string) (int, error) {
	_, status, err := c.do("GET", path, nil, nil, nil)
	return status, err
}

func (c *Client) cacheKey(path string, params map[string]string) string {
	key := path
	key += "|base_url=" + c.BaseURL
	key += "|tier=" + c.requestTier + "|tier_base_url=" + c.baseURLForRequest()
	if c.Config != nil {
		key += "|auth_source=" + c.Config.AuthSource
		if authHeader := c.Config.AuthHeader(); authHeader != "" {
			authHash := sha256.Sum256([]byte(authHeader))
			key += "|auth=" + hex.EncodeToString(authHash[:8])
		}
		if c.Config.Path != "" {
			key += "|config_path=" + c.Config.Path
		}
	}
	paramKeys := make([]string, 0, len(params))
	for k := range params {
		paramKeys = append(paramKeys, k)
	}
	sort.Strings(paramKeys)
	for _, k := range paramKeys {
		key += k + "=" + params[k]
	}
	h := sha256.Sum256([]byte(key))
	return hex.EncodeToString(h[:8])
}

func (c *Client) readCache(path string, params map[string]string) (json.RawMessage, bool) {
	cacheFile := filepath.Join(c.cacheDir, c.cacheKey(path, params)+".json")
	info, err := os.Stat(cacheFile)
	if err != nil || time.Since(info.ModTime()) > 5*time.Minute {
		return nil, false
	}
	data, err := os.ReadFile(cacheFile)
	if err != nil {
		return nil, false
	}
	return json.RawMessage(data), true
}

func (c *Client) writeCache(path string, params map[string]string, data json.RawMessage) {
	os.MkdirAll(c.cacheDir, 0o755)
	cacheFile := filepath.Join(c.cacheDir, c.cacheKey(path, params)+".json")
	os.WriteFile(cacheFile, []byte(data), 0o644)
}

// invalidateCache wholesale-removes the cache directory so the next read
// after a mutation cannot return a stale snapshot. Selective per-resource
// invalidation rejected: cache keys are opaque sha256 hashes.
func (c *Client) invalidateCache() {
	if c.cacheDir == "" {
		return
	}
	_ = os.RemoveAll(c.cacheDir)
}

func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
	return c.do("POST", path, nil, body, nil)
}

func (c *Client) PostWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
	return c.do("POST", path, params, body, nil)
}

func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("POST", path, nil, body, headers)
}

func (c *Client) PostWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("POST", path, params, body, headers)
}

// PostQueryWithParams is a POST that does not mutate remote state — used
// by read-only operations that ride a mutating verb on the wire (GraphQL
// queries, JSON-RPC reads, POST-based search endpoints). The verify-mode
// short-circuit does not fire for these calls; the request reaches the
// real transport even under PRINTING_PRESS_VERIFY=1 without LIVE_HTTP=1.
func (c *Client) PostQueryWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
	return c.doRead("POST", path, params, body, nil)
}

// PostQueryWithParamsAndHeaders is the headers-aware counterpart to
// PostQueryWithParams. See PostQueryWithParams for the verify-mode rationale.
func (c *Client) PostQueryWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.doRead("POST", path, params, body, headers)
}

func (c *Client) Delete(path string) (json.RawMessage, int, error) {
	return c.do("DELETE", path, nil, nil, nil)
}

func (c *Client) DeleteWithParams(path string, params map[string]string) (json.RawMessage, int, error) {
	return c.do("DELETE", path, params, nil, nil)
}

func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("DELETE", path, nil, nil, headers)
}

func (c *Client) DeleteWithParamsAndHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("DELETE", path, params, nil, headers)
}

func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
	return c.do("PUT", path, nil, body, nil)
}

func (c *Client) PutWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
	return c.do("PUT", path, params, body, nil)
}

func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("PUT", path, nil, body, headers)
}

func (c *Client) PutWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("PUT", path, params, body, headers)
}

func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
	return c.do("PATCH", path, nil, body, nil)
}

func (c *Client) PatchWithParams(path string, params map[string]string, body any) (json.RawMessage, int, error) {
	return c.do("PATCH", path, params, body, nil)
}

func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("PATCH", path, nil, body, headers)
}

func (c *Client) PatchWithParamsAndHeaders(path string, params map[string]string, body any, headers map[string]string) (json.RawMessage, int, error) {
	return c.do("PATCH", path, params, body, headers)
}

// isMutatingVerb reports whether the HTTP method writes server state.
// Used by do()'s verify-mode short-circuit to gate dial-out: under
// PRINTING_PRESS_VERIFY=1 (without LIVE_HTTP=1 opt-in), generated
// commands must not actually issue mutating requests, even if a
// handler-level dry-run check was missed.
func isMutatingVerb(method string) bool {
	switch method {
	case "DELETE", "POST", "PUT", "PATCH":
		return true
	}
	return false
}

// verifyShortCircuitEnvelope returns the synthetic JSON body that
// stands in for a real mutating response when do() short-circuits in
// verify mode. The __pp_verify_synthetic__ sentinel is namespace-
// reserved (no real API uses __pp_*) so downstream consumers
// (validate-narrative, agent inspections) can key on one obvious field
// instead of trying to disambiguate common literals like status:"noop".
// method and path are echoed back as diagnostic prose for human/agent
// inspection.
func verifyShortCircuitEnvelope(method, path string) json.RawMessage {
	body, _ := json.Marshal(map[string]any{
		"__pp_verify_synthetic__": true,
		"status":                  "noop",
		"reason":                  "verify_short_circuit",
		"method":                  method,
		"path":                    path,
	})
	return json.RawMessage(body)
}

// do executes an HTTP request. headerOverrides, when non-nil, override global
// RequiredHeaders for this specific request (used for per-endpoint API versioning).
func (c *Client) do(method, path string, params map[string]string, body any, headerOverrides map[string]string) (json.RawMessage, int, error) {
	return c.doInternal(method, path, params, body, headerOverrides, false)
}

// doRead is do() minus the verify-mode mutating-verb gate. Used by the
// PostQuery* family for read-only operations that ride a mutating verb on
// the wire (GraphQL queries, JSON-RPC reads, POST-based search endpoints).
// The wire verb is still POST/PUT/PATCH so the server sees a real request,
// but the verify-mode short-circuit does not fire because the operation
// does not mutate remote state.
func (c *Client) doRead(method, path string, params map[string]string, body any, headerOverrides map[string]string) (json.RawMessage, int, error) {
	return c.doInternal(method, path, params, body, headerOverrides, true)
}

// doInternal is the shared implementation behind do() and doRead(). The
// readOnlyIntent flag is set by doRead() callers (read-only POST/PUT/PATCH
// operations like GraphQL queries) to skip the mutating-verb verify-mode
// gate. Plain do() callers leave it false and get the usual short-circuit.
func (c *Client) doInternal(method, path string, params map[string]string, body any, headerOverrides map[string]string, readOnlyIntent bool) (json.RawMessage, int, error) {
	// Verify-mode transport-layer gate. When the verifier (or any consumer
	// that sets PRINTING_PRESS_VERIFY=1) drives a mutating verb without
	// the LIVE_HTTP=1 opt-in, return a synthetic envelope without dialing,
	// minting auth, or touching the cache. The verify pipeline itself
	// sets both env vars in mock mode so its httptest server still sees
	// real requests; every other consumer gets a safe no-op.
	//
	// readOnlyIntent suppresses the gate for read-only operations that
	// happen to ride a mutating verb on the wire (GraphQL queries, JSON-RPC
	// reads, POST-based search endpoints). The handler-level annotation
	// `mcp:read-only: true` drives the codegen choice of doRead() vs do().
	//
	// Placement note: this fires BEFORE URL building, auth header
	// minting, and the success-branch invalidateCache() call below — so
	// no cache invalidation runs (no remote state changed) and no
	// client_credentials mint happens unnecessarily.
	if !readOnlyIntent && isMutatingVerb(method) && cliutil.IsVerifyEnv() && !cliutil.IsVerifyLiveHTTPEnv() {
		return verifyShortCircuitEnvelope(method, path), http.StatusOK, nil
	}
	requestBaseURL := c.baseURLForRequest()
	targetURL := requestBaseURL + path

	var bodyBytes []byte
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			return nil, 0, fmt.Errorf("marshaling body: %w", err)
		}
		bodyBytes = b
	}

	// Resolve auth material before the dry-run branch so --dry-run can preview
	// exactly what would be sent. Uses only cached credentials; a token that
	// requires a network refresh will be re-fetched on the live request path,
	// not during dry-run.
	authInfo, err := c.authForRequest()
	authHeader := authInfo.Value
	if err != nil {
		return nil, 0, err
	}

	// Build the request for dry-run display or actual execution
	if c.DryRun {
		return c.dryRun(method, targetURL, path, params, bodyBytes, headerOverrides, authHeader)
	}

	const maxRetries = 3
	var lastErr error

	for attempt := 0; attempt <= maxRetries; attempt++ {
		// Proactive rate limiting — wait before sending
		c.limiter.Wait()
		var bodyReader io.Reader
		if bodyBytes != nil {
			bodyReader = strings.NewReader(string(bodyBytes))
		}

		req, err := http.NewRequest(method, targetURL, bodyReader)
		if err != nil {
			return nil, 0, fmt.Errorf("creating request: %w", err)
		}
		if bodyBytes != nil {
			req.Header.Set("Content-Type", "application/json")
		}

		if params != nil {
			q := req.URL.Query()
			for k, v := range params {
				if v != "" {
					q.Set(k, v)
				}
			}
			req.URL.RawQuery = q.Encode()
		}

		if authHeader != "" {
			if authInfo.In == "query" {
				q := req.URL.Query()
				q.Set(authInfo.Name, authHeader)
				req.URL.RawQuery = q.Encode()
			} else {
				req.Header.Set(authInfo.Name, authHeader)
			}
		}
		if c.Config != nil {
			for k, v := range c.Config.Headers {
				req.Header.Set(k, v)
			}
		}
		// Per-endpoint header overrides (e.g., different API version per resource)
		for k, v := range headerOverrides {
			req.Header.Set(k, v)
		}
		binaryResponse := strings.EqualFold(req.Header.Get(BinaryResponseHeader), "true")
		if binaryResponse {
			req.Header.Del(BinaryResponseHeader)
		}
		if req.Header.Get("User-Agent") == "" {
			req.Header.Set("User-Agent", "tier-routing-golden-pp-cli/1.0.0")
		}
		// Go's net/http omits Accept by default; browsers, curl, and other
		// stdlibs always send it. Fingerprint-checking WAFs (Imperva, Akamai,
		// Cloudflare bot-mode, DataDome) flag the absence as a bot signal
		// and answer with empty-body 5xx, 403, or a challenge redirect
		// depending on vendor and rule tier. The value is application/json
		// rather than */* because strict-JSON APIs (Zendesk, Atlassian REST,
		// Salesforce) return 415 on anything that isn't literally
		// application/json; specs that need a different content type
		// (vendor mediatypes, XML, HTML) declare it via RequiredHeaders or
		// per-endpoint headerOverrides, both of which run before this
		// if-empty default.
		if req.Header.Get("Accept") == "" {
			if binaryResponse {
				req.Header.Set("Accept", "*/*")
			} else {
				req.Header.Set("Accept", "application/json")
			}
		}

		resp, err := c.HTTPClient.Do(req)
		if err != nil {
			lastErr = fmt.Errorf("%s %s: %w", method, path, err)
			continue
		}

		respBody, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, 0, fmt.Errorf("reading response: %w", err)
		}

		// Success
		if resp.StatusCode < 400 {
			c.limiter.OnSuccess()
			if method != http.MethodGet && !c.DryRun {
				c.invalidateCache()
			}
			// Non-textual bodies (PDF, zip, image, octet-stream) must not be
			// run through the JSON sanitizer or returned as raw json.RawMessage
			// — return a self-describing base64 envelope instead. Textual and
			// JSON responses fall through to the unchanged path.
			if isBinaryResponseContentType(resp.Header.Get("Content-Type")) {
				env, encErr := wrapBinaryResponse(resp.Header.Get("Content-Type"), respBody)
				if encErr != nil {
					return nil, 0, encErr
				}
				return env, resp.StatusCode, nil
			}
			return json.RawMessage(sanitizeJSONResponse(respBody)), resp.StatusCode, nil
		}

		if !binaryResponse {
			respBody = sanitizeJSONResponse(respBody)
		}

		apiErr := &APIError{
			Method:     method,
			Path:       path,
			StatusCode: resp.StatusCode,
			Body:       truncateBody(respBody),
		}

		// Rate limited - adjust adaptive limiter and retry
		if resp.StatusCode == 429 && attempt < maxRetries {
			c.limiter.OnRateLimit()
			wait := cliutil.RetryAfter(resp)
			fmt.Fprintf(os.Stderr, "rate limited, waiting %s (attempt %d/%d, rate adjusted to %.1f req/s)\n", wait, attempt+1, maxRetries, c.limiter.Rate())
			time.Sleep(wait)
			lastErr = apiErr
			continue
		}

		// Server error - retry with backoff
		if resp.StatusCode >= 500 && attempt < maxRetries {
			wait := time.Duration(math.Pow(2, float64(attempt))) * time.Second
			fmt.Fprintf(os.Stderr, "server error %d, retrying in %s (attempt %d/%d)\n", resp.StatusCode, wait, attempt+1, maxRetries)
			time.Sleep(wait)
			lastErr = apiErr
			continue
		}

		// Client error or retries exhausted - return the error
		return nil, resp.StatusCode, apiErr
	}

	return nil, 0, lastErr
}

// dryRun prints the outgoing request exactly as the live path would send it,
// using the auth material already resolved in `do()`. Never triggers a network
// call — the caller is responsible for passing cached auth material only.
func (c *Client) dryRun(method, targetURL, path string, params map[string]string, body []byte, headerOverrides map[string]string, authHeader string) (json.RawMessage, int, error) {
	authInfo, _ := c.authForRequest()
	authInfo.Value = authHeader
	fmt.Fprintf(os.Stderr, "%s %s\n", method, targetURL)
	queryPrinted := false
	if params != nil {
		keys := make([]string, 0, len(params))
		for k := range params {
			if params[k] != "" {
				keys = append(keys, k)
			}
		}
		sort.Strings(keys)
		for _, k := range keys {
			sep := "?"
			if queryPrinted {
				sep = "&"
			}
			fmt.Fprintf(os.Stderr, "  %s%s=%s\n", sep, k, params[k])
			queryPrinted = true
		}
	}
	if authInfo.Value != "" && authInfo.In == "query" {
		sep := "?"
		if queryPrinted {
			sep = "&"
		}
		fmt.Fprintf(os.Stderr, "  %s%s=%s\n", sep, authInfo.Name, maskToken(authInfo.Value))
		queryPrinted = true
	}
	_ = queryPrinted
	if body != nil {
		var pretty json.RawMessage
		if json.Unmarshal(body, &pretty) == nil {
			enc := json.NewEncoder(os.Stderr)
			enc.SetIndent("  ", "  ")
			fmt.Fprintf(os.Stderr, "  Body:\n")
			enc.Encode(pretty)
		}
	}
	if authInfo.Value != "" && authInfo.In != "query" {
		fmt.Fprintf(os.Stderr, "  %s: %s\n", authInfo.Name, maskToken(authInfo.Value))
	}
	fmt.Fprintf(os.Stderr, "\n(dry run - no request sent)\n")
	return json.RawMessage(`{"dry_run": true}`), 0, nil
}

func (c *Client) ConfiguredTimeout() time.Duration {
	if c.HTTPClient != nil && c.HTTPClient.Timeout > 0 {
		return c.HTTPClient.Timeout
	}
	return 30 * time.Second
}

func (c *Client) authHeader() (string, error) {
	if c.Config == nil {
		return "", nil
	}
	return c.Config.AuthHeader(), nil
}

// binaryResponseEnvelope wraps a non-textual success body so it survives the
// json.RawMessage contract every consumer (CLI output, --json, MCP tools)
// depends on. Without it, raw bytes (PDF, zip, image) are corrupted by
// sanitizeJSONResponse and emitted as invalid JSON. The _pp_binary
// discriminator lets callers and agents detect and base64-decode the payload.
type binaryResponseEnvelope struct {
	PPBinary    bool   `json:"_pp_binary"`
	ContentType string `json:"content_type"`
	Encoding    string `json:"encoding"`
	Bytes       int    `json:"bytes"`
	Data        string `json:"data"`
}

// isBinaryResponseContentType reports whether a successful response with this
// Content-Type must be base64-wrapped instead of treated as text/JSON. It is
// deliberately narrow: JSON, */*, XML, and every text/* type (including
// text/html, so response_format:html CLIs are untouched) pass through
// unchanged. Only genuinely binary payloads are wrapped.
func isBinaryResponseContentType(ct string) bool {
	mt := strings.ToLower(strings.TrimSpace(ct))
	if i := strings.IndexByte(mt, ';'); i >= 0 {
		mt = strings.TrimSpace(mt[:i])
	}
	if mt == "" {
		return false
	}
	switch {
	case mt == "application/json", mt == "text/json", mt == "*/*":
		return false
	case strings.HasPrefix(mt, "text/"):
		return false
	case strings.HasSuffix(mt, "+json"), strings.HasSuffix(mt, "+xml"):
		return false
	case mt == "application/xml", mt == "application/xhtml+xml":
		return false
	case mt == "application/javascript", mt == "application/ecmascript",
		mt == "application/x-www-form-urlencoded", mt == "application/graphql":
		return false
	}
	return true
}

// wrapBinaryResponse marshals body into a self-describing base64 envelope.
func wrapBinaryResponse(ct string, body []byte) (json.RawMessage, error) {
	out, err := json.Marshal(binaryResponseEnvelope{
		PPBinary:    true,
		ContentType: ct,
		Encoding:    "base64",
		Bytes:       len(body),
		Data:        base64.StdEncoding.EncodeToString(body),
	})
	if err != nil {
		return nil, fmt.Errorf("encoding binary response: %w", err)
	}
	return json.RawMessage(out), nil
}

// sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
// response bodies so that downstream JSON parsing succeeds. For clean JSON
// responses these checks are no-ops.
func sanitizeJSONResponse(body []byte) []byte {
	// UTF-8 BOM
	body = bytes.TrimPrefix(body, []byte("\xEF\xBB\xBF"))

	// JSONP/XSSI prefixes, ordered longest-first where prefixes overlap
	prefixes := [][]byte{
		[]byte(")]}'\n"),
		[]byte(")]}'"),
		[]byte("{}&&"),
		[]byte("for(;;);"),
		[]byte("while(1);"),
	}
	for _, p := range prefixes {
		if bytes.HasPrefix(body, p) {
			body = bytes.TrimPrefix(body, p)
			body = bytes.TrimLeft(body, " \t\r\n")
			break
		}
	}
	return body
}

// maskToken redacts all but the last 4 characters of a token for safe display.
func maskToken(token string) string {
	if token == "" {
		return ""
	}
	if len(token) <= 4 {
		return "****"
	}
	return "****" + token[len(token)-4:]
}

func truncateBody(b []byte) string {
	const maxBytes = 4096
	if len(b) <= maxBytes {
		return string(b)
	}
	return strings.ToValidUTF8(string(b[:maxBytes]), "") + "..."
}
