package portfolio

import (
	"bytes"
	"context"
	"crussell/db"
	"crussell/internal/s3"
	"crussell/mw"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

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

const MaxInputLength = 256

// processImage strips metadata and auto-orients the image
func processImage(data []byte, quality int) ([]byte, error) {
	// Decode the image - this automatically applies EXIF orientation
	// and strips all metadata (EXIF, GPS, etc.)
	img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
	if err != nil {
		return nil, fmt.Errorf("failed to decode image: %w", err)
	}

	// Encode to JPEG without any metadata
	var buf bytes.Buffer
	err = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(quality))
	if err != nil {
		return nil, fmt.Errorf("failed to encode image: %w", err)
	}

	return buf.Bytes(), nil
}

// 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.DB.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
		}
	}
	return categories, nil
}

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

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

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
	offset := 0

	if l := r.URL.Query().Get("limit"); l != "" {
		if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
			limit = parsed
		}
	}
	if o := r.URL.Query().Get("offset"); o != "" {
		if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
			offset = 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
	}

	filterClauses := ""
	filterArgs := []interface{}{}

	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 += fmt.Sprintf(" AND $%d = ANY(tag_names)", len(filterArgs)+1)
				filterArgs = append(filterArgs, category+":"+value)
			}
		}
	}

	var query string
	var args []interface{}

	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, " + ")

		argOffset := len(filterArgs)
		query = fmt.Sprintf(`
			SELECT id, url, thumbnail_url, tag_names, created_at,
			       COUNT(t) as match_count,
			       %s as relevance
			FROM images, unnest(tag_names) as t
			WHERE %s%s
			GROUP BY id, url, thumbnail_url, tag_names, created_at
			ORDER BY match_count DESC, relevance DESC, created_at DESC
			LIMIT $%d OFFSET $%d
		`, similaritySum, whereClause, filterClauses, argOffset+len(cleanTags)+1, argOffset+len(cleanTags)+2)

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

		queryArgs := make([]interface{}, len(filterArgs)+3)
		queryArgs[0] = searchPattern
		copy(queryArgs[1:], filterArgs)
		queryArgs[len(filterArgs)+1] = limit
		queryArgs[len(filterArgs)+2] = offset
		args = queryArgs
	} else {
		argOffset := len(filterArgs)
		query = fmt.Sprintf(`
			SELECT id, url, thumbnail_url, tag_names, created_at, 0 as match_count, 0.0 as relevance
			FROM images
			WHERE 1=1%s
			ORDER BY created_at DESC
			LIMIT $%d OFFSET $%d
		`, filterClauses, argOffset+1, argOffset+2)

		queryArgs := make([]interface{}, len(filterArgs)+2)
		copy(queryArgs, filterArgs)
		queryArgs[len(filterArgs)] = limit
		queryArgs[len(filterArgs)+1] = offset
		args = queryArgs
	}

	rows, err := db.DB.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
		if err := rows.Scan(&img.ID, &img.URL, &img.ThumbnailURL, (*[]string)(&img.TagNames), &img.CreatedAt, &matchCount, &relevance); err != nil {
			log.Printf("Failed to scan image: %v", err)
			continue
		}
		images = append(images, img)
	}

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

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(images)
}

