package templates

import (
	"encoding/json"
	"fmt"
	"html/template"
	"mal/internal/domain"
	"net/url"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"
)

type genreOption struct {
	Genre          domain.Genre
	Selected       bool
	StartsInactive bool
}

func dict(values ...any) (map[string]any, error) {
	if len(values)%2 != 0 {
		return nil, fmt.Errorf("dict expects even number of values, got %d", len(values))
	}
	out := make(map[string]any, len(values)/2)
	for i := 0; i < len(values); i += 2 {
		key, ok := values[i].(string)
		if !ok {
			return nil, fmt.Errorf("dict key at index %d is not a string", i)
		}
		out[key] = values[i+1]
	}
	return out, nil
}

func jsonAttr(v any) (template.HTMLAttr, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return "", fmt.Errorf("json marshal: %w", err)
	}
	return template.HTMLAttr(b), nil
}

func genresParams(genres []int) string {
	if len(genres) == 0 {
		return ""
	}
	var b strings.Builder
	for i, g := range genres {
		if i > 0 {
			b.WriteByte('&')
		}
		b.WriteString("genres=")
		b.WriteString(strconv.Itoa(g))
	}
	return b.String()
}

func browseURL(v map[string]any, overrides map[string]any) (string, error) {
	values := url.Values{}
	setQueryValue(values, "q", stringValue(v["Query"]))
	setQueryValue(values, "type", stringValue(v["Type"]))
	setQueryValue(values, "status", stringValue(v["Status"]))
	setQueryValue(values, "order_by", stringValue(v["OrderBy"]))
	setQueryValue(values, "sort", stringValue(v["Sort"]))
	setQueryValue(values, "studio", stringValue(v["Studio"]))
	if sfw, ok := v["SFW"]; ok {
		values.Set("sfw", strconv.FormatBool(boolValue(sfw)))
	}
	for _, genre := range intSliceValue(v["Genres"]) {
		values.Add("genres", strconv.Itoa(genre))
	}
	setQueryValue(values, "page", stringValue(v["Page"]))

	for key, raw := range overrides {
		switch key {
		case "genres":
			values.Del("genres")
			for _, genre := range intSliceValue(raw) {
				values.Add("genres", strconv.Itoa(genre))
			}
		case "sfw":
			values.Set("sfw", strconv.FormatBool(boolValue(raw)))
		default:
			setQueryValue(values, key, stringValue(raw))
		}
	}

	encoded := values.Encode()
	if encoded == "" {
		return "/browse", nil
	}
	return "/browse?" + encoded, nil
}

func setQueryValue(values url.Values, key string, value string) {
	if value == "" {
		values.Del(key)
		return
	}
	values.Set(key, value)
}

func stringValue(v any) string {
	switch value := v.(type) {
	case nil:
		return ""
	case string:
		return value
	case int:
		if value == 0 {
			return ""
		}
		return strconv.Itoa(value)
	case int64:
		if value == 0 {
			return ""
		}
		return strconv.FormatInt(value, 10)
	case float64:
		if value == 0 {
			return ""
		}
		return strconv.Itoa(int(value))
	default:
		return fmt.Sprint(v)
	}
}

func boolValue(v any) bool {
	switch value := v.(type) {
	case bool:
		return value
	case string:
		return value == "true"
	default:
		return false
	}
}

func intSliceValue(v any) []int {
	switch value := v.(type) {
	case []int:
		return value
	case []int64:
		out := make([]int, 0, len(value))
		for _, item := range value {
			out = append(out, int(item))
		}
		return out
	case []any:
		out := make([]int, 0, len(value))
		for _, item := range value {
			n := toInt(item)
			if n > 0 {
				out = append(out, n)
			}
		}
		return out
	default:
		return nil
	}
}

func hasGenre(id int, genres []int) bool {
	return slices.Contains(genres, id)
}

func orderedGenreOptions(genres []domain.Genre, selectedGenres []int) []genreOption {
	selected := make(map[int]struct{}, len(selectedGenres))
	for _, genreID := range selectedGenres {
		selected[genreID] = struct{}{}
	}

	out := make([]genreOption, 0, len(genres))
	inactive := make([]genreOption, 0, len(genres))
	for _, genre := range genres {
		_, isSelected := selected[genre.MalID]
		option := genreOption{Genre: genre, Selected: isSelected}
		if isSelected {
			out = append(out, option)
			continue
		}
		inactive = append(inactive, option)
	}

	if len(out) > 0 && len(inactive) > 0 {
		inactive[0].StartsInactive = true
	}

	return append(out, inactive...)
}

func div(a, b float64) float64 {
	if b == 0 {
		return 0
	}
	return a / b
}

func ceilDiv(a, b int) int {
	if b == 0 {
		return 0
	}
	return (a + b - 1) / b
}

func idiv(a, b int) int {
	if b == 0 {
		return 0
	}
	return a / b
}

func atoi(v any) int {
	s, ok := v.(string)
	if !ok {
		return 0
	}
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0
	}
	return n
}

func seq(v any) []int {
	count := 0
	switch n := v.(type) {
	case int:
		count = n
	case int64:
		if n > int64(^uint(0)>>1) {
			count = 0
		} else {
			count = int(n)
		}
	}
	if count <= 0 {
		return []int{}
	}
	res := make([]int, count)
	for i := 0; i < count; i++ {
		res[i] = i
	}
	return res
}

func toInt(v any) int {
	switch n := v.(type) {
	case int:
		return n
	case int64:
		if n > int64(^uint(0)>>1) {
			return 0
		}
		return int(n)
	case float64:
		if n > float64(int(^uint(0)>>1)) {
			return 0
		}
		return int(n)
	case string:
		i, err := strconv.Atoi(n)
		if err != nil {
			return 0
		}
		return i
	default:
		return 0
	}
}

func percent(current, total float64) float64 {
	if total == 0 {
		return 0
	}
	return (current / total) * 100
}

func formatDate(dateStr string) string {
	t, err := time.Parse(time.RFC3339, dateStr)
	if err != nil {
		t, err = time.Parse("2006-01-02T15:04:05+00:00", dateStr)
		if err != nil {
			return dateStr
		}
	}
	return t.Format("Jan 2, 2006")
}

func nextSort(sort string) string {
	if sort == "asc" {
		return "desc"
	}
	return "asc"
}

func posterURL(webp, jpg any, width, height int) string {
	if webp != nil {
		if s, ok := webp.(string); ok && s != "" {
			return s
		}
	}
	if jpg != nil {
		if s, ok := jpg.(string); ok && s != "" {
			return s
		}
	}
	return fmt.Sprintf("https://placehold.co/%dx%d?text=No+Image", width, height)
}

func episodeRangeStart(epNum, step int) int {
	if epNum < 1 || step < 1 {
		return 1
	}
	return ((epNum-1)/step)*step + 1
}

func assetURL(assetPath string) string {
	info, err := os.Stat(strings.TrimPrefix(assetPath, "/"))
	if err != nil {
		return assetPath
	}

	values := url.Values{}
	values.Set("v", fmt.Sprintf("%d-%d", info.ModTime().Unix(), info.Size()))
	return assetPath + "?" + values.Encode()
}
