package templates

import (
	"bookhoard/internal/database"
	"bookhoard/internal/handlers"
	"encoding/json"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
)

func activeClass(current, target string) string {
	base := "flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors "
	if strings.TrimRight(current, "/") == strings.TrimRight(target, "/") {
		return base + "bg-brand/15 text-brand"
	}
	return base + "text-content-muted hover:bg-surface-hover hover:text-content"
}

// navItemClass returns classes for top-nav links, marking the active route.
func navItemClass(current, target string) string {
	base := "text-sm font-medium px-3 py-2 rounded-lg transition-colors "
	if current == target {
		return base + "text-content bg-surface-hover"
	}
	return base + "text-content-muted hover:text-content hover:bg-surface-hover"
}

// themeCheckClass returns the class suffix for a theme option's checkmark
// wrapper: empty for the active theme (visible), " hidden" otherwise. The
// check element is always rendered so updateThemeIndicators() can move it
// client-side when the user picks a different theme.
func themeCheckClass(name, current string) string {
	if name == current {
		return ""
	}
	return " hidden"
}

func ContainsString(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}

func isUserVisible(userID string, visibility []UserVisibilityData) bool {
	for _, v := range visibility {
		if v.UserID == userID {
			return v.IsVisible
		}
	}
	return false
}

// TotalMediaCount sums the MediaCount across the given libraries,
// used to display the total next to the "All Libraries" option.
func TotalMediaCount(libs []LibraryData) int64 {
	var total int64
	for _, l := range libs {
		total += l.MediaCount
	}
	return total
}

func uuidToString(id pgtype.UUID) string {
	if !id.Valid {
		return ""
	}
	u, err := uuid.FromBytes(id.Bytes[0:16])
	if err != nil {
		return ""
	}
	return u.String()
}

// renderStars converts rating (1-10 scale) to star display
// Always displays 5 stars total with filled stars in yellow
// and empty stars in grey (using theme variable)
// Rating scale: 1-10 where each 2 points = 1 full star, odd numbers = half stars
func renderStars(rating int32) string {
	const totalStars = 5
	fullStars := rating / 2
	hasHalf := rating%2 != 0

	var stars strings.Builder

	for i := int32(0); i < totalStars; i++ {
		if i < fullStars {
			// Filled star - accent color (theme-aware highlight)
			stars.WriteString(`<span class="text-brand">★</span>`)
		} else if i == fullStars && hasHalf {
			// Half star - gradient from accent (left) to grey (right)
			stars.WriteString(`<span style="background: linear-gradient(90deg, var(--accent) 50%, var(--text-secondary) 50%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">★</span>`)
		} else {
			// Empty star - grey using theme variable
			stars.WriteString(`<span class="text-content-muted/40">★</span>`)
		}
	}

	return stars.String()
}

// formatFileSize converts bytes to human-readable format
func formatFileSize(bytes int64) string {
	const (
		KB = 1024
		MB = KB * 1024
		GB = MB * 1024
	)

	switch {
	case bytes >= GB:
		return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
	case bytes >= MB:
		return fmt.Sprintf("%.2f MB", float64(bytes)/float64(MB))
	case bytes >= KB:
		return fmt.Sprintf("%.2f KB", float64(bytes)/float64(KB))
	default:
		return fmt.Sprintf("%d B", bytes)
	}
}

// getExternalURL generates URL for external book services
// Priority: ID > ISBN > Title+Author search
func getExternalURL(service string, id string, isbn pgtype.Text, title string, author pgtype.Text) string {
	baseURL := ""
	searchTerm := ""

	// Determine search term: ID > ISBN > Title+Author
	if id != "" {
		searchTerm = id
	} else if isbn.Valid && isbn.String != "" {
		searchTerm = isbn.String
	} else {
		// Build title+author search query
		if author.Valid && author.String != "" {
			searchTerm = fmt.Sprintf("%s %s", title, author.String)
		} else {
			searchTerm = title
		}
	}

	// Build URL based on service
	switch service {
	case "goodreads":
		if id != "" {
			baseURL = "https://www.goodreads.com/book/show/"
		} else {
			baseURL = "https://www.goodreads.com/search?q="
		}
	case "openlibrary":
		if id != "" {
			baseURL = "https://openlibrary.org/books/"
		} else {
			baseURL = "https://openlibrary.org/search?q="
		}
	case "googlebooks":
		if id != "" {
			baseURL = "https://books.google.com/books?id="
		} else {
			baseURL = "https://www.google.com/search?tbm=bks&q="
		}
	case "amazon":
		// Amazon doesn't have direct book IDs, always search
		baseURL = "https://www.amazon.com/s?k="
		if isbn.Valid && isbn.String != "" {
			searchTerm = isbn.String
		}
	}

	return baseURL + searchTerm
}

