package web

import (
	"encoding/json"
	"fmt"
	"html/template"
	"strings"
	"sync"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)

var (
	webTimezone     *time.Location
	webTimezoneOnce sync.Once
)

// getTimezone returns the Europe/Budapest timezone, cached after first load.
// Falls back to UTC if tzdata is unavailable.
func getTimezone() *time.Location {
	webTimezoneOnce.Do(func() {
		loc, err := time.LoadLocation("Europe/Budapest")
		if err != nil {
			loc = time.UTC
		}
		webTimezone = loc
	})
	return webTimezone
}

// routeUnpublished reports whether the reverse proxy (Traefik) is withholding a deployed stack's public
// route because the container is not healthy. Traefik's docker provider only publishes a route to a
// container that is healthy (or has no healthcheck); an unhealthy or restarting container yields a 404
// at its URL even though the card "looks deployed". Templates use this to surface that distinctly (F5),
// so an unhealthy app with a dead URL isn't mistaken for a merely-degraded-but-reachable one.
func routeUnpublished(state stacks.ContainerState) bool {
	switch state {
	case stacks.StateUnhealthy, stacks.StateRestarting:
		return true
	default:
		return false
	}
}

// templateFuncMap returns the FuncMap used by all HTML templates.
func (s *Server) templateFuncMap() template.FuncMap {
	loc := getTimezone()

	return template.FuncMap{
		// stateColor maps container states to design-system-v2 semantic tokens,
		// consumed as class suffixes (tag-run, stack-state-run, state-text-run...).
		// Exception-color principle: a customer-stopped app is neutral, NOT red —
		// genuine failures surface via unhealthy state + the alert system.
		"stateColor": func(state stacks.ContainerState) string {
			switch state {
			case stacks.StateRunning:
				return "run"
			case stacks.StateStarting, stacks.StateDeploying:
				return "progress"
			case stacks.StateUnhealthy:
				return "warn"
			case stacks.StateRestarting:
				// a restart loop is a problem, not progress
				return "warn"
			case stacks.StateStopped, stacks.StateExited:
				return "neutral"
			case stacks.StatePaused:
				return "neutral"
			case stacks.StateNotDeployed:
				return "off"
			default:
				return "off"
			}
		},
		"stateLabel": func(state stacks.ContainerState) string {
			switch state {
			case stacks.StateRunning:
				return "Fut"
			case stacks.StateStarting:
				return "Indulás..."
			case stacks.StateDeploying:
				return "Telepítés..."
			case stacks.StateUnhealthy:
				return "Nem egészséges"
			case stacks.StateStopped, stacks.StateExited:
				return "Leállítva"
			case stacks.StateRestarting:
				return "Újraindítás..."
			case stacks.StateNotDeployed:
				return "Nincs telepítve"
			case stacks.StatePaused:
				return "Szüneteltetve"
			default:
				return "Ismeretlen"
			}
		},
		"stateIcon": func(state stacks.ContainerState) string {
			switch state {
			case stacks.StateRunning:
				return "●"
			case stacks.StateStarting, stacks.StateDeploying:
				return "◐"
			case stacks.StateUnhealthy:
				return "◑"
			case stacks.StateStopped, stacks.StateExited:
				return "○"
			case stacks.StateRestarting:
				return "◐"
			default:
				return "◌"
			}
		},
		"stateStr": func(state stacks.ContainerState) string {
			return string(state)
		},
		// isOperational returns true for any state where the stack has containers
		// and is not stopped/exited — used by templates for showing action buttons
		"isOperational": func(state stacks.ContainerState) bool {
			switch state {
			case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
				return true
			default:
				return false
			}
		},
		"routeUnpublished": routeUnpublished,
		"logoURL": func(slug string) string {
			return s.cfg.AppLogoURL(slug)
		},
		"logoPNGURL": func(slug string) string {
			return s.cfg.AppLogoPNGURL(slug)
		},
		"appPageURL": func(slug string) string {
			return s.cfg.AppPageURL(slug)
		},
		// usageColor: capacity meters are blue below 70%, amber 70–85, red ≥85 (v2).
		"usageColor": func(percent float64) string {
			if percent >= 85 {
				return "crit"
			}
			if percent >= 70 {
				return "warn"
			}
			return "nominal"
		},
		"fmtMB": func(mb uint64) string {
			if mb >= 1024 {
				gb := float64(mb) / 1024.0
				if gb >= 10 {
					return fmt.Sprintf("%.0f GB", gb)
				}
				return fmt.Sprintf("%.1f GB", gb)
			}
			return fmt.Sprintf("%d MB", mb)
		},
		"fmtGB": func(gb float64) string {
			if gb >= 100 {
				return fmt.Sprintf("%.0f GB", gb)
			}
			if gb >= 10 {
				return fmt.Sprintf("%.1f GB", gb)
			}
			return fmt.Sprintf("%.2f GB", gb)
		},
		"subtract": func(a, b int) int {
			r := a - b
			if r < 0 {
				return 0
			}
			return r
		},
		"screenshotURL": func(slug string, index int) string {
			return s.cfg.AppScreenshotURL(slug, index)
		},
		"seq": func(n int) []int {
			result := make([]int, n)
			for i := range result {
				result[i] = i + 1
			}
			return result
		},
		// tempColor: thresholds unchanged; outputs remapped to v2 tokens.
		"tempColor": func(celsius float64) string {
			if celsius > 75 {
				return "crit"
			}
			if celsius >= 60 {
				return "warn"
			}
			return "nominal"
		},
		"fmtTemp": func(celsius float64) string {
			return fmt.Sprintf("%.0f°C", celsius)
		},
		"fmtLoad": func(load float64) string {
			return fmt.Sprintf("%.2f", load)
		},
		"filterCategory": func(state stacks.ContainerState, deployed bool) string {
			switch state {
			case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting:
				return "running"
			case stacks.StateStopped, stacks.StateExited, stacks.StatePaused:
				return "stopped"
			default:
				if deployed {
					return "stopped"
				}
				return "available"
			}
		},
		"timeAgo": func(t time.Time) string {
			if t.IsZero() {
				return "–"
			}
			now := time.Now().In(loc)
			d := now.Sub(t.In(loc))
			switch {
			case d < time.Minute:
				return "most"
			case d < time.Hour:
				return fmt.Sprintf("%d perce", int(d.Minutes()))
			case d < 24*time.Hour:
				return fmt.Sprintf("%d órája", int(d.Hours()))
			case d < 48*time.Hour:
				return "tegnap"
			default:
				return fmt.Sprintf("%d napja", int(d.Hours()/24))
			}
		},
		// timeAgoStr is timeAgo for RFC3339 string timestamps (e.g. OffboxTarget.LastRun,
		// which persists as a string in settings.json). Found during D0: the backups page
		// passed the raw string to timeAgo and 500'd once an off-box backup had ever run.
		"timeAgoStr": func(s string) string {
			t, err := time.Parse(time.RFC3339, s)
			if err != nil {
				return s
			}
			now := time.Now().In(loc)
			d := now.Sub(t.In(loc))
			switch {
			case d < time.Minute:
				return "most"
			case d < time.Hour:
				return fmt.Sprintf("%d perce", int(d.Minutes()))
			case d < 24*time.Hour:
				return fmt.Sprintf("%d órája", int(d.Hours()))
			case d < 48*time.Hour:
				return "tegnap"
			default:
				return fmt.Sprintf("%d napja", int(d.Hours()/24))
			}
		},
		"fmtTime": func(t time.Time) string {
			if t.IsZero() {
				return "–"
			}
			return t.In(loc).Format("2006-01-02 15:04")
		},
		"fmtTimeShort": func(t time.Time) string {
			if t.IsZero() {
				return "–"
			}
			lt := t.In(loc)
			now := time.Now().In(loc)
			if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
				return lt.Format("15:04")
			}
			return lt.Format("01-02 15:04")
		},
		"dbTypeLabel": func(t backup.DBType) string {
			switch t {
			case backup.DBTypePostgres:
				return "PostgreSQL"
			case backup.DBTypeMariaDB:
				return "MariaDB"
			default:
				return string(t)
			}
		},
		"nextRunLabel": func(t time.Time) string {
			if t.IsZero() {
				return "–"
			}
			lt := t.In(loc)
			now := time.Now().In(loc)
			timeStr := lt.Format("15:04")
			if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() {
				return "ma " + timeStr
			}
			if lt.Year() == now.Year() && lt.YearDay() == now.YearDay()+1 {
				return "holnap " + timeStr
			}
			return lt.Format("2006-01-02") + " " + timeStr
		},
		"pruneLabel": func(s string) string {
			switch strings.ToLower(s) {
			case "weekly":
				return "vasárnap"
			case "daily":
				return "naponta"
			case "sunday":
				return "vasárnap"
			default:
				return s
			}
		},
		"nextPruneLabel": func(schedule string) string {
			now := time.Now().In(loc)
			var next time.Time
			switch strings.ToLower(schedule) {
			case "daily":
				next = now.Add(24 * time.Hour)
			default: // weekly/sunday
				daysUntilSunday := (7 - int(now.Weekday())) % 7
				if daysUntilSunday == 0 {
					if now.Hour() >= 4 {
						daysUntilSunday = 7 // Already ran today, next week
					} else {
						return "ma" // Today (Sunday), hasn't run yet
					}
				}
				next = now.AddDate(0, 0, daysUntilSunday)
			}
			return next.Format("2006-01-02")
		},
		"fmtDuration": func(d time.Duration) string {
			if d < time.Second {
				return "< 1s"
			}
			if d < time.Minute {
				return fmt.Sprintf("%ds", int(d.Seconds()))
			}
			return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
		},
		"fmtBytes": func(b int64) string {
			const (
				kb = 1024
				mb = 1024 * kb
				gb = 1024 * mb
			)
			switch {
			case b >= int64(gb):
				return fmt.Sprintf("%.1f GB", float64(b)/float64(gb))
			case b >= int64(mb):
				return fmt.Sprintf("%.1f MB", float64(b)/float64(mb))
			case b >= int64(kb):
				return fmt.Sprintf("%.1f KB", float64(b)/float64(kb))
			default:
				return fmt.Sprintf("%d B", b)
			}
		},
		"shortID": func(id string) string {
			if len(id) > 8 {
				return id[:8]
			}
			return id
		},
		// statusText maps DR restore status codes to Hungarian labels.
		"statusText": func(status string) string {
			switch status {
			case "pending":
				return "Várakozik"
			case "restoring":
				return "Visszaállítás..."
			case "done":
				return "Kész"
			case "failed":
				return "Sikertelen"
			case "skipped":
				return "Kihagyva"
			default:
				return status
			}
		},
		// json marshals a value to JSON for embedding in <script> blocks.
		"json": func(v interface{}) template.JS {
			b, _ := json.Marshal(v)
			return template.JS(b)
		},
		// pageMatch returns true if currentPage is in the pages slice.
		// Used to filter page-specific alerts in layout.html.
		"pageMatch": func(pages []string, currentPage string) bool {
			for _, p := range pages {
				if p == currentPage {
					return true
				}
			}
			return false
		},
	}
}
