package template

import (
	"encoding/json"
	"fmt"
	"html"
	"maps"
	"os"
	"strconv"
	"strings"
	"text/template"
	"time"

	"gh.tarampamp.am/error-pages/internal/appmeta"
	"gh.tarampamp.am/error-pages/l10n"
)

var builtInFunctions = template.FuncMap{ //nolint:gochecknoglobals
	// the current time in unix format (seconds since 1970 UTC):
	//	`{{ nowUnix }}`	// `1631610000`
	"nowUnix": func() int64 { return time.Now().Unix() },

	// current hostname:
	//	`{{ hostname }}`	// `localhost`
	"hostname": func() string { h, _ := os.Hostname(); return h }, //nolint:nlreturn

	// json-serialized value (safe to use with any type):
	//	`{{ json "test" }}`	// `"test"`
	//	`{{ json 42 }}`	// `42`
	"json": func(v any) string { b, _ := json.Marshal(v); return string(b) }, //nolint:nlreturn,errchkjson

	// cast any type to int, or return 0 if it's not possible:
	//	`{{ int "42" }}`	// `42`
	//	`{{ int 42 }}`	// `42`
	//	`{{ int 3.14 }}`	// `3`
	//	`{{ int "test" }}`	// `0`
	//	`{{ int "42test" }}`	// `0`
	"int": func(v any) int { // cast any type to int, or return 0 if it's not possible
		switch v := v.(type) {
		case string:
			if i, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
				return i
			}
		case int:
			return v
		case int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
			if i, err := strconv.Atoi(fmt.Sprintf("%d", v)); err == nil { // not effective, but safe
				return i
			}
		case float32, float64:
			if i, err := strconv.ParseFloat(fmt.Sprintf("%f", v), 32); err == nil { // not effective, but safe
				return int(i)
			}
		case fmt.Stringer:
			if i, err := strconv.Atoi(v.String()); err == nil {
				return i
			}
		}

		return 0
	},

	// current application version:
	//	`{{ version }}`	// `1.0.0`
	"version": appmeta.Version,

	// counts the number of non-overlapping instances of substr in s:
	//	`{{ strCount "test" "t" }}`	// `2`
	"strCount": strings.Count,

	// reports whether substr is within s:
	//	`{{ strContains "test" "es" }}`	// `true`
	//	`{{ strContains "test" "ez" }}`	// `false`
	"strContains": strings.Contains,

	// returns a slice of the string s, with all leading and trailing white space removed:
	//	`{{ strTrimSpace "  test  " }}`	// `test`
	"strTrimSpace": strings.TrimSpace,

	// returns s without the provided leading prefix string:
	//	`{{ strTrimPrefix "test" "te" }}`	// `st`
	"strTrimPrefix": strings.TrimPrefix,

	// returns s without the provided trailing suffix string:
	//	`{{ strTrimSuffix "test" "st" }}`	// `te`
	"strTrimSuffix": strings.TrimSuffix,

	// returns a copy of the string s with all non-overlapping instances of old replaced by new:
	//	`{{ strReplace "test" "t" "z" }}`	// `zesz`
	"strReplace": strings.ReplaceAll,

	// returns the index of the first instance of substr in s, or -1 if substr is not present in s:
	//	`{{ strIndex "barfoobaz" "foo" }}`	// `3`
	"strIndex": strings.Index,

	// splits the string s around each instance of one or more consecutive white space characters:
	//	`{{ strFields "foo bar baz" }}`	// `[foo bar baz]`
	"strFields": strings.Fields,

	// retrieves the value of the environment variable named by the key:
	//	`{{ env "SHELL" }}`	// `/bin/bash`
	"env": os.Getenv,

	// escapes special characters like "<" to become "&lt;":
	//	`{{ escape "<test>" }}`	// `&lt;test&gt;`
	"escape": html.EscapeString,

	// returns the content of the JS file with a script for automatic error page localization:
	//	`{{ l10nScript }}`	// `Object.defineProperty(window, ...`
	"l10nScript": l10n.L10n,
}

func Render(content string, props Props) (string, error) {
	var fns = maps.Clone(builtInFunctions)

	maps.Copy(fns, template.FuncMap{ // add custom functions
		"hide_details": func() bool { return !props.ShowRequestDetails }, // inverted logic
		"l10n_enabled": func() bool { return !props.L10nDisabled },       // inverted logic
	})

	// allow the direct access to the properties tokens, e.g. `{{ service_port | json }}`
	// instead of `{{ .service_port | json }}`
	for k, v := range props.Values() {
		fns[k] = func() any { return v }
	}

	tmpl, tErr := template.New("template").Funcs(fns).Parse(content)
	if tErr != nil {
		return "", fmt.Errorf("failed to parse template: %w", tErr)
	}

	var buf strings.Builder

	if err := tmpl.Execute(&buf, props); err != nil {
		return "", err
	}

	return buf.String(), nil
}