package portfolio

import (
	"bytes"
	"context"
	"crussell/clock"
	"crussell/db"
	"crussell/internal/images"
	"crussell/internal/s3"
	"crussell/internal/validators"
	"crussell/mw"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net/http"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
)

const MaxInputLength = 256

func mimeTypeForField(fieldName string) string {
	switch fieldName {
	case "file_full_avif", "file_thumb_avif":
		return "image/avif"
	case "file_full_webp", "file_thumb_webp":
		return "image/webp"
	case "file_full_jpg", "file_thumb_jpg":
		return "image/jpeg"
	case "file_full_jxl":
		return "image/jxl"
	default:
		return "application/octet-stream"
	}
}

// validateInputLength returns an error if input exceeds max length

func validateInputLength(input string) error {
	if len(input) > MaxInputLength {
		return fmt.Errorf("input exceeds maximum length of %d characters", MaxInputLength)
	}
	return nil
}

// getAllowedCategories fetches all unique category prefixes from existing tags
func getAllowedCategories(ctx context.Context) (map[string]bool, error) {
	rows, err := db.Conn.Query(ctx, `
		SELECT DISTINCT SPLIT_PART(t, ':', 1) as category
		FROM images, unnest(tag_names) as t
		WHERE t LIKE '%:%'
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	categories := make(map[string]bool)
	for rows.Next() {
		var cat string
		if err := rows.Scan(&cat); err == nil && cat != "" {
			categories[cat] = true
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return categories, nil
}

type FullFormatURLs struct {
	Avif string `json:"avif"`
	Webp string `json:"webp"`
	Jpg  string `json:"jpg"`
	Jxl  string `json:"jxl,omitempty"`
}

type ThumbFormatURLs struct {
	Avif string `json:"avif"`
	Webp string `json:"webp"`
	Jpg  string `json:"jpg"`
}

type Image struct {
	ID           string          `json:"id"`
	URL          string          `json:"url"`
	ThumbnailURL string          `json:"thumbnail_url"`
	Full         FullFormatURLs  `json:"full"`
	Thumb        ThumbFormatURLs `json:"thumb"`
	TagNames     []string        `json:"tag_names"`
	CreatedAt    time.Time       `json:"created_at"`
}

type Tag struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// parseCursor splits a "createdAt|id" cursor string into its components.

type ImageListResponse struct {
	Images     []Image `json:"images"`
	NextCursor *string `json:"next_cursor,omitempty"`
}

func ListImages(w http.ResponseWriter, r *http.Request) {
	tagFilter := r.URL.Query().Get("tag")
	tagsFilter := r.URL.Query().Get("tags")

	// Validate input length
	if tagFilter != "" {
		if err := validateInputLength(tagFilter); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
	}
	if tagsFilter != "" {
		if err := validateInputLength(tagsFilter); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
	}

	limit := 20
	cursorStr := r.URL.Query().Get("cursor")

	if l := r.URL.Query().Get("limit"); l != "" {
		if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
			limit = parsed
		}
	}

	// Validate filter categories against allowed list from DB
	allowedCategories, err := getAllowedCategories(r.Context())
	if err != nil {
		log.Printf("Failed to get allowed categories: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	var filterClauses strings.Builder
	filterArgs := []any{}

	for key, values := range r.URL.Query() {
		if len(values) == 0 || values[0] == "" {
			continue
		}
		match, _ := regexp.Compile(`^filter\[(.+)\]$`)
		if match != nil {
			matches := match.FindStringSubmatch(key)
			if len(matches) == 2 {
				category := matches[1]
				value := values[0]

				// Validate category exists
				if !allowedCategories[category] {
					continue // Skip invalid categories silently for backward compatibility
				}

				// Validate input length
				if err := validateInputLength(category + ":" + value); err != nil {
					http.Error(w, err.Error(), http.StatusBadRequest)
					return
				}

				filterClauses.WriteString(fmt.Sprintf(" AND $%d::text = ANY(tag_names)", len(filterArgs)+1))
				filterArgs = append(filterArgs, category+":"+value)
			}
		}
	}

	var query string
	var args []any

	const formatCols = `, full_avif_url, full_webp_url, full_jpg_url, full_jxl_url, thumb_avif_url, thumb_webp_url, thumb_jpg_url`

	if tagsFilter != "" {
		tagList := strings.Split(tagsFilter, ",")
		cleanTags := make([]string, len(tagList))
		for i, t := range tagList {
			cleanTags[i] = "%" + strings.TrimSpace(t) + "%"
		}
		conditions := make([]string, len(cleanTags))
		for i := range cleanTags {
			conditions[i] = fmt.Sprintf("t ILIKE $%d", len(filterArgs)+i+1)
		}
		whereClause := strings.Join(conditions, " OR ")
		similarityCalls := make([]string, len(cleanTags))
		for i := range cleanTags {
			similarityCalls[i] = fmt.Sprintf("MAX(similarity(t, $%d))", len(filterArgs)+i+1)
		}
		similaritySum := strings.Join(similarityCalls, " + ")

		query = fmt.Sprintf(`
			SELECT id, url, thumbnail_url, tag_names, created_at%s,
			       COUNT(t) as match_count,
			       %s as relevance
			FROM images, unnest(tag_names) as t
			WHERE %s%s
			GROUP BY id
		`, formatCols, similaritySum, whereClause, filterClauses.String())

		var cursorArgs []any
		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			cursorArgs = []any{cursorCreatedAt, cursorID}
			havingIdx := len(cleanTags) + len(filterArgs) + 1
			query += fmt.Sprintf(" HAVING (created_at, id) < ($%d, $%d)", havingIdx, havingIdx+1)
		}

		query += " ORDER BY match_count DESC, relevance DESC, created_at DESC, id DESC"
		argOffset := len(filterArgs) + len(cleanTags) + len(cursorArgs)
		query += fmt.Sprintf(" LIMIT $%d", argOffset+1)

		queryArgs := make([]any, len(filterArgs)+len(cleanTags)+len(cursorArgs)+1)
		copy(queryArgs, filterArgs)
		for i, t := range cleanTags {
			queryArgs[len(filterArgs)+i] = t
		}
		for i, ca := range cursorArgs {
			queryArgs[len(filterArgs)+len(cleanTags)+i] = ca
		}
		queryArgs[len(filterArgs)+len(cleanTags)+len(cursorArgs)] = limit
		args = queryArgs
	} else if tagFilter != "" {
		argOffset := len(filterArgs)
		searchPattern := "%" + tagFilter + "%"
		searchIdx := argOffset + 1
		query = fmt.Sprintf(`
			SELECT id, url, thumbnail_url, tag_names, created_at%s,
			       CASE WHEN t = $%d THEN 2 ELSE 1 END as match_priority,
			       similarity(t, $%d) as relevance
			FROM images, unnest(tag_names) as t
			WHERE 1=1%s AND t ILIKE '%%' || $%d || '%%'
		`, formatCols, searchIdx, searchIdx, filterClauses.String(), searchIdx)

		cursorArgs := []any{}
		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			query += fmt.Sprintf(" AND (created_at, id) < ($%d, $%d)", searchIdx+1, searchIdx+2)
			cursorArgs = append(cursorArgs, cursorCreatedAt, cursorID)
		}

		query += " ORDER BY match_priority DESC, relevance DESC, created_at DESC, id DESC"
		query += fmt.Sprintf(" LIMIT $%d", searchIdx+1+len(cursorArgs))

		queryArgs := make([]any, searchIdx+1+len(cursorArgs))
		copy(queryArgs[:argOffset], filterArgs)
		queryArgs[argOffset] = searchPattern
		for i, ca := range cursorArgs {
			queryArgs[searchIdx+i] = ca
		}
		queryArgs[searchIdx+len(cursorArgs)] = limit
		args = queryArgs
	} else {
		argOffset := len(filterArgs)
		query = fmt.Sprintf(`
			SELECT id, url, thumbnail_url, tag_names, created_at%s, 0 as match_count, 0.0 as relevance
			FROM images
			WHERE 1=1%s
		`, formatCols, filterClauses.String())

		if cursorStr != "" {
			cursorCreatedAt, cursorID, err := validators.ParseCursor(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			query += fmt.Sprintf(" AND (created_at, id) < ($%d, $%d)", argOffset+1, argOffset+2)
			filterArgs = append(filterArgs, cursorCreatedAt, cursorID)
			argOffset += 2
		}

		query += " ORDER BY created_at DESC, id DESC"
		query += fmt.Sprintf(" LIMIT $%d", argOffset+1)

		queryArgs := make([]any, len(filterArgs)+1)
		copy(queryArgs, filterArgs)
		queryArgs[len(filterArgs)] = limit
		args = queryArgs
	}

	rows, err := db.Conn.Query(r.Context(), query, args...)
	if err != nil {
		log.Printf("Failed to list images: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var images []Image
	for rows.Next() {
		var img Image
		var matchCount int
		var relevance float64
		var fullAvif, fullWebp, fullJpg, fullJxl sql.NullString
		var thumbAvif, thumbWebp, thumbJpg sql.NullString
		if err := rows.Scan(
			&img.ID, &img.URL, &img.ThumbnailURL,
			(*[]string)(&img.TagNames), &img.CreatedAt,
			&fullAvif, &fullWebp, &fullJpg, &fullJxl,
			&thumbAvif, &thumbWebp, &thumbJpg,
			&matchCount, &relevance,
		); err != nil {
			log.Printf("Failed to scan image: %v", err)
			continue
		}
		if fullAvif.Valid {
			img.Full.Avif = fullAvif.String
		}
		if fullWebp.Valid {
			img.Full.Webp = fullWebp.String
		}
		if fullJpg.Valid {
			img.Full.Jpg = fullJpg.String
		}
		if fullJxl.Valid {
			img.Full.Jxl = fullJxl.String
		}
		if thumbAvif.Valid {
			img.Thumb.Avif = thumbAvif.String
		}
		if thumbWebp.Valid {
			img.Thumb.Webp = thumbWebp.String
		}
		if thumbJpg.Valid {
			img.Thumb.Jpg = thumbJpg.String
		}
		images = append(images, img)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if images == nil {
		images = []Image{}
	}

	var nextCursor *string
	if len(images) > 0 {
		last := images[len(images)-1]
		cursor := last.CreatedAt.UTC().Format(time.RFC3339) + "|" + last.ID
		nextCursor = &cursor
	}

	_ = json.NewEncoder(w).Encode(ImageListResponse{
		Images:      images,
		NextCursor:  nextCursor,
	})
}

func ListTags(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query().Get("q")
	if len(q) > 100 {
		q = q[:100]
	}

	var query string
	var args []any

	// Query tags from images.tag_names column (stored as array)
	if q != "" {
		query = `
			SELECT DISTINCT tag
			FROM (
				SELECT unnest(tag_names) as tag
				FROM images
				WHERE tag_names IS NOT NULL
			) t
			WHERE tag ILIKE '%' || $1 || '%'
			ORDER BY tag
			LIMIT 20
		`
		args = []any{q}
	} else {
		query = `
			SELECT DISTINCT tag
			FROM (
				SELECT unnest(tag_names) as tag
				FROM images
				WHERE tag_names IS NOT NULL
			) t
			ORDER BY tag
			LIMIT 20
		`
	}

	rows, err := db.Conn.Query(r.Context(), query, args...)
	if err != nil {
		log.Printf("Failed to list tags: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var tags []Tag
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			log.Printf("Failed to scan tag: %v", err)
			continue
		}
		tags = append(tags, Tag{ID: name, Name: name})
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if tags == nil {
		tags = []Tag{}
	}

	_ = json.NewEncoder(w).Encode(tags)
}

type FilterCategory struct {
	Category string        `json:"category"`
	Values   []FilterValue `json:"values"`
}

type FilterValue struct {
	Value string `json:"value"`
	Count int    `json:"count"`
}

func ListFilters(w http.ResponseWriter, r *http.Request) {
	tagFilter := r.URL.Query().Get("tag")
	tagsFilter := r.URL.Query().Get("tags")

	// Get all filter params
	selectedCategories := make(map[string]string)
	for key, values := range r.URL.Query() {
		if strings.HasPrefix(key, "filter[") && len(values) > 0 && values[0] != "" {
			category := strings.Trim(key, "[]")
			category = strings.TrimPrefix(category, "filter[")
			value := values[0]
			if len(category)+len(value)+1 > 256 {
				http.Error(w, "filter value too long", http.StatusBadRequest)
				return
			}
			selectedCategories[category] = value
		}
	}

	// Build base query
	baseQuery := "SELECT DISTINCT id FROM images WHERE 1=1"
	args := []any{}
	argNum := 1

	if tagFilter != "" {
		searchPattern := "%" + tagFilter + "%"
		baseQuery += fmt.Sprintf(" AND EXISTS (SELECT 1 FROM unnest(tag_names) AS t WHERE t ILIKE $%d)", argNum)
		args = append(args, searchPattern)
		argNum++
	}
	if tagsFilter != "" {
		tagList := strings.Split(tagsFilter, ",")
		conditions := make([]string, len(tagList))
		for i := range tagList {
			tagList[i] = "%" + strings.TrimSpace(tagList[i]) + "%"
			conditions[i] = fmt.Sprintf("t ILIKE $%d", argNum)
			args = append(args, tagList[i])
			argNum++
		}
		baseQuery += " AND EXISTS (SELECT 1 FROM unnest(tag_names) AS t WHERE " + strings.Join(conditions, " OR ") + ")"
	}

	// Build category filters for OTHER categories
	otherFilters := make([]string, 0)
	otherArgs := make([]any, len(args))
	copy(otherArgs, args)
	otherArgNum := argNum
	for cat, val := range selectedCategories {
		tagValue := cat + ":" + val
		otherFilters = append(otherFilters, fmt.Sprintf("AND $%d = ANY(tag_names)", otherArgNum))
		otherArgs = append(otherArgs, tagValue)
		otherArgNum++
	}

	// Build the filter count query
	filterQuery := `
		SELECT
			SPLIT_PART(t, ':', 1) as category,
			SPLIT_PART(t, ':', 2) as value,
			COUNT(*) as count
		FROM (` + baseQuery + `) as img_ids
		JOIN images ON images.id = img_ids.id
		JOIN unnest(images.tag_names) as t ON true
		WHERE t LIKE '%:%'
	`

	// If there are selected categories, we need two queries
	if len(selectedCategories) > 0 {
		// Query 1: Get all categories with filters applied (for unselected categories)
		filterClause := ""
		if len(otherFilters) > 0 {
			filterClause = " " + strings.Join(otherFilters, " ")
		}

		filteredQuery := filterQuery + filterClause + " GROUP BY category, value ORDER BY category, count DESC"

		rows, err := db.Conn.Query(r.Context(), filteredQuery, otherArgs...)
		if err != nil {
			log.Printf("Failed to list filters: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		results := make(map[string]map[string]int)
		for rows.Next() {
			var category, value string
			var count int
			if err := rows.Scan(&category, &value, &count); err != nil {
				continue
			}
			// Skip selected categories in this query
			if _, isSelected := selectedCategories[category]; isSelected {
				continue
			}
			if results[category] == nil {
				results[category] = make(map[string]int)
			}
			results[category][value] = count
		}
		if err := rows.Err(); err != nil {
			log.Printf("Row iteration error: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		rows.Close()

		// Query 2: Get selected categories WITHOUT filters (show all options)
		unfilteredQuery := filterQuery + " GROUP BY category, value ORDER BY category, count DESC"
		rows, err = db.Conn.Query(r.Context(), unfilteredQuery, args...)
		if err != nil {
			log.Printf("Failed to list unfiltered filters: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		for rows.Next() {
			var category, value string
			var count int
			if err := rows.Scan(&category, &value, &count); err != nil {
				continue
			}
			// Only include selected categories
			if _, isSelected := selectedCategories[category]; !isSelected {
				continue
			}
			if results[category] == nil {
				results[category] = make(map[string]int)
			}
			results[category][value] = count
		}
		if err := rows.Err(); err != nil {
			log.Printf("Row iteration error: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		// Convert to response
		var filters []FilterCategory
		for cat, values := range results {
			var fv []FilterValue
			for val, cnt := range values {
				fv = append(fv, FilterValue{Value: val, Count: cnt})
			}
			sort.Slice(fv, func(i, j int) bool { return fv[i].Count > fv[j].Count })
			filters = append(filters, FilterCategory{Category: cat, Values: fv})
		}
		sort.Slice(filters, func(i, j int) bool {
			sumI := 0
			for _, v := range filters[i].Values {
				sumI += v.Count
			}
			sumJ := 0
			for _, v := range filters[j].Values {
				sumJ += v.Count
			}
			return sumI > sumJ
		})

		_ = json.NewEncoder(w).Encode(filters)
		return
	}

	// No selected categories - simple query
	finalQuery := filterQuery + " GROUP BY category, value ORDER BY category, count DESC"

	rows, err := db.Conn.Query(r.Context(), finalQuery, args...)
	if err != nil {
		log.Printf("Failed to list filters: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var filters []FilterCategory
	currentCategory := ""
	var currentValues []FilterValue

	for rows.Next() {
		var category, value string
		var count int
		if err := rows.Scan(&category, &value, &count); err != nil {
			continue
		}
		if category != currentCategory {
			if currentCategory != "" {
				filters = append(filters, FilterCategory{Category: currentCategory, Values: currentValues})
			}
			currentCategory = category
			currentValues = []FilterValue{}
		}
		currentValues = append(currentValues, FilterValue{Value: value, Count: count})
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	if currentCategory != "" {
		filters = append(filters, FilterCategory{Category: currentCategory, Values: currentValues})
	}

	// Deduplicate categories - use a map to ensure each category appears only once
	seenCategories := make(map[string]bool)
	var uniqueFilters []FilterCategory
	for _, f := range filters {
		if !seenCategories[f.Category] {
			seenCategories[f.Category] = true
			uniqueFilters = append(uniqueFilters, f)
		}
	}
	filters = uniqueFilters

	_ = json.NewEncoder(w).Encode(filters)
}

func UploadImage(w http.ResponseWriter, r *http.Request) {
	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	adminRole, _ := r.Context().Value(mw.UserRoleKey).(string)
	if adminRole != "admin" {
		http.Error(w, "Admin access required", http.StatusForbidden)
		return
	}

	if s3.Client == nil {
		log.Printf("S3 client not initialized")
		http.Error(w, "Storage not configured", http.StatusInternalServerError)
		return
	}

	if err := s3.Client.HealthCheck(r.Context()); err != nil {
		log.Printf("S3 pre-flight health check failed: %v", err)
		http.Error(w, "Storage backend is unreachable — upload cannot proceed", http.StatusServiceUnavailable)
		return
	}

	//nolint:errcheck // parse errors are non-fatal; form values may still be available
	_ = r.ParseMultipartForm(50 << 20)

	tagsStr := r.FormValue("tags")
	tags := []string{}
	if tagsStr != "" {
		for t := range strings.SplitSeq(tagsStr, ",") {
			if trimmed := strings.TrimSpace(t); trimmed != "" {
				tags = append(tags, trimmed)
			}
		}
	}

	type formatFile struct {
		fieldName string
		data      []byte
		ext       string
	}

	fullFields := []formatFile{
		{fieldName: "file_full_avif"},
		{fieldName: "file_full_webp"},
		{fieldName: "file_full_jpg"},
	}
	optionalFullFields := []formatFile{
		{fieldName: "file_full_jxl"},
	}
	thumbFields := []formatFile{
		{fieldName: "file_thumb_avif"},
		{fieldName: "file_thumb_webp"},
		{fieldName: "file_thumb_jpg"},
	}

	for i := range fullFields {
		f, _, err := r.FormFile(fullFields[i].fieldName)
		if err != nil {
			log.Printf("Failed to get %s: %v", fullFields[i].fieldName, err)
			http.Error(w, fmt.Sprintf("Missing %s", fullFields[i].fieldName), http.StatusBadRequest)
			return
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			log.Printf("Failed to read %s: %v", fullFields[i].fieldName, err)
			http.Error(w, fmt.Sprintf("Failed to read %s", fullFields[i].fieldName), http.StatusBadRequest)
			return
		}
		ext, err := images.ValidateImageBytes(data)
		if err != nil {
			http.Error(w, fmt.Sprintf("Invalid %s: %v", fullFields[i].fieldName, err), http.StatusBadRequest)
			return
		}
		fullFields[i].data = data
		fullFields[i].ext = ext
	}

	for i := range thumbFields {
		f, _, err := r.FormFile(thumbFields[i].fieldName)
		if err != nil {
			log.Printf("Failed to get %s: %v", thumbFields[i].fieldName, err)
			http.Error(w, fmt.Sprintf("Missing %s", thumbFields[i].fieldName), http.StatusBadRequest)
			return
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			log.Printf("Failed to read %s: %v", thumbFields[i].fieldName, err)
			http.Error(w, fmt.Sprintf("Failed to read %s", thumbFields[i].fieldName), http.StatusBadRequest)
			return
		}
		ext, err := images.ValidateImageBytes(data)
		if err != nil {
			http.Error(w, fmt.Sprintf("Invalid %s: %v", thumbFields[i].fieldName, err), http.StatusBadRequest)
			return
		}
		thumbFields[i].data = data
		thumbFields[i].ext = ext
	}

	for i := range optionalFullFields {
		f, _, err := r.FormFile(optionalFullFields[i].fieldName)
		if err != nil {
			continue
		}
		defer f.Close()
		data, err := io.ReadAll(f)
		if err != nil {
			continue
		}
		ext, err := images.ValidateImageBytes(data)
		if err != nil {
			continue
		}
		optionalFullFields[i].data = data
		optionalFullFields[i].ext = ext
	}

	timestamp := clock.Now().UnixNano()
	bucket := "crussell"

	var fullURLs FullFormatURLs
	var thumbURLs ThumbFormatURLs

	for _, ff := range fullFields {
		if ff.data == nil {
			continue
		}
		key := fmt.Sprintf("portfolio/%d_full%s", timestamp, ff.ext)
		contentType := mimeTypeForField(ff.fieldName)
		if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(ff.data), contentType); err != nil {
			log.Printf("Failed to upload %s to S3: %v", ff.fieldName, err)
			http.Error(w, "Failed to upload image", http.StatusInternalServerError)
			return
		}
		url, err := s3.Client.GetURL(r.Context(), bucket, key)
		if err != nil {
			log.Printf("Failed to get URL for %s: %v", ff.fieldName, err)
			http.Error(w, "Failed to get URL", http.StatusInternalServerError)
			return
		}
		switch ff.fieldName {
		case "file_full_avif":
			fullURLs.Avif = url
		case "file_full_webp":
			fullURLs.Webp = url
		case "file_full_jpg":
			fullURLs.Jpg = url
		case "file_full_jxl":
			fullURLs.Jxl = url
		}
	}

	for _, ff := range optionalFullFields {
		if ff.data == nil {
			continue
		}
		key := fmt.Sprintf("portfolio/%d_full%s", timestamp, ff.ext)
		contentType := mimeTypeForField(ff.fieldName)
		if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(ff.data), contentType); err != nil {
			log.Printf("Failed to upload %s to S3: %v", ff.fieldName, err)
			continue
		}
		url, err := s3.Client.GetURL(r.Context(), bucket, key)
		if err != nil {
			log.Printf("Failed to get URL for %s: %v", ff.fieldName, err)
			continue
		}
		if ff.fieldName == "file_full_jxl" {
			fullURLs.Jxl = url
		}
	}

	for _, tf := range thumbFields {
		key := fmt.Sprintf("portfolio/%d_thumb%s", timestamp, tf.ext)
		contentType := mimeTypeForField(tf.fieldName)
		if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(tf.data), contentType); err != nil {
			log.Printf("Failed to upload %s to S3: %v", tf.fieldName, err)
			http.Error(w, "Failed to upload thumbnail", http.StatusInternalServerError)
			return
		}
		url, err := s3.Client.GetURL(r.Context(), bucket, key)
		if err != nil {
			log.Printf("Failed to get URL for %s: %v", tf.fieldName, err)
			http.Error(w, "Failed to get thumbnail URL", http.StatusInternalServerError)
			return
		}
		switch tf.fieldName {
		case "file_thumb_avif":
			thumbURLs.Avif = url
		case "file_thumb_webp":
			thumbURLs.Webp = url
		case "file_thumb_jpg":
			thumbURLs.Jpg = url
		}
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	var imgID string
	err = tx.QueryRow(r.Context(), `
		INSERT INTO images (url, thumbnail_url, tag_names, full_avif_url, full_webp_url, full_jpg_url, full_jxl_url, thumb_avif_url, thumb_webp_url, thumb_jpg_url)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
		RETURNING id
	`, fullURLs.Avif, thumbURLs.Webp, tags, fullURLs.Avif, fullURLs.Webp, fullURLs.Jpg, fullURLs.Jxl, thumbURLs.Avif, thumbURLs.Webp, thumbURLs.Jpg).Scan(&imgID)

	if err != nil {
		log.Printf("Failed to insert image record: %v", err)
		http.Error(w, "Failed to save image record", http.StatusInternalServerError)
		return
	}

	for _, tag := range tags {
		if tag == "" {
			continue
		}
		_, err = tx.Exec(r.Context(), `
			INSERT INTO tags (name) VALUES ($1)
			ON CONFLICT (name) DO NOTHING
		`, tag)
		if err != nil {
			log.Printf("Failed to insert tag %s: %v", tag, err)
		}
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(Image{
		ID:           imgID,
		URL:          fullURLs.Avif,
		ThumbnailURL: thumbURLs.Webp,
		Full:         fullURLs,
		Thumb:        thumbURLs,
		TagNames:     tags,
		CreatedAt:    clock.Now(),
	})
}

func DeleteImage(w http.ResponseWriter, r *http.Request) {
	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	adminRole, _ := r.Context().Value(mw.UserRoleKey).(string)
	if adminRole != "admin" {
		http.Error(w, "Admin access required", http.StatusForbidden)
		return
	}

	imageID := chi.URLParam(r, "id")
	if imageID == "" || !validators.IsValidID(imageID) {
		http.Error(w, "Image not found", http.StatusNotFound)
		return
	}

	var img Image
	var fullAvif, fullWebp, fullJpg, fullJxl sql.NullString
	var thumbAvif, thumbWebp, thumbJpg sql.NullString
	err := db.Conn.QueryRow(r.Context(), `
		SELECT url, thumbnail_url,
		       full_avif_url, full_webp_url, full_jpg_url, full_jxl_url,
		       thumb_avif_url, thumb_webp_url, thumb_jpg_url
		FROM images WHERE id = $1
	`, imageID).Scan(
		&img.URL, &img.ThumbnailURL,
		&fullAvif, &fullWebp, &fullJpg, &fullJxl,
		&thumbAvif, &thumbWebp, &thumbJpg,
	)

	if err != nil {
		log.Printf("Failed to find image: %v", err)
		http.Error(w, "Image not found", http.StatusNotFound)
		return
	}

	if fullAvif.Valid {
		img.Full.Avif = fullAvif.String
	}
	if fullWebp.Valid {
		img.Full.Webp = fullWebp.String
	}
	if fullJpg.Valid {
		img.Full.Jpg = fullJpg.String
	}
	if fullJxl.Valid {
		img.Full.Jxl = fullJxl.String
	}
	if thumbAvif.Valid {
		img.Thumb.Avif = thumbAvif.String
	}
	if thumbWebp.Valid {
		img.Thumb.Webp = thumbWebp.String
	}
	if thumbJpg.Valid {
		img.Thumb.Jpg = thumbJpg.String
	}

	if s3.Client != nil {
		urls := []string{
			img.URL, img.ThumbnailURL,
			img.Full.Avif, img.Full.Webp, img.Full.Jpg, img.Full.Jxl,
			img.Thumb.Avif, img.Thumb.Webp, img.Thumb.Jpg,
		}
		for _, u := range urls {
			if u == "" {
				continue
			}
			key := extractKey(u)
			if err := s3.Client.Delete(context.Background(), "crussell", key); err != nil {
		slog.Warn("failed to delete S3 object", "err", err)
	}
		}
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	_, err = tx.Exec(r.Context(), `DELETE FROM images WHERE id = $1`, imageID)
	if err != nil {
		log.Printf("Failed to delete image: %v", err)
		http.Error(w, "Failed to delete image", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func extractKey(url string) string {
	// URL format: https://endpoint/bucket/portfolio/1234567890.jpg
	// Need to return: portfolio/1234567890.jpg
	// Find the bucket segment: skip past scheme://endpoint/
	_, after, ok := strings.Cut(url, "://")
	if !ok {
		return url
	}
	rest := after // skip "://"
	// Now rest = "endpoint/bucket/portfolio/1234567890.jpg"
	// Skip first path segment (endpoint)
	slashIdx := strings.Index(rest, "/")
	if slashIdx == -1 {
		return rest
	}
	rest = rest[slashIdx+1:] // "bucket/portfolio/1234567890.jpg"
	// Skip second path segment (bucket)
	slashIdx = strings.Index(rest, "/")
	if slashIdx == -1 {
		return rest
	}
	return rest[slashIdx+1:] // "portfolio/1234567890.jpg"
}

func GetImage(w http.ResponseWriter, r *http.Request) {
	imageID := chi.URLParam(r, "id")

	// Validate input length
	if err := validateInputLength(imageID); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	var img Image

	// Lookup by timestamp (nanosecond Unix epoch from URL)
	// Only allow numeric timestamps to prevent pattern enumeration
	timestampMatch, _ := regexp.Compile(`^\d{15,20}$`)
	if !timestampMatch.MatchString(imageID) {
		log.Printf("Invalid image ID format: %s", imageID)
		http.Error(w, "Image not found", http.StatusNotFound)
		return
	}

	searchPattern := "%" + imageID + ".%"
	var fullAvif, fullWebp, fullJpg, fullJxl sql.NullString
	var thumbAvif, thumbWebp, thumbJpg sql.NullString
	err := db.Conn.QueryRow(r.Context(), `
		SELECT id, url, thumbnail_url, tag_names, created_at,
		       full_avif_url, full_webp_url, full_jpg_url, full_jxl_url,
		       thumb_avif_url, thumb_webp_url, thumb_jpg_url
		FROM images
		WHERE url LIKE $1 OR thumbnail_url LIKE $1
		LIMIT 1
	`, searchPattern).Scan(
		&img.ID, &img.URL, &img.ThumbnailURL,
		(*[]string)(&img.TagNames), &img.CreatedAt,
		&fullAvif, &fullWebp, &fullJpg, &fullJxl,
		&thumbAvif, &thumbWebp, &thumbJpg,
	)

	if err != nil {
		log.Printf("Failed to get image: %v", err)
		http.Error(w, "Image not found", http.StatusNotFound)
		return
	}

	if fullAvif.Valid {
		img.Full.Avif = fullAvif.String
	}
	if fullWebp.Valid {
		img.Full.Webp = fullWebp.String
	}
	if fullJpg.Valid {
		img.Full.Jpg = fullJpg.String
	}
	if fullJxl.Valid {
		img.Full.Jxl = fullJxl.String
	}
	if thumbAvif.Valid {
		img.Thumb.Avif = thumbAvif.String
	}
	if thumbWebp.Valid {
		img.Thumb.Webp = thumbWebp.String
	}
	if thumbJpg.Valid {
		img.Thumb.Jpg = thumbJpg.String
	}

	_ = json.NewEncoder(w).Encode(img)
}