func ListTags(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query().Get("q")

	var query string
	var args []interface{}

	// 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 = []interface{}{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.DB.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 tags == nil {
		tags = []Tag{}
	}

	w.Header().Set("Content-Type", "application/json")
	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[")
			selectedCategories[category] = values[0]
		}
	}

	// Build base query
	baseQuery := "SELECT DISTINCT id FROM images WHERE 1=1"
	args := []interface{}{}
	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([]interface{}, 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.DB.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
		}
		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.DB.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
		}

		// 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
		})

		w.Header().Set("Content-Type", "application/json")
		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.DB.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 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

	w.Header().Set("Content-Type", "application/json")
	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
	}

	r.ParseMultipartForm(10 << 20)

	file, header, err := r.FormFile("file")
	if err != nil {
		log.Printf("Failed to get file: %v", err)
		http.Error(w, "No file provided", http.StatusBadRequest)
		return
	}
	defer file.Close()

	thumbFile, thumbHeader, err := r.FormFile("thumbnail")
	if err != nil {
		log.Printf("Failed to get thumbnail: %v", err)
		http.Error(w, "No thumbnail provided", http.StatusBadRequest)
		return
	}
	defer thumbFile.Close()

	tagsStr := r.FormValue("tags")
	var tags []string
	if tagsStr != "" {
		tags = strings.Split(tagsStr, ",")
		for i := range tags {
			tags[i] = strings.TrimSpace(tags[i])
		}
	}

	// Use nanosecond timestamp for unique keys
	timestamp := time.Now().UnixNano()

	// Extract extension from main file
	mainExt := ".jpg"
	if idx := strings.LastIndex(header.Filename, "."); idx != -1 {
		mainExt = strings.ToLower(header.Filename[idx:])
	}

	// Extract extension from thumbnail file
	thumbExt := ".jpg"
	if idx := strings.LastIndex(thumbHeader.Filename, "."); idx != -1 {
		thumbExt = strings.ToLower(thumbHeader.Filename[idx:])
	}

	key := fmt.Sprintf("portfolio/%d%s", timestamp, mainExt)
	thumbKey := fmt.Sprintf("portfolio/%d_thumb%s", timestamp, thumbExt)

	fileBytes, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read file: %v", err)
		http.Error(w, "Failed to read file", http.StatusInternalServerError)
		return
	}
	// Images are already processed by frontend (compressed, metadata stripped)
	// Skip re-encoding - just use the uploaded bytes directly

	thumbBytes, err := io.ReadAll(thumbFile)
	if err != nil {
		log.Printf("Failed to read thumbnail: %v", err)
		http.Error(w, "Failed to read thumbnail", http.StatusInternalServerError)
		return
	}

	// Thumbnail is already processed by frontend - use as-is

	bucket := "crussell"

	if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(fileBytes)); err != nil {
		log.Printf("Failed to upload image to S3: %v", err)
		http.Error(w, "Failed to upload image", http.StatusInternalServerError)
		return
	}

	if err := s3.Client.Upload(r.Context(), bucket, thumbKey, bytes.NewReader(thumbBytes)); err != nil {
		log.Printf("Failed to upload thumbnail to S3: %v", 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 image URL: %v", err)
		http.Error(w, "Failed to get URL", http.StatusInternalServerError)
		return
	}

	thumbURL, err := s3.Client.GetURL(r.Context(), bucket, thumbKey)
	if err != nil {
		log.Printf("Failed to get thumbnail URL: %v", err)
		http.Error(w, "Failed to get thumbnail URL", http.StatusInternalServerError)
		return
	}

	var imgID string
	err = db.DB.QueryRow(r.Context(), `
		INSERT INTO images (url, thumbnail_url, tag_names)
		VALUES ($1, $2, $3)
		RETURNING id
	`, url, thumbURL, tags).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 = db.DB.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)
		}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(Image{
		ID:           imgID,
		URL:          url,
		ThumbnailURL: thumbURL,
		TagNames:     tags,
		CreatedAt:    time.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")

	var url, thumbURL string
	err := db.DB.QueryRow(r.Context(), `
		SELECT url, thumbnail_url FROM images WHERE id = $1
	`, imageID).Scan(&url, thumbURL)

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

	if s3.Client != nil {
		key := extractKey(url)
		thumbKey := extractKey(thumbURL)

		s3.Client.Delete(context.Background(), "crussell", key)
		s3.Client.Delete(context.Background(), "crussell", thumbKey)
	}

	_, err = db.DB.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
	}

	w.WriteHeader(http.StatusNoContent)
}

func extractKey(url string) string {
	parts := strings.Split(url, "/")
	if len(parts) > 0 {
		return parts[len(parts)-1]
	}
	return url
}

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 + ".%"
	err := db.DB.QueryRow(r.Context(), `
		SELECT id, url, thumbnail_url, tag_names, created_at
		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)

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

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(img)
}