// getBookRating returns the rating value or 0 if not rated
func getBookRating(rating *database.MediaRatings) int32 {
	if rating != nil {
		return rating.Rating
	}
	return 0
}

// conflictWinnerSource returns a valid source key from a conflict's
// conflict_data map, to pass as the "winner" when resolving it. It prefers
// "web" (since the user is acting via the web UI) and otherwise falls back to
// the lexicographically smallest key. The chosen winner does not affect the
// final read/unread state, which is set by a subsequent progress write; it only
// needs to be a key present in the conflict data so the resolve endpoint
// accepts it and arms its 10-minute suppression window.
func conflictWinnerSource(c *handlers.ConflictDetailResponse) string {
	if c == nil || len(c.ConflictData) == 0 {
		return ""
	}
	if _, ok := c.ConflictData["web"]; ok {
		return "web"
	}
	keys := make([]string, 0, len(c.ConflictData))
	for k := range c.ConflictData {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys[0]
}

// conflictID returns the active conflict's ID, or "" when there is none. Used
// to render a data-conflict-id attribute the frontend can read.
func conflictID(c *handlers.ConflictDetailResponse) string {
	if c == nil {
		return ""
	}
	return c.ID
}

// getAlternateSeries extracts the alternate series name from JSONB data
func getAlternateSeries(data []byte) string {
	if len(data) == 0 {
		return ""
	}
	var result map[string]interface{}
	if err := json.Unmarshal(data, &result); err != nil {
		return ""
	}
	if series, ok := result["alternate_series"].(string); ok {
		return series
	}
	return ""
}

// getDomainName extracts a clean domain name from URL for display
func getDomainName(rawURL string) string {
	if rawURL == "" {
		return "Source"
	}
	u, err := url.Parse(rawURL)
	if err != nil {
		return "Source"
	}
	// Return hostname without www.
	hostname := u.Hostname()
	return strings.TrimPrefix(hostname, "www.")
}

// formatAlternateInfo formats alternate series info as readable string
func formatAlternateInfo(data []byte) string {
	if len(data) == 0 {
		return ""
	}
	var result map[string]interface{}
	if err := json.Unmarshal(data, &result); err != nil {
		return ""
	}

	var parts []string
	if series, ok := result["alternate_series"].(string); ok && series != "" {
		parts = append(parts, series)
	}
	if num, ok := result["alternate_number"].(float64); ok && num > 0 {
		parts = append(parts, fmt.Sprintf("#%d", int(num)))
	}
	if count, ok := result["alternate_count"].(float64); ok && count > 0 {
		parts = append(parts, fmt.Sprintf("(of %d)", int(count)))
	}

	return strings.Join(parts, " ")
}

// FormatInTimezone formats a time.Time in the specified timezone as MM-DD-YYYY HH:MM
func FormatInTimezone(t time.Time, timezone string) string {
	if t.IsZero() {
		return ""
	}

	loc, err := time.LoadLocation(timezone)
	if err != nil {
		loc = time.UTC
	}

	return t.In(loc).Format("01-02-2006 03:04 PM")
}

// FormatTimestamptzInTimezone formats a pgtype.Timestamptz in the specified timezone
func FormatTimestamptzInTimezone(t pgtype.Timestamptz, timezone string) string {
	if !t.Valid {
		return ""
	}
	return FormatInTimezone(t.Time, timezone)
}

func textToString(t pgtype.Text) string {
	if t.Valid {
		return t.String
	}
	return ""
}

func tagSliceToString(tags []string) string {
	return strings.Join(tags, ", ")
}

func stringSliceToString(s []string) string {
	return strings.Join(s, ", ")
}

func formatDateForInput(d pgtype.Date) string {
	if !d.Valid {
		return ""
	}
	return d.Time.Format("2006-01-02")
}

// GroupTunableSettings splits a flat, group-sorted entry list into labeled
// sub-section groups, separated into "live" (applies immediately) and
// "restart required" buckets. Entries keep their original order so groups stay
// coherent.
func GroupTunableSettings(entries []SettingEntry) (live, restart []SettingGroup) {
	var liveGroups, restartGroups []SettingGroup
	for _, e := range entries {
		target := &liveGroups
		if e.RequiresRestart {
			target = &restartGroups
		}
		if len(*target) == 0 || (*target)[len(*target)-1].Name != e.Group {
			*target = append(*target, SettingGroup{Name: e.Group})
		}
		(*target)[len(*target)-1].Entries = append((*target)[len(*target)-1].Entries, e)
	}
	return liveGroups, restartGroups
}
