//go:build js && wasm

package wasmruntime

import (
	"errors"
	"fmt"
	"strings"
	"sync"
	"syscall/js"

	"kjol/httputil"
)

// Installing the transport here (rather than leaving it to each app's main)
// keeps httputil's invariant true by construction: FetchGob / FetchJSON have a
// transport exactly when they are running in the browser. That is what lets them
// no-op during SSR instead of reporting a failure. Apps that need custom headers
// or a base URL can wrap FetchBytes with httputil.SetClientTransport at startup.
func init() { httputil.SetClientTransport(FetchBytes) }

// absURL resolves a relative path against the current origin (a browser resolves
// relative fetch URLs itself, but building the absolute URL keeps behavior
// uniform — and lets non-browser hosts, e.g. tests, fetch too).
func absURL(url string) string {
	if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
		return url
	}
	loc := js.Global().Get("location")
	return loc.Get("protocol").String() + "//" + loc.Get("host").String() + url
}

// Fetch performs an HTTP GET via the browser Fetch API and returns the response
// body as a string. It bridges a JS Promise into Go: it registers then/catch
// callbacks and blocks the calling goroutine on a channel until the request
// settles.
//
// Because it blocks, call it from a goroutine — never from a reactive callback
// or event handler — and write the result into signals when it returns, e.g.:
//
//	go func() {
//	    body, err := Fetch(url)
//	    ...
//	    data.Set(...) // updates the UI reactively
//	}()
func Fetch(url string) (string, error) {
	var (
		once                  sync.Once
		done                  = make(chan struct{})
		body                  string
		ferr                  error
		onResp, onText, onErr js.Func
	)
	finish := func() { once.Do(func() { close(done) }) }

	onErr = js.FuncOf(func(this js.Value, args []js.Value) any {
		msg := "fetch error"
		if len(args) > 0 && args[0].Truthy() {
			e := args[0]
			if m := e.Get("message"); m.Truthy() {
				msg = m.String()
			} else {
				msg = e.Call("toString").String()
			}
		}
		ferr = errors.New(msg)
		finish()
		return nil
	})

	onText = js.FuncOf(func(this js.Value, args []js.Value) any {
		if len(args) > 0 {
			body = args[0].String()
		}
		finish()
		return nil
	})

	onResp = js.FuncOf(func(this js.Value, args []js.Value) any {
		resp := args[0]
		if !resp.Get("ok").Bool() {
			ferr = fmt.Errorf("HTTP %d", resp.Get("status").Int())
			finish()
			return nil
		}
		// resp.text() -> Promise<string>
		resp.Call("text").Call("then", onText).Call("catch", onErr)
		return nil
	})

	js.Global().Call("fetch", absURL(url)).Call("then", onResp).Call("catch", onErr)

	<-done
	onResp.Release()
	onText.Release()
	onErr.Release()
	return body, ferr
}

// FetchBytes is like Fetch but returns the raw response body as bytes (via
// resp.arrayBuffer()), for binary payloads such as gob (Fetch's resp.text()
// would corrupt non-UTF-8 data). It blocks the calling goroutine the same way —
// call it from a goroutine and write the result into signals when it returns.
func FetchBytes(url string) ([]byte, error) {
	var (
		once                 sync.Once
		done                 = make(chan struct{})
		body                 []byte
		ferr                 error
		onResp, onBuf, onErr js.Func
	)
	finish := func() { once.Do(func() { close(done) }) }

	onErr = js.FuncOf(func(this js.Value, args []js.Value) any {
		msg := "fetch error"
		if len(args) > 0 && args[0].Truthy() {
			e := args[0]
			if m := e.Get("message"); m.Truthy() {
				msg = m.String()
			} else {
				msg = e.Call("toString").String()
			}
		}
		ferr = errors.New(msg)
		finish()
		return nil
	})

	onBuf = js.FuncOf(func(this js.Value, args []js.Value) any {
		if len(args) > 0 {
			arr := js.Global().Get("Uint8Array").New(args[0])
			b := make([]byte, arr.Get("length").Int())
			js.CopyBytesToGo(b, arr)
			body = b
		}
		finish()
		return nil
	})

	onResp = js.FuncOf(func(this js.Value, args []js.Value) any {
		resp := args[0]
		if !resp.Get("ok").Bool() {
			ferr = fmt.Errorf("HTTP %d", resp.Get("status").Int())
			finish()
			return nil
		}
		resp.Call("arrayBuffer").Call("then", onBuf).Call("catch", onErr)
		return nil
	})

	js.Global().Call("fetch", absURL(url)).Call("then", onResp).Call("catch", onErr)

	<-done
	onResp.Release()
	onBuf.Release()
	onErr.Release()
	return body, ferr
}
