package funcs

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"math"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/yuin/goldmark"
	highlighting "github.com/yuin/goldmark-highlighting"
	"golang.org/x/text/language"
	"golang.org/x/text/message"
)

type WidgetOption struct {
	Key   string
	Value string
}

const (
	day  = 24 * time.Hour
	year = 365 * day
)

var printer = message.NewPrinter(language.English)

var TemplateFuncs = template.FuncMap{
	// Time functions
	"now":            time.Now,
	"timeSince":      time.Since,
	"timeUntil":      time.Until,
	"formatTime":     formatTime,
	"approxDuration": approxDuration,

	// String functions
	"uppercase":         strings.ToUpper,
	"lowercase":         strings.ToLower,
	"pluralize":         pluralize,
	"slugify":           slugify,
	"safeHTML":          safeHTML,
	"renderFieldValue":  renderFieldValue,
	"renderFieldValues": renderFieldValues,
	"stringToArray":     stringToArray,

	// Slice functions
	"join": strings.Join,

	// Number functions
	"incr":        incr,
	"decr":        decr,
	"addI":        addI,
	"subI":        subI,
	"formatInt":   formatInt,
	"formatFloat": formatFloat,

	// Boolean functions
	"yesno": yesno,

	// URL functions
	"urlSetParam": urlSetParam,
	"urlDelParam": urlDelParam,

	//Markdown render
	"markdownfy": markdownfy,

	"linksList": linksList,

	//Widgets
	"widget_relation_type": widget_relation_type,
	"widget_text":          widget_text,
	"widget_select":        widget_select,
	"widget_slim_select":   widget_slim_select,
	"widget_checkbox":      widget_checkbox,
	"widget_checkboxes":    widget_checkboxes,
	"field_widget":         field_widget,

	//Map
	"map": tmap,
}

func formatTime(format string, t time.Time) string {
	return t.Format(format)
}

func approxDuration(d time.Duration) string {
	if d < time.Second {
		return "less than 1 second"
	}

	ds := int(math.Round(d.Seconds()))
	if ds == 1 {
		return "1 second"
	} else if ds < 60 {
		return fmt.Sprintf("%d seconds", ds)
	}

	dm := int(math.Round(d.Minutes()))
	if dm == 1 {
		return "1 minute"
	} else if dm < 60 {
		return fmt.Sprintf("%d minutes", dm)
	}

	dh := int(math.Round(d.Hours()))
	if dh == 1 {
		return "1 hour"
	} else if dh < 24 {
		return fmt.Sprintf("%d hours", dh)
	}

	dd := int(math.Round(float64(d / day)))
	if dd == 1 {
		return "1 day"
	} else if dd < 365 {
		return fmt.Sprintf("%d days", dd)
	}

	dy := int(math.Round(float64(d / year)))
	if dy == 1 {
		return "1 year"
	}

	return fmt.Sprintf("%d years", dy)
}

func pluralize(count any, singular string, plural string) (string, error) {
	n, err := toInt64(count)
	if err != nil {
		return "", err
	}

	if n == 1 {
		return singular, nil
	}

	return plural, nil
}

func slugify(s string) string {
	var buf bytes.Buffer

	for _, r := range s {
		switch {
		case r > unicode.MaxASCII:
			continue
		case unicode.IsLetter(r):
			buf.WriteRune(unicode.ToLower(r))
		case unicode.IsDigit(r), r == '_', r == '-':
			buf.WriteRune(r)
		case unicode.IsSpace(r):
			buf.WriteRune('-')
		}
	}

	return buf.String()
}

func safeHTML(s string) template.HTML {
	return template.HTML(s)
}

func incr(i any) (int64, error) {
	n, err := toInt64(i)
	if err != nil {
		return 0, err
	}

	n++
	return n, nil
}

func decr(i any) (int64, error) {
	n, err := toInt64(i)
	if err != nil {
		return 0, err
	}

	n--
	return n, nil
}

func addI(i1 any, i2 any) (int64, error) {
	i1_64, err := toInt64(i1)
	if err != nil {
		return 0, err
	}
	i2_64, err := toInt64(i2)
	if err != nil {
		return 0, err
	}

	t := i1_64 + i2_64
	return t, nil
}

func subI(i1 any, i2 any) (int64, error) {
	i1_64, err := toInt64(i1)
	if err != nil {
		return 0, err
	}
	i2_64, err := toInt64(i2)
	if err != nil {
		return 0, err
	}

	t := i1_64 - i2_64
	return t, nil
}

