package app

import (
	"strconv"
	"strings"

	"kjol/httputil"
	. "kjol/wasmruntime/vdom"
	ui "kjol/webui"
)

// Quote is the payload the /api/quotes endpoint returns. The server encodes a
// []Quote with httputil.RespondGob; the client decodes it straight back into
// []Quote — the SAME Go type, no JSON, no hand-written unmarshalling.
type Quote struct {
	Author string
	Text   string
}

// repoInfo is a subset of GitHub's repo JSON (a third-party API), tagged for
// json decoding.
type repoInfo struct {
	FullName    string `json:"full_name"`
	Description string `json:"description"`
	Stars       int    `json:"stargazers_count"`
}

//gowasm:page /wasm/data layout=app static
func DataPage(d Deps) func() *VNode {
	// (1) gob from our own server via httputil.RespondGob / FetchGob.
	quotes := NewSignal([]Quote{})
	qLoading := NewSignal(true)
	qErr := NewSignal("")
	// (2) JSON from a third-party API (GitHub), for a user-entered repo.
	repo := NewSignal(repoInfo{})
	rLoading := NewSignal(true)
	rErr := NewSignal("")
	repoQuery := NewSignal("golang/go")
	started := false

	// fetchRepo loads owner/name from the GitHub API into the repo signal.
	fetchRepo := func(q string) {
		q = strings.Trim(strings.TrimSpace(q), "/")
		if q == "" {
			rErr.Set("enter a repo as owner/name")
			rLoading.Set(false)
			return
		}
		rErr.Set("")
		rLoading.Set(true)
		httputil.FetchJSON("https://api.github.com/repos/"+q, func(r repoInfo, err error) {
			if err != nil {
				rErr.Set(err.Error())
			} else {
				repo.Set(r)
			}
			rLoading.Set(false)
		})
	}

	return func() *VNode {
		// Fire the initial fetches once, on the client (no transport on the server,
		// so SSR ships the loading state and the client takes over).
		if !started {
			started = true
			httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
				if err != nil {
					qErr.Set(err.Error())
				} else {
					quotes.Set(qs)
				}
				qLoading.Set(false)
			})
			fetchRepo(repoQuery.Get())
		}

		return docPage("Rendering", "Data fetching",
			"Fetching happens in the browser, so a server-rendered page ships its LOADING state and the "+
				"client fills it in. Two shapes are shown here: gob against your own server, where the same "+
				"Go type crosses the wire untranslated, and JSON against somebody else's API.",

			docSection("gob", "gob — the same Go type on both ends",
				prose("Your server already speaks Go and so does your client, so there is no reason to translate "+
					"through JSON in between. The handler answers with httputil.RespondGob([]Quote) and the "+
					"client decodes straight back into []Quote — one type, declared once, with no tags and no "+
					"hand-written unmarshalling to drift out of sync with it."),
				code("app/data.go + server/main.go", gobSnippet),
				demo("GET /api/quotes, decoded into []Quote",
					quotesBody(qLoading.Get(), qErr.Get(), quotes.Get()),
				),
			),

			docSection("json", "JSON — for everyone else's API",
				prose("A third-party API does not speak gob, so httputil.FetchJSON decodes into a tagged struct "+
					"the ordinary way. Enter a repository and the browser calls api.github.com directly."),
				demo("GET api.github.com/repos/…, decoded into a tagged struct",
					row("mb-4 flex items-end gap-2",
						row("flex grow flex-col gap-1 max-w-sm",
							ui.FormLabel(ui.FormLabelProps{}, Text("GitHub repo (owner/name)")),
							ui.FormInput(ui.FormInputProps{
								Value:       repoQuery.Get(),
								Placeholder: "golang/go",
								OnInput:     func(v string) { repoQuery.Set(v) },
							}),
						),
						ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Fetch", OnClick: func() { fetchRepo(repoQuery.Get()) }}),
					),
					repoBody(rLoading.Get(), rErr.Get(), repo.Get()),
				),
			),

			docSection("ssr", "What the server renders",
				prose("This route is static, so the server pre-renders it — but there is no fetch on the server: "+
					"no transport is installed there, and inventing one would mean the server quietly making "+
					"requests on the user's behalf. So a fetch started during SSR does nothing at all, the page "+
					"renders its spinner, and the client runs the fetch for real once it has hydrated."),
				note("A fetch that fails on the server is a bug in the framework, not in your page",
					"An earlier version of this returned an error from SSR, and every static page that fetched "+
						"anything rendered \"no client transport installed\" into its own HTML. Loading is the "+
						"correct server-side answer to \"have you fetched this yet?\"."),
				apiTable(
					apiRow{"httputil.RespondGob", "Server: write a Go value as gob."},
					apiRow{"httputil.FetchGob", "Client: decode a gob response into a Go value."},
					apiRow{"httputil.FetchJSON", "Client: decode a JSON response into a tagged struct."},
					apiRow{"httputil.SetClientTransport", "Override the transport — a base URL, auth headers. The runtime installs a fetch-based one for you."},
				),
			),
		)
	}
}

const gobSnippet = `// One type. Both ends. No tags, no JSON.
type Quote struct {
    Author string
    Text   string
}

// --- server ---
mux.HandleFunc("GET /api/quotes", func(w http.ResponseWriter, r *http.Request) {
    httputil.RespondGob(w, http.StatusOK, sampleQuotes())   // []Quote
})

// --- client ---
httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
    if err != nil { qErr.Set(err.Error()); return }
    quotes.Set(qs)                                          // []Quote
})`

func quotesBody(loading bool, failed string, quotes []Quote) *VNode {
	switch {
	case failed != "":
		return ui.Alert(ui.AlertRed, "Fetch failed", Text(failed))
	case loading:
		return ui.Loader()
	default:
		cards := make([]*VNode, 0, len(quotes))
		for _, q := range quotes {
			cards = append(cards, ui.BorderCard("",
				P(Attr("class", "text-ink"), Text("“"+q.Text+"”")),
				P(Attr("class", "mt-2 text-sm text-ink-muted"), Text("— "+q.Author)),
			))
		}
		return row("grid gap-3 sm:grid-cols-2", cards...)
	}
}

func repoBody(loading bool, failed string, r repoInfo) *VNode {
	switch {
	case failed != "":
		return ui.Alert(ui.AlertRed, "Fetch failed", Text(failed))
	case loading:
		return ui.Loader()
	default:
		return ui.BorderCard("",
			row("flex items-center gap-2",
				Strong(Attr("class", "text-ink"), Text(r.FullName)),
				ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber}, Text("★ "+strconv.Itoa(r.Stars))),
			),
			P(Attr("class", "mt-2 text-sm text-ink-soft"), Text(r.Description)),
		)
	}
}
