package content

import (
	"fmt"
	"html/template"
	"strings"
	"unicode/utf16"

	"thermograph/frontend/internal/config"
	"thermograph/frontend/internal/format"
)

// FuncMap builds the template helper set (the Jinja globals/filters
// content.py registered on its Environment). Pass it to render.New at boot —
// html/template resolves names at parse time.
//
// Unit-dependent helpers (temp, temp_bare) take the active unit as their
// FIRST argument: the Python scoped it in a ContextVar, but html/template
// FuncMaps are engine-global, so per-request state must arrive through the
// call. Every page context carries the active unit under the "unit" key
// (same value as "unit_default"), so template call sites are
// {{temp $.unit .high_f}} where Jinja had {{ temp(m.high_f) }}.
func FuncMap(cfg config.Config) template.FuncMap {
	return template.FuncMap{
		// temp / temp_bare / temp_class accept the loosely-typed values the
		// templates hand them: nullable *float64 payload fields, plain
		// float64s, and int literals (Jinja's {{ temp(-10) }}).
		"temp": func(unit, f any) template.HTML {
			return format.Temp(unitArg(unit), floatArg(f))
		},
		"temp_bare": func(unit, f any) template.HTML {
			return format.TempBare(unitArg(unit), floatArg(f))
		},
		"temp_class": func(f any) string {
			return format.TempClass(floatArg(f))
		},
		// The `ordinal` filter: {{ c.percentile|ordinal }} -> {{ordinal .percentile}}.
		"ordinal": format.PctOrdinal,
		// Search-engine ownership-verification <meta> tags, from env (empty
		// when unset). No backend dependency — the same two variables the
		// Python read, resolved once into config.
		"head_verify": func() template.HTML {
			return headVerifyHTML(cfg.GoogleVerify, cfg.BingVerify)
		},
		// Jinja's |tojson (glossary_term.html.j2's inline JSON-LD): JSON with
		// the HTML-unsafe characters escaped so it can sit inside <script>.
		"tojson": toJSON,
		// A visible HTML comment that survives html/template's parser. A
		// LITERAL <!-- --> in a .tmpl file does NOT reach the output --
		// html/template strips real HTML comments while parsing (verified:
		// a template consisting of only `<p>a</p><!-- x --><p>b</p>` renders
		// as `<p>a</p><p>b</p>`). Marking the string template.HTML makes it a
		// trusted content INSERTION rather than markup the parser interprets,
		// so it passes through untouched. Only ever called with developer-
		// authored literals in the templates themselves, never request data.
		"comment": func(s string) template.HTML {
			return template.HTML("<!-- " + s + " -->")
		},
		// The same problem as `comment`, one layer down: html/template's
		// contextual escaper ALSO strips JavaScript `//`/`/* */` comments from
		// <script> bodies while parsing (verified the same way: an inline
		// script containing only a `// comment` line and a `var` statement
		// loses the comment line entirely on render, with zero {{ }} actions
		// anywhere nearby). Needs template.JS specifically, not template.HTML
		// -- inside a <script> context html/template treats template.HTML as
		// untrusted and re-escapes it as a quoted JS value (the same bug
		// class the JSONLDStr sites hit); template.JS is what means "emit
		// this JS source as-is".
		"jscomment": func(s string) template.JS {
			return template.JS("// " + s)
		},
	}
}

// unitArg coerces the template-side unit value ("" / "C" / "F", as string or
// format.Unit) — the context stores it as a plain string for rendering the
// data-unit-default attribute.
func unitArg(v any) format.Unit {
	switch u := v.(type) {
	case format.Unit:
		return u
	case string:
		return format.Unit(u)
	case nil:
		return ""
	}
	return ""
}

// floatArg coerces template arguments to the nullable float the format
// helpers take: nil / nil *float64 mean "missing" (rendered as the em-dash),
// numeric literals are Go ints inside templates.
func floatArg(v any) *float64 {
	switch x := v.(type) {
	case nil:
		return nil
	case *float64:
		return x
	case float64:
		return &x
	case int:
		f := float64(x)
		return &f
	}
	return nil
}

// headVerifyHTML is the port of content.py's head_verify_html: one <meta>
// per configured verification token, joined with the newline + two-space
// indent the Jinja Markup join produced. Token values are attribute-escaped
// exactly like markupsafe's Markup.format did.
func headVerifyHTML(google, bing string) template.HTML {
	var metas []string
	if google != "" {
		metas = append(metas, fmt.Sprintf(`<meta name="google-site-verification" content="%s">`,
			template.HTMLEscapeString(google)))
	}
	if bing != "" {
		metas = append(metas, fmt.Sprintf(`<meta name="msvalidate.01" content="%s">`,
			template.HTMLEscapeString(bing)))
	}
	return template.HTML(strings.Join(metas, "\n  "))
}

// toJSON mirrors Jinja's |tojson filter (jinja2.utils.htmlsafe_json_dumps):
// json.dumps with ensure_ascii (every non-ASCII rune as a backslash-uXXXX
// escape, surrogate pairs beyond the BMP), then the HTML-unsafe characters
// <, >, & and ' replaced with their backslash-u00XX escapes so the result is
// safe inside a <script> block. Byte-for-byte the same output for the string
// values the templates feed it.
func toJSON(v any) (template.HTML, error) {
	s, ok := v.(string)
	if !ok {
		// The templates only |tojson strings (term, page_description). Fail
		// loudly if that changes rather than guessing at dict key order.
		return "", fmt.Errorf("tojson: unsupported type %T", v)
	}
	var b strings.Builder
	b.WriteByte('"')
	for _, r := range s {
		switch r {
		case '"':
			b.WriteString(`\"`)
		case '\\':
			b.WriteString(`\\`)
		case '\n':
			b.WriteString(`\n`)
		case '\r':
			b.WriteString(`\r`)
		case '\t':
			b.WriteString(`\t`)
		case '\b':
			b.WriteString(`\b`)
		case '\f':
			b.WriteString(`\f`)
		case '<', '>', '&', '\'':
			// Jinja's htmlsafe_json_dumps post-replaces these four with
			// their backslash-u00XX escapes so the JSON can sit inside a
			// <script> block without ever closing it.
			fmt.Fprintf(&b, `\u%04x`, r)
		default:
			switch {
			case r < 0x20:
				fmt.Fprintf(&b, `\u%04x`, r)
			case r < 0x80: // ASCII (incl. DEL) passes through, like json.dumps
				b.WriteRune(r)
			case r <= 0xffff:
				fmt.Fprintf(&b, `\u%04x`, r)
			default:
				hi, lo := utf16.EncodeRune(r)
				fmt.Fprintf(&b, `\u%04x\u%04x`, hi, lo)
			}
		}
	}
	b.WriteByte('"')
	return template.HTML(b.String()), nil
}