func formatInt(i any) (string, error) {
	n, err := toInt64(i)
	if err != nil {
		return "", err
	}

	return printer.Sprintf("%d", n), nil
}

func formatFloat(f float64, dp int) string {
	format := "%." + strconv.Itoa(dp) + "f"
	return printer.Sprintf(format, f)
}

func yesno(b bool) string {
	if b {
		return "Yes"
	}

	return "No"
}

func urlSetParam(u *url.URL, key string, value any) *url.URL {
	nu := *u
	values := nu.Query()

	values.Set(key, fmt.Sprintf("%v", value))

	nu.RawQuery = values.Encode()
	return &nu
}

func urlDelParam(u *url.URL, key string) *url.URL {
	nu := *u
	values := nu.Query()

	values.Del(key)

	nu.RawQuery = values.Encode()
	return &nu
}

func toInt64(i any) (int64, error) {
	switch v := i.(type) {
	case int:
		return int64(v), nil
	case int8:
		return int64(v), nil
	case int16:
		return int64(v), nil
	case int32:
		return int64(v), nil
	case int64:
		return v, nil
	case uint:
		return int64(v), nil
	case uint8:
		return int64(v), nil
	case uint16:
		return int64(v), nil
	case uint32:
		return int64(v), nil
	// Note: uint64 not supported due to risk of truncation.
	case string:
		return strconv.ParseInt(v, 10, 64)
	}

	return 0, fmt.Errorf("unable to convert type %T to int", i)
}

func markdownfy(s string) string {
	markdown := goldmark.New(
		goldmark.WithExtensions(
			highlighting.Highlighting,
		),
	)

	var buf bytes.Buffer
	markdown.Convert([]byte(s), &buf)
	return buf.String()
}

func stringToArray(s string, delim string) []string {
	return strings.Split(strings.Trim(s, delim), delim)
}

func renderFieldValueAsString(value string, widget string) string {
	o := ""
	widget = strings.ToLower(widget)
	switch widget {
	case "url":
		if len(value) > 0 {
			label, _ := strings.CutPrefix(value, "https://")
			label, _ = strings.CutPrefix(label, "www.")
			if len(label) > 20 {
				label = label[:20] + " ..."
			}
			o = fmt.Sprintf("<a href=\"%v\" title=\"%v\" target=\"_new\">%v</a>", value, value, label)
		}
	default:
		o = strings.Trim(fmt.Sprint(value), "[]")
	}
	return o
}

func renderFieldValue(value string, widget string) template.HTML {
	return template.HTML(renderFieldValueAsString(value, widget))
}

func renderFieldValues(values map[int]string, widget string) template.HTML {
	if len(values) > 0 {
		var values_a []string
		for _, value := range values {
			values_a = append(values_a, renderFieldValueAsString(value, widget))
		}

		return template.HTML(strings.Join(values_a, ", "))
	}

	return template.HTML("")
}

func linksList(urls map[string]string, delim string) template.HTML {
	o := ""
	return template.HTML(o)
}

func widget_relation_type(name string, value string, attributes string) template.HTML {
	var options []WidgetOption

	options = append(options, WidgetOption{Key: "Parent", Value: "Parent"})
	options = append(options, WidgetOption{Key: "Child", Value: "Child"})
	options = append(options, WidgetOption{Key: "Link", Value: "Link"})

	return widget_select(name, "", value, options, attributes)
}

func widget_select(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
	return render_select(name, label, value, options, attributes, "")
}

func widget_slim_select(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
	return render_select(name, label, value, options, attributes, "slim-select")
}

func render_select(name string, label string, value any, options []WidgetOption, attributes string, classes string) template.HTML {
	var values []string

	switch v := value.(type) {
	case int64:
		values = append(values, strconv.FormatInt(v, 10))
	case []int64:
		for _, i := range v {
			values = append(values, strconv.FormatInt(i, 10))
		}
	case string:
		if strings.HasPrefix(v, "|") && strings.HasSuffix(v, "|") {
			values = strings.Split(strings.Trim(v, "|"), "|")
		} else {
			values = append(values, v)
		}
	case []string:
		values = v
	}

	o := ""
	if len(label) > 0 {
		o = fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
	}
	o = o + fmt.Sprintf(`<select name="%v" id="%v" class="%v" %v>`, name, name, classes, attributes)
	selected := ""
	for _, option := range options {
		selected = ""
		if slices.Contains(values, option.Key) {
			selected = "selected"
		}
		o = o + fmt.Sprintf(`<option value="%v" %v>%v</option>`, option.Key, selected, option.Value)
	}
	o = o + `</select>`
	return template.HTML(o)
}

