// Command gendocssearch builds the client-side search index for the static
// docs site (examples/docs/). It scans every *.html page, pulls the page
// title (<h1>, both en + zh data-lang variants) and all section headings
// (<h2>/<h3>, bilingual), and writes examples/docs/search-index.js as a
// single `window.SEEK_DOCS_INDEX = [...]` assignment that search.js consumes.
//
// It is a build-time tool, NOT part of the binary: the docs site has no
// runtime index (zero-daemon, works on file://), so this must be re-run
// whenever docs content changes:
//
//	go run ./scripts/gendocssearch
//
// Output is deterministic (pages sorted by filename, headings de-duped in
// document order) so re-running with unchanged input produces byte-identical
// output — no spurious diffs. Stdlib only.
package main

import (
	"encoding/json"
	"fmt"
	"html"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

type page struct {
	Page     string   `json:"page"`     // filename, e.g. "commands.html"
	Title    string   `json:"title"`    // display title (zh; falls back to en)
	TitleEn  string   `json:"titleEn"`  // english title (for en-mode display + search)
	Headings []string `json:"headings"` // h2/h3 section titles, both languages, de-duped
}

var (
	h1Re  = regexp.MustCompile(`(?is)<h1[^>]*\bdata-lang="(en|zh)"[^>]*>(.*?)</h1>`)
	hRe   = regexp.MustCompile(`(?is)<h[23][^>]*>(.*?)</h[23]>`)
	tagRe = regexp.MustCompile(`(?s)<[^>]*>`)
	wsRe  = regexp.MustCompile(`\s+`)
)

// clean strips inner tags, decodes HTML entities, and collapses whitespace.
func clean(s string) string {
	s = tagRe.ReplaceAllString(s, " ")
	s = html.UnescapeString(s)
	s = wsRe.ReplaceAllString(s, " ")
	return strings.TrimSpace(s)
}

func main() {
	dir := "examples/docs"
	if len(os.Args) > 1 {
		dir = os.Args[1]
	}

	matches, err := filepath.Glob(filepath.Join(dir, "*.html"))
	if err != nil {
		fail(err)
	}
	sort.Strings(matches)

	var pages []page
	for _, m := range matches {
		base := filepath.Base(m)
		if base == "index.html" {
			continue // the docs home hosts the search; it isn't a search result
		}
		raw, err := os.ReadFile(m)
		if err != nil {
			fail(err)
		}
		body := string(raw)

		p := page{Page: base}
		for _, mm := range h1Re.FindAllStringSubmatch(body, -1) {
			switch mm[1] {
			case "zh":
				p.Title = clean(mm[2])
			case "en":
				p.TitleEn = clean(mm[2])
			}
		}
		if p.Title == "" {
			p.Title = p.TitleEn // pages without a zh title fall back to en
		}

		seen := map[string]bool{}
		for _, mm := range hRe.FindAllStringSubmatch(body, -1) {
			h := clean(mm[1])
			if h == "" || seen[h] {
				continue
			}
			seen[h] = true
			p.Headings = append(p.Headings, h)
		}
		if p.Headings == nil {
			p.Headings = []string{} // emit [] not null for stable JSON
		}
		pages = append(pages, p)
	}

	js, err := json.MarshalIndent(pages, "", "  ")
	if err != nil {
		fail(err)
	}

	out := filepath.Join(dir, "search-index.js")
	var b strings.Builder
	b.WriteString("// AUTO-GENERATED by scripts/gendocssearch — do not edit by hand.\n")
	b.WriteString("// Regenerate after docs changes: go run ./scripts/gendocssearch\n")
	b.WriteString("window.SEEK_DOCS_INDEX = ")
	b.Write(js)
	b.WriteString(";\n")
	if err := os.WriteFile(out, []byte(b.String()), 0o644); err != nil {
		fail(err)
	}
	fmt.Printf("wrote %s — %d pages\n", out, len(pages))
}

func fail(err error) {
	fmt.Fprintln(os.Stderr, "gendocssearch:", err)
	os.Exit(1)
}
