package webserver

import (
	"fmt"
	"html/template"
	"io/fs"
	"net/http"
	"strings"
	"time"

	"git.wntrmute.dev/kyle/mcr/web"
)

// templateSet wraps parsed templates and provides a render method.
type templateSet struct {
	templates map[string]*template.Template
}

// templateFuncs returns the function map used across all templates.
func templateFuncs() template.FuncMap {
	return template.FuncMap{
		"formatSize":  formatSize,
		"formatTime":  formatTime,
		"truncate":    truncate,
		"joinStrings": joinStrings,
	}
}

// loadTemplates parses all page templates with the layout template.
func loadTemplates() (*templateSet, error) {
	// Read layout template.
	layoutBytes, err := fs.ReadFile(web.Content, "templates/layout.html")
	if err != nil {
		return nil, fmt.Errorf("webserver: read layout template: %w", err)
	}
	layoutStr := string(layoutBytes)

	pages := []string{
		"login",
		"dashboard",
		"repositories",
		"repository_detail",
		"manifest_detail",
		"policies",
		"audit",
	}

	ts := &templateSet{
		templates: make(map[string]*template.Template, len(pages)),
	}

	for _, page := range pages {
		pageBytes, readErr := fs.ReadFile(web.Content, "templates/"+page+".html")
		if readErr != nil {
			return nil, fmt.Errorf("webserver: read template %s: %w", page, readErr)
		}

		t, parseErr := template.New("layout").Funcs(templateFuncs()).Parse(layoutStr)
		if parseErr != nil {
			return nil, fmt.Errorf("webserver: parse layout for %s: %w", page, parseErr)
		}

		_, parseErr = t.Parse(string(pageBytes))
		if parseErr != nil {
			return nil, fmt.Errorf("webserver: parse template %s: %w", page, parseErr)
		}

		ts.templates[page] = t
	}

	return ts, nil
}

// render executes a named template and writes the result to w.
func (ts *templateSet) render(w http.ResponseWriter, name string, data any) {
	t, ok := ts.templates[name]
	if !ok {
		http.Error(w, "template not found", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := t.Execute(w, data); err != nil {
		// Template already started writing; log but don't send another error.
		_ = err // best-effort; headers may already be sent
	}
}

// formatSize converts bytes to a human-readable string.
func formatSize(b int64) string {
	const (
		kib = 1024
		mib = 1024 * kib
		gib = 1024 * mib
		tib = 1024 * gib
	)

	switch {
	case b >= tib:
		return fmt.Sprintf("%.1f TiB", float64(b)/float64(tib))
	case b >= gib:
		return fmt.Sprintf("%.1f GiB", float64(b)/float64(gib))
	case b >= mib:
		return fmt.Sprintf("%.1f MiB", float64(b)/float64(mib))
	case b >= kib:
		return fmt.Sprintf("%.1f KiB", float64(b)/float64(kib))
	default:
		return fmt.Sprintf("%d B", b)
	}
}

// formatTime converts an RFC3339 string to a more readable format.
func formatTime(s string) string {
	t, err := time.Parse(time.RFC3339, s)
	if err != nil {
		// Try RFC3339Nano.
		t, err = time.Parse(time.RFC3339Nano, s)
		if err != nil {
			return s
		}
	}
	return t.Format("2006-01-02 15:04:05")
}

// truncate returns the first n characters of s, appending "..." if truncated.
func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}

// joinStrings joins string slices for template rendering.
func joinStrings(ss []string, sep string) string {
	return strings.Join(ss, sep)
}