func widget_checkbox(name string, label string, value string, value_assigned any) template.HTML {
	var checked string = ""
	var value_assigned_str string = ""

	switch v := value_assigned.(type) {
	case int:
		value_assigned_str = strconv.Itoa(v)
	case int64:

		value_assigned_str = strconv.FormatInt(v, 10)
	case string:
		value_assigned_str = value_assigned.(string)
	}

	if value == value_assigned_str {
		checked = `checked="checked"`
	}

	var o string = `<label class="switch">`
	o = o + fmt.Sprintf(`<input id="%v" name="%v" type="checkbox" value="%v" %v />`, name, name, value, checked)
	o = o + fmt.Sprintf(`<span class="slider round"></span></label><label for="%v" class="label-checkbox">%v</label>`, name, label)
	return template.HTML(o)
}

func widget_checkboxes(name string, label string, value any, options []WidgetOption, attributes string) template.HTML {
	var values []string

	switch v := value.(type) {
	case int64:
		values = append(values, strconv.FormatInt(v, 10))
	case []int64:
		for _, i := range v {
			values = append(values, strconv.FormatInt(i, 10))
		}
	case string:
		if strings.HasPrefix(v, "|") && strings.HasSuffix(v, "|") {
			values = strings.Split(strings.Trim(v, "|"), "|")
		} else {
			values = append(values, v)
		}
	case []string:
		values = v
	}

	o := ""
	o = o + "<fieldset>"
	if len(label) > 0 {
		o = o + fmt.Sprintf(`<label>%v</label>`, label)
	}
	checked := ""
	for _, option := range options {
		checked = ""
		if slices.Contains(values, option.Key) {
			checked = `checked="checked"`
		}
		id_str := strings.ReplaceAll(name+"-"+option.Key, " ", "-")
		o = o + "<p>"
		o = o + fmt.Sprintf(`<label class="switch"><input id="%v" type="checkbox" name="%v" %v value="%v" %v /><span class="slider round"></span></label>`, id_str, name, checked, option.Key, attributes)
		o = o + fmt.Sprintf(`<label class="label-checkbox" for="%v">%v</label>`, id_str, option.Value)
		o = o + "</p>"
	}
	o = o + "</fiedlset>"
	return template.HTML(o)
}

func widget_text(name string, label string, value string, attributes string) template.HTML {
	o := ""
	if len(label) > 0 {
		o = o + fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
	}
	o = o + fmt.Sprintf(`<input id="%v" type="text" name="%v" value="%v" %v />`, name, name, value, attributes)
	return template.HTML(o)
}

func widget_url(name string, label string, value string, attributes string) template.HTML {
	o := ""
	if len(label) > 0 {
		o = o + fmt.Sprintf(`<label for="%v">%v</label>`, name, label)
	}
	o = o + fmt.Sprintf(`<input id="%v" type="url" name="%v" value="%v" %v />`, name, name, value, attributes)
	return template.HTML(o)
}

func field_widget(widget string, bm_type_field_id int64, counter int, label string, value string, valid_values string, attributes string) template.HTML {
	widget_name := fmt.Sprintf("FieldsValues-%v-%v", bm_type_field_id, counter)
	var options []WidgetOption

	switch widget {
	case "select":
		entries := strings.Split(valid_values, "|")
		for _, entry := range entries {
			parts := strings.Split(entry, ",")
			if len(parts) == 2 {
				options = append(options, WidgetOption{Key: parts[0], Value: parts[1]})
			} else if len(parts) == 1 {
				options = append(options, WidgetOption{Key: parts[0], Value: parts[0]})
			}
		}
		return widget_select(widget_name, label, value, options, attributes)
	case "text":
		return widget_text(widget_name, label, value, attributes)
	case "url":
		return widget_url(widget_name, label, value, attributes)
	}

	return template.HTML("")
}

func tmap(pairs ...any) (map[string]any, error) {
	if len(pairs)%2 != 0 {
		return nil, errors.New("misaligned map")
	}

	m := make(map[string]any, len(pairs)/2)

	for i := 0; i < len(pairs); i += 2 {
		key, ok := pairs[i].(string)
		if !ok {
			return nil, fmt.Errorf("cannot use type %T as map key", pairs[i])
		}
		m[key] = pairs[i+1]
	}
	return m, nil
}
