package httputil

import (
	"bytes"
	"encoding/gob"
	"encoding/json"
)

// Client-side fetch helpers — the counterparts to RespondGob / RespondJSON. In a
// gowasm app the server and the WebAssembly client share the same Go types, so
// the server encodes with RespondGob(data) and the client decodes straight back
// into that type with FetchGob — no JSON, no hand-written unmarshalling. These
// are neutral (they only touch encoding/*), so app code can call them while
// staying SSR-able; the actual HTTP transport is client-only and installed at
// startup by the wasm runtime (SetClientTransport), the inversion vdom.Schedule
// uses.

// clientTransport does an HTTP GET returning the raw body. The wasm runtime
// installs it in its init, so it is always set in the browser and always nil
// everywhere else — "nil transport" therefore means "not the client", which is
// what makes the SSR behavior in fetchInto safe.
var clientTransport func(url string) ([]byte, error)

// SetClientTransport installs the HTTP GET used by FetchGob / FetchJSON. The
// wasm client runtime installs wasmruntime.FetchBytes automatically; call this
// only to wrap it (auth headers, a base URL, a test double).
func SetClientTransport(get func(url string) ([]byte, error)) { clientTransport = get }

func fetchInto[T any](url string, cb func(T, error), decode func([]byte, *T) error) {
	// Off the client (SSR of a static page, a native build) there is no transport
	// and no browser to fetch with, so the request simply never happens: cb is not
	// called, and the caller stays in the loading state it started in. That is
	// what SSR should ship — a spinner or skeleton, not a "fetch failed" error the
	// server was never able to avoid. The client re-runs the fetch for real on its
	// first render after hydration.
	if clientTransport == nil {
		return
	}
	go func() {
		var out T
		body, err := clientTransport(url)
		if err != nil {
			cb(out, err)
			return
		}
		if err := decode(body, &out); err != nil {
			cb(out, err)
			return
		}
		cb(out, nil)
	}()
}

// FetchGob GETs url and gob-decodes the body into T (pair with RespondGob), then
// calls cb(result, err) on a background goroutine — safe to call from a render
// or event handler; set the result into a signal in the callback. T is inferred
// from the callback:
//
//	httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
//	    if err != nil { /* … */ } else { quotes.Set(qs) }
//	})
//
// Fetching is client-only: during SSR this does nothing and cb is never called,
// so a page that starts out loading renders its spinner into the static HTML and
// fetches for real once the client takes over.
func FetchGob[T any](url string, cb func(T, error)) {
	fetchInto(url, cb, func(b []byte, out *T) error {
		return gob.NewDecoder(bytes.NewReader(b)).Decode(out)
	})
}

// FetchJSON GETs url and json-decodes the body into T — for third-party JSON
// APIs (or your own RespondJSON). Same callback/goroutine shape as FetchGob.
func FetchJSON[T any](url string, cb func(T, error)) {
	fetchInto(url, cb, func(b []byte, out *T) error {
		return json.Unmarshal(b, out)
	})
}
