package handlers

import (
	"bookhoard/internal/database"
	"bookhoard/internal/services"
	wsync "bookhoard/internal/sync"
	"bookhoard/internal/utils"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

// CreateMediaItemRequest represents the request for creating a media item
type CreateMediaItemRequest struct {
	LibraryID      uuid.UUID `json:"library_id" validate:"required"`
	Title          string    `json:"title" validate:"required,min=1,max=500"`
	Author         string    `json:"author"`
	ISBN           string    `json:"isbn"`
	Description    string    `json:"description"`
	FilePath       string    `json:"file_path" validate:"required"`
	FileSize       int64     `json:"file_size" validate:"required,min=1"`
	MimeType       string    `json:"mime_type" validate:"required"`
	CoverImagePath string    `json:"cover_image_path"`
	Series         string    `json:"series"`
	SeriesNumber   int32     `json:"series_number"`
	Tags           []string  `json:"tags"`
	ASIN           string    `json:"asin"`
	DatePublished  string    `json:"date_published"`
	Publisher      string    `json:"publisher"`
	Contributors   []string  `json:"contributors"`
	// NEW: Reading direction and comic metadata fields
	MangaType        string  `json:"manga_type"`        // 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left'
	ReadingDirection string  `json:"reading_direction"` // 'auto' | 'ltr' | 'rtl' | 'vertical'
	SeriesCount      int32   `json:"series_count"`
	Volume           int32   `json:"volume"`
	Imprint          string  `json:"imprint"`
	AgeRating        string  `json:"age_rating"` // 'Everyone' | 'Teen' | 'Mature' | 'Adult'
	WebURL           string  `json:"web_url"`
	MetadataNotes    string  `json:"metadata_notes"`
	CommunityRating  float64 `json:"community_rating"`
	StoryArc         string  `json:"story_arc"`
	IsBlackAndWhite  bool    `json:"is_black_and_white"`
	AlternateInfo    string  `json:"alternate_info"` // JSON string
	ScanInformation  string  `json:"scan_information"`
	Summary          string  `json:"summary"`
}

// UpdateMediaItemRequest represents the request for updating a media item
type UpdateMediaItemRequest struct {
	Title          string   `form:"title" json:"title" validate:"required,min=1,max=500"`
	Author         string   `form:"author" json:"author"`
	ISBN           string   `form:"isbn" json:"isbn"`
	Description    string   `form:"description" json:"description"`
	CoverImagePath string   `form:"cover_image_path" json:"cover_image_path"`
	CoverAction    string   `form:"cover_action" json:"cover_action"`
	Series         string   `form:"series" json:"series"`
	SeriesNumber   int32    `form:"series_number" json:"series_number"`
	Tags           []string `form:"tags" json:"tags"`
	ASIN           string   `form:"asin" json:"asin"`
	DatePublished  string   `form:"date_published" json:"date_published"`
	Publisher      string   `form:"publisher" json:"publisher"`
	Contributors   []string `form:"contributors" json:"contributors"`
	Language       string   `form:"language" json:"language"`
	Edition        string   `form:"edition" json:"edition"`
	PageCount      int32    `form:"page_count" json:"page_count"`
	Genre          string   `form:"genre" json:"genre"`
	CopyrightYear  int32    `form:"copyright_year" json:"copyright_year"`
	GoodreadsID    string   `form:"goodreads_id" json:"goodreads_id"`
	OpenlibraryID  string   `form:"openlibrary_id" json:"openlibrary_id"`
	GoogleBooksID  string   `form:"google_books_id" json:"google_books_id"`
	MangaType        string  `form:"manga_type" json:"manga_type"`
	ReadingDirection string  `form:"reading_direction" json:"reading_direction"`
	SeriesCount      int32   `form:"series_count" json:"series_count"`
	Volume           int32   `form:"volume" json:"volume"`
	Imprint          string  `form:"imprint" json:"imprint"`
	AgeRating        string  `form:"age_rating" json:"age_rating"`
	WebURL           string  `form:"web_url" json:"web_url"`
	MetadataNotes    string  `form:"metadata_notes" json:"metadata_notes"`
	CommunityRating  float64 `form:"community_rating" json:"community_rating"`
	StoryArc         string  `form:"story_arc" json:"story_arc"`
	IsBlackAndWhite  bool    `form:"is_black_and_white" json:"is_black_and_white"`
	AlternateInfo    string  `form:"alternate_info" json:"alternate_info"`
	ScanInformation  string  `form:"scan_information" json:"scan_information"`
	Summary          string  `form:"summary" json:"summary"`
}

// CreateMediaNoteRequest represents the request for creating a media note
type CreateMediaNoteRequest struct {
	Content  string `json:"content" validate:"required,min=1,max=10000"`
	Position string `json:"position"`
}

// UpdateMediaNoteRequest represents the request for updating a media note
type UpdateMediaNoteRequest struct {
	Content  string `json:"content" validate:"required,min=1,max=10000"`
	Position string `json:"position"`
}

// CreateMediaHighlightRequest represents the request for creating a media highlight
type CreateMediaHighlightRequest struct {
	SelectionText    string  `json:"selection_text" validate:"required,min=1,max=5000"`
	StartPosition    string  `json:"start_position" validate:"max=1000"`
	EndPosition      string  `json:"end_position" validate:"max=1000"`
	EpubcfiStart     string  `json:"epubcfi_start" validate:"max=2000"`
	EpubcfiEnd       string  `json:"epubcfi_end" validate:"max=2000"`
	Color            string  `json:"color" validate:"omitempty,len=7"`
	NoteText         string  `json:"note_text" validate:"max=10000"`
	NoteID           string  `json:"note_id"`
	PercentageStart  float64 `json:"percentage_start"`
	PercentageEnd    float64 `json:"percentage_end"`
	ChapterReference int32   `json:"chapter_reference"`
}

// UpdateMediaHighlightRequest represents the request for updating a media highlight
type UpdateMediaHighlightRequest struct {
	SelectionText    string  `json:"selection_text" validate:"required,min=1,max=5000"`
	StartPosition    string  `json:"start_position" validate:"max=1000"`
	EndPosition      string  `json:"end_position" validate:"max=1000"`
	EpubcfiStart     string  `json:"epubcfi_start" validate:"max=2000"`
	EpubcfiEnd       string  `json:"epubcfi_end" validate:"max=2000"`
	Color            string  `json:"color" validate:"omitempty,len=7"`
	NoteText         string  `json:"note_text" validate:"max=10000"`
	NoteID           string  `json:"note_id"`
	PercentageStart  float64 `json:"percentage_start"`
	PercentageEnd    float64 `json:"percentage_end"`
	ChapterReference int32   `json:"chapter_reference"`
}

// CreateMediaBookmarkRequest represents the request for creating a media bookmark
type CreateMediaBookmarkRequest struct {
	Title            string  `json:"title" validate:"required,min=1,max=255"`
	Position         string  `json:"position" validate:"max=100"`
	Notes            string  `json:"notes" validate:"max=10000"`
	CfiPosition      string  `json:"cfi_position" validate:"max=255"`
	PageNumber       int32   `json:"page_number"`
	ChapterNumber    int32   `json:"chapter_number"`
	Percentage       float64 `json:"percentage"`
	ChapterReference int32   `json:"chapter_reference"`
}

// UpdateMediaBookmarkRequest represents the request for updating a media bookmark
type UpdateMediaBookmarkRequest struct {
	Title    string `json:"title" validate:"required,min=1,max=255"`
	Notes    string `json:"notes" validate:"max=10000"`
	Position string `json:"position" validate:"max=100"`
}

type MediaHandler struct {
	db             *database.Queries
	worker         *services.Worker
	libraryService *services.LibraryService
	searchService  *services.SearchService
	progressSvc    *wsync.ProgressService
	annotationSvc  *wsync.AnnotationService
}

func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, worker ...*services.Worker) *MediaHandler {
	mh := &MediaHandler{
		db:             db,
		libraryService: libraryService,
		searchService:  services.NewSearchService(db),
	}
	if len(worker) > 0 && worker[0] != nil {
		mh.worker = worker[0]
	}
	return mh
}

func (mh *MediaHandler) SetProgressService(svc *wsync.ProgressService) {
	mh.progressSvc = svc
}

func (mh *MediaHandler) SetAnnotationService(svc *wsync.AnnotationService) {
	mh.annotationSvc = svc
}

func (h *MediaHandler) DownloadBook(c *echo.Context) error {
	bookUUID, err := uuid.Parse(c.Param("uuid"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book UUID"})
	}

	pgBookUUID := pgtype.UUID{Bytes: bookUUID, Valid: true}

	mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgBookUUID)
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "book not found"})
	}

	// Resolve relative path to absolute filesystem path
	fullPath, err := h.getFullFilePath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "book file not found on disk"})
	}

	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "book file not found on disk"})
	}

	file, err := os.Open(fullPath)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to open book file"})
	}
	defer file.Close()

	mimeType := mediaItem.MimeType.String
	if !mediaItem.MimeType.Valid || mimeType == "" {
		mimeType = mime.TypeByExtension(filepath.Ext(mediaItem.FilePath))
	}

	c.Response().Header().Set("Content-Type", mimeType)
	c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+filepath.Base(mediaItem.FilePath)+"\"")

	if mediaItem.FileSize.Valid && mediaItem.FileSize.Int64 > 0 {
		c.Response().Header().Set("Content-Length", strconv.FormatInt(mediaItem.FileSize.Int64, 10))
	}

	_, err = io.Copy(c.Response(), file)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to stream file"})
	}

	return nil
}

// ExecuteSearch performs search and returns results with count
// Public wrapper for shared search logic used by both JSON and HTML endpoints
func (h *MediaHandler) ExecuteSearch(ctx context.Context, params services.SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
	return h.searchService.ExecuteSearch(ctx, params)
}

type AddToShelfRequest struct {
	MediaItemIDs  []string `json:"media_item_ids" validate:"required"`
	ShelfName     string   `json:"shelf_name"`
	ShelfPosition int      `json:"shelf_position"`
}

func (h *MediaHandler) AddToShelf(c *echo.Context) error {
	deviceUUID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
	}

	var req AddToShelfRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
	}

	if req.ShelfName == "" {
		req.ShelfName = "Default"
	}

	pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
	addedCount := 0

	for _, mediaID := range req.MediaItemIDs {
		mediaUUID, err := uuid.Parse(mediaID)
		if err != nil {
			continue
		}

		_, err = h.db.AddBookToKoboShelf(c.Request().Context(), database.AddBookToKoboShelfParams{
			DeviceID:      pgDeviceUUID,
			MediaItemID:   pgtype.UUID{Bytes: mediaUUID, Valid: true},
			ShelfName:     pgtype.Text{String: req.ShelfName, Valid: true},
			ShelfPosition: pgtype.Int4{Int32: int32(req.ShelfPosition), Valid: true},
		})

		if err == nil {
			addedCount++
			req.ShelfPosition++
		}
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":      "success",
		"added_count": addedCount,
		"shelf_name":  req.ShelfName,
		"total_count": len(req.MediaItemIDs),
	})
}

func (h *MediaHandler) GetShelf(c *echo.Context) error {
	deviceUUID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
	}

	shelfName := c.QueryParam("shelf")
	pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}

	var shelfBooks []database.GetKoboShelfBooksByShelfNameRow
	if shelfName != "" {
		shelfBooks, err = h.db.GetKoboShelfBooksByShelfName(c.Request().Context(), database.GetKoboShelfBooksByShelfNameParams{
			DeviceID:  pgDeviceUUID,
			ShelfName: pgtype.Text{String: shelfName, Valid: true},
		})
	} else {
		var defaultBooks []database.GetKoboShelfBooksRow
		defaultBooks, err = h.db.GetKoboShelfBooks(c.Request().Context(), pgDeviceUUID)
		shelfBooks = make([]database.GetKoboShelfBooksByShelfNameRow, len(defaultBooks))
		for i, b := range defaultBooks {
			shelfBooks[i] = database.GetKoboShelfBooksByShelfNameRow{
				ID:             b.ID,
				DeviceID:       b.DeviceID,
				MediaItemID:    b.MediaItemID,
				ShelfName:      b.ShelfName,
				ShelfPosition:  b.ShelfPosition,
				AddedAt:        b.AddedAt,
				LastSyncedAt:   b.LastSyncedAt,
				Title:          b.Title,
				Author:         b.Author,
				MimeType:       b.MimeType,
				EntitlementID:  b.EntitlementID,
				KoboContentID:  b.KoboContentID,
				RevisionNumber: b.RevisionNumber,
			}
		}
	}

	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to fetch shelf"})
	}

	type ShelfBookResponse struct {
		ID             string `json:"id"`
		MediaItemID    string `json:"media_item_id"`
		Title          string `json:"title"`
		Author         string `json:"author"`
		FilePath       string `json:"file_path"`
		MimeType       string `json:"mime_type"`
		ShelfName      string `json:"shelf_name"`
		ShelfPosition  int    `json:"shelf_position"`
		EntitlementID  string `json:"entitlement_id,omitempty"`
		KoboContentID  string `json:"kobo_content_id,omitempty"`
		RevisionNumber int    `json:"revision_number"`
		AddedAt        string `json:"added_at"`
	}

	response := []ShelfBookResponse{}
	for _, book := range shelfBooks {
		response = append(response, ShelfBookResponse{
			ID:             uuid.UUID(book.ID.Bytes).String(),
			MediaItemID:    uuid.UUID(book.MediaItemID.Bytes).String(),
			Title:          book.Title,
			Author:         book.Author.String,
			MimeType:       book.MimeType.String,
			ShelfName:      book.ShelfName.String,
			ShelfPosition:  int(book.ShelfPosition.Int32),
			EntitlementID:  book.EntitlementID.String,
			KoboContentID:  book.KoboContentID.String,
			RevisionNumber: int(book.RevisionNumber.Int32),
			AddedAt:        book.AddedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
		})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":     "success",
		"device_id":  deviceUUID.String(),
		"shelf_name": shelfName,
		"books":      response,
		"total":      len(response),
	})
}

func (h *MediaHandler) RemoveFromShelf(c *echo.Context) error {
	deviceUUID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
	}

	mediaUUID, err := uuid.Parse(c.QueryParam("media_item_id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item ID"})
	}

	pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
	pgMediaUUID := pgtype.UUID{Bytes: mediaUUID, Valid: true}

	err = h.db.RemoveBookFromKoboShelf(c.Request().Context(), database.RemoveBookFromKoboShelfParams{
		DeviceID:    pgDeviceUUID,
		MediaItemID: pgMediaUUID,
	})

	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to remove from shelf"})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":  "success",
		"message": "book removed from shelf",
	})
}

func (h *MediaHandler) ClearShelf(c *echo.Context) error {
	deviceUUID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
	}

	shelfName := c.QueryParam("shelf")
	pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}

	var result string
	if shelfName != "" {
		err = h.db.ClearKoboShelfByName(c.Request().Context(), database.ClearKoboShelfByNameParams{
			DeviceID:  pgDeviceUUID,
			ShelfName: pgtype.Text{String: shelfName, Valid: true},
		})
		result = "shelf cleared"
	} else {
		err = h.db.ClearKoboShelf(c.Request().Context(), pgDeviceUUID)
		result = "all shelves cleared"
	}

	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to clear shelf"})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":  "success",
		"message": result,
	})
}

// Bulk Operations

// POST /api/media-items/bulk-delete
// Bulk delete media items
func (h *MediaHandler) HandleBulkDelete(c *echo.Context) error {
	var req struct {
		MediaItemIDs []string `json:"media_item_ids" validate:"required"`
	}

	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
	}

	if len(req.MediaItemIDs) == 0 {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_item_ids required"})
	}

	results := make([]map[string]interface{}, 0)
	successCount := 0
	failedCount := 0

	for _, mediaIDStr := range req.MediaItemIDs {
		mediaID, err := uuid.Parse(mediaIDStr)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": mediaIDStr,
				"status":        "error",
				"error":         "Invalid UUID",
			})
			failedCount++
			continue
		}

		mediaUUID := pgtype.UUID{Bytes: mediaID, Valid: true}

		media, err := h.db.GetMediaItem(c.Request().Context(), mediaUUID)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": mediaIDStr,
				"status":        "error",
				"error":         "Media item not found",
			})
			failedCount++
			continue
		}

		err = h.db.DeleteMediaItem(c.Request().Context(), mediaUUID)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": mediaIDStr,
				"status":        "error",
				"error":         err.Error(),
			})
			failedCount++
			continue
		}

		if media.FilePath != "" {
			if err := os.Remove(media.FilePath); err != nil {
				// Log file removal failure but don't fail the request
				// DB record is already deleted, which is the primary concern
				log.Printf("Warning: failed to remove file %s: %v", media.FilePath, err)
			}
		}

		results = append(results, map[string]interface{}{
			"media_item_id": mediaIDStr,
			"status":        "success",
		})
		successCount++
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"results": results,
		"total":   len(req.MediaItemIDs),
		"deleted": successCount,
		"failed":  failedCount,
	})
}

// POST /api/media-items/bulk-update
// Bulk update media item metadata
func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error {
	var req struct {
		MediaItemUpdates []struct {
			MediaItemID string `json:"media_item_id" validate:"required"`
			Updates     struct {
				Title    *string  `json:"title,omitempty"`
				Author   *string  `json:"author,omitempty"`
				Genre    *string  `json:"genre,omitempty"`
				Language *string  `json:"language,omitempty"`
				Tags     []string `json:"tags,omitempty"`
			} `json:"updates"`
		} `json:"media_item_updates" validate:"required"`
	}

	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
	}

	if len(req.MediaItemUpdates) == 0 {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_item_updates required"})
	}

	for _, update := range req.MediaItemUpdates {
		if update.MediaItemID == "" {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "media_item_id cannot be empty"})
		}
	}

	results := make([]map[string]interface{}, 0)
	successCount := 0
	failedCount := 0

	for _, update := range req.MediaItemUpdates {
		mediaID, err := uuid.Parse(update.MediaItemID)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": update.MediaItemID,
				"status":        "error",
				"error":         "Invalid UUID",
			})
			failedCount++
			continue
		}

		mediaUUID := pgtype.UUID{Bytes: mediaID, Valid: true}

		existingMedia, err := h.db.GetMediaItem(c.Request().Context(), mediaUUID)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": update.MediaItemID,
				"status":        "error",
				"error":         "Media item not found",
			})
			failedCount++
			continue
		}

		updateParams := database.UpdateMediaItemParams{
			ID:                 mediaUUID,
			Title:              existingMedia.Title,
			Author:             existingMedia.Author,
			Genre:              existingMedia.Genre,
			Language:           existingMedia.Language,
			Tags:               existingMedia.Tags,
			TagsSearch:         existingMedia.TagsSearch,
			Description:        existingMedia.Description,
			Publisher:          existingMedia.Publisher,
			CopyrightYear:      existingMedia.CopyrightYear,
			Isbn:               existingMedia.Isbn,
			Series:             existingMedia.Series,
			SeriesNumber:       existingMedia.SeriesNumber,
			Asin:               existingMedia.Asin,
			DatePublished:      existingMedia.DatePublished,
			Contributors:       existingMedia.Contributors,
			ContributorsSearch: existingMedia.ContributorsSearch,
			Edition:            existingMedia.Edition,
			PageCount:          existingMedia.PageCount,
			GoodreadsID:        existingMedia.GoodreadsID,
			OpenlibraryID:      existingMedia.OpenlibraryID,
			GoogleBooksID:      existingMedia.GoogleBooksID,
			CoverImagePath:     existingMedia.CoverImagePath,
			MangaType:          existingMedia.MangaType,
			ReadingDirection:   existingMedia.ReadingDirection,
			SeriesCount:        existingMedia.SeriesCount,
			Volume:             existingMedia.Volume,
			Imprint:            existingMedia.Imprint,
			AgeRating:          existingMedia.AgeRating,
			WebUrl:             existingMedia.WebUrl,
			MetadataNotes:      existingMedia.MetadataNotes,
			CommunityRating:    existingMedia.CommunityRating,
			StoryArc:           existingMedia.StoryArc,
			IsBlackAndWhite:    existingMedia.IsBlackAndWhite,
			AlternateInfo:      existingMedia.AlternateInfo,
			ScanInformation:    existingMedia.ScanInformation,
			Summary:            existingMedia.Summary,
		}

		if update.Updates.Title != nil {
			updateParams.Title = *update.Updates.Title
		}
		if update.Updates.Author != nil {
			updateParams.Author = pgtype.Text{String: *update.Updates.Author, Valid: true}
		}
		if update.Updates.Genre != nil {
			updateParams.Genre = pgtype.Text{String: *update.Updates.Genre, Valid: true}
		}
		if update.Updates.Language != nil {
			updateParams.Language = pgtype.Text{String: *update.Updates.Language, Valid: true}
		}
		if len(update.Updates.Tags) > 0 {
			normalizedTags := utils.NormalizeTags(update.Updates.Tags)
			updateParams.Tags = normalizedTags
			tagsSearch := utils.NormalizeTagsSearch(update.Updates.Tags)
			updateParams.TagsSearch = tagsSearch
		}

		_, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams)
		if err != nil {
			results = append(results, map[string]interface{}{
				"media_item_id": update.MediaItemID,
				"status":        "error",
				"error":         err.Error(),
			})
			failedCount++
			continue
		}

		results = append(results, map[string]interface{}{
			"media_item_id": update.MediaItemID,
			"status":        "success",
		})
		successCount++
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"results": results,
		"total":   len(req.MediaItemUpdates),
		"updated": successCount,
		"failed":  failedCount,
	})
}

// ListMediaItems handles GET /api/media-items
func (mh *MediaHandler) ListMediaItems(c *echo.Context) error {
	libraryID := c.QueryParam("library_id")
	sort := c.QueryParam("sort")
	limit, _ := strconv.Atoi(c.QueryParam("limit"))
	offset, _ := strconv.Atoi(c.QueryParam("offset"))

	if limit == 0 {
		limit = 50
	}

	if sort == "" {
		sort = "created_at DESC"
	}

	allowedSorts := map[string]bool{
		"created_at ASC":      true,
		"created_at DESC":     true,
		"title ASC":           true,
		"title DESC":          true,
		"author ASC":          true,
		"author DESC":         true,
		"series ASC":          true,
		"series DESC":         true,
		"date_published ASC":  true,
		"date_published DESC": true,
		"copyright_year ASC":  true,
		"copyright_year DESC": true,
		"page_count ASC":      true,
		"page_count DESC":     true,
		"genre ASC":           true,
		"genre DESC":          true,
	}

	if !allowedSorts[sort] {
		sort = "created_at DESC"
	}

	if limit > maxPaginationLimit {
		limit = maxPaginationLimit
	}

	if offset < 0 {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "offset cannot be negative"})
	}

	if libraryID != "" {
		libUUID, err := uuid.Parse(libraryID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
		}

		items, err := mh.db.ListMediaItemsSorted(c.Request().Context(), database.ListMediaItemsSortedParams{
			LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
			Sort:      pgtype.Text{String: sort, Valid: true},
			Limit:     pgtype.Int4{Int32: int32(limit), Valid: true},
			Offset:    pgtype.Int4{Int32: int32(offset), Valid: true},
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}

		return c.JSON(http.StatusOK, map[string]interface{}{"data": items})
	}

	items, err := mh.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
		Limit:  int32(limit),
		Offset: int32(offset),
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	resolvedItems := make([]map[string]interface{}, len(items))
	for i, item := range items {
		resolvedItems[i] = map[string]interface{}{
			"id":               uuid.UUID(item.ID.Bytes).String(),
			"library_id":       uuid.UUID(item.LibraryID.Bytes).String(),
			"title":            item.Title,
			"author":           textToString(item.Author),
			"isbn":             textToString(item.Isbn),
			"description":      textToString(item.Description),
			"file_path":        utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
			"file_size":        item.FileSize,
			"mime_type":        textToString(item.MimeType),
			"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
			"series":           textToString(item.Series),
			"series_number":    item.SeriesNumber,
			"tags":             item.Tags,
			"asin":             textToString(item.Asin),
			"date_published":   item.DatePublished.Time.Format("2006-01-02"),
			"publisher":        textToString(item.Publisher),
			"contributors":     item.Contributors,
			"language":         textToString(item.Language),
			"edition":          textToString(item.Edition),
			"page_count":       item.PageCount,
			"genre":            textToString(item.Genre),
			"created_at":       item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
			"updated_at":       item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
			"format_group":     item.FormatGroup, // already string
			// NEW: Reading direction fields
			"manga_type":        textToString(item.MangaType),
			"reading_direction": textToString(item.ReadingDirection),
			// NEW: Universal metadata fields
			"series_count":     item.SeriesCount,
			"volume":           item.Volume,
			"imprint":          textToString(item.Imprint),
			"age_rating":       textToString(item.AgeRating),
			"web_url":          textToString(item.WebUrl),
			"metadata_notes":   textToString(item.MetadataNotes),
			"community_rating": numericToFloat(item.CommunityRating),
			// NEW: Comic-specific fields
			"story_arc":          textToString(item.StoryArc),
			"is_black_and_white": item.IsBlackAndWhite,
			"alternate_info":     jsonBytesToMap(item.AlternateInfo),
			"scan_information":   textToString(item.ScanInformation),
			"summary":            textToString(item.Summary),
		}
	}
	return c.JSON(http.StatusOK, map[string]interface{}{"data": resolvedItems})
}

// GetMediaItem handles GET /api/media-items/:id
func (mh *MediaHandler) GetMediaItem(c *echo.Context) error {
	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"id":               uuid.UUID(item.ID.Bytes).String(),
		"library_id":       uuid.UUID(item.LibraryID.Bytes).String(),
		"title":            item.Title,
		"author":           textToString(item.Author),
		"isbn":             textToString(item.Isbn),
		"description":      textToString(item.Description),
		"file_path":        utils.ResolveMediaURL(item.LibraryID, pgtype.Text{String: item.FilePath, Valid: item.FilePath != ""}),
		"file_size":        item.FileSize,
		"mime_type":        textToString(item.MimeType),
		"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
		"series":           textToString(item.Series),
		"series_number":    item.SeriesNumber,
		"tags":             item.Tags,
		"asin":             textToString(item.Asin),
		"date_published":   item.DatePublished.Time.Format("2006-01-02"),
		"publisher":        textToString(item.Publisher),
		"contributors":     item.Contributors,
		"language":         textToString(item.Language),
		"edition":          textToString(item.Edition),
		"page_count":       item.PageCount,
		"genre":            textToString(item.Genre),
		"created_at":       item.CreatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
		"updated_at":       item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
		"format_group":     item.FormatGroup,
		"format_mimetype":  textToString(item.FormatMimetype),
		// NEW: Reading direction fields
		"manga_type":        textToString(item.MangaType),
		"reading_direction": textToString(item.ReadingDirection),
		// NEW: Universal metadata fields
		"series_count":     item.SeriesCount,
		"volume":           item.Volume,
		"imprint":          textToString(item.Imprint),
		"age_rating":       textToString(item.AgeRating),
		"web_url":          textToString(item.WebUrl),
		"metadata_notes":   textToString(item.MetadataNotes),
		"community_rating": numericToFloat(item.CommunityRating),
		// NEW: Comic-specific fields
		"story_arc":          textToString(item.StoryArc),
		"is_black_and_white": item.IsBlackAndWhite,
		"alternate_info":     jsonBytesToMap(item.AlternateInfo),
		"scan_information":   textToString(item.ScanInformation),
		"summary":            textToString(item.Summary),
		"library_type_name":  textPtrToString(item.LibraryTypeName),
	})
}

// CreateMediaRating handles POST /api/media-items/:id/rating
func (mh *MediaHandler) CreateMediaRating(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req struct {
		Rating int32 `json:"rating" validate:"required,min=1,max=10"`
	}
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	rating, err := mh.db.CreateMediaRating(c.Request().Context(), database.CreateMediaRatingParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
		Rating:      req.Rating,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusCreated, rating)
}

// GetMediaRating handles GET /api/media-items/:id/rating
func (mh *MediaHandler) GetMediaRating(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	rating, err := mh.db.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, rating)
}

// UpdateMediaRating handles PUT /api/media-items/:id/rating
func (mh *MediaHandler) UpdateMediaRating(c *echo.Context) error {
	return mh.CreateMediaRating(c)
}

// DeleteMediaRating handles DELETE /api/media-items/:id/rating
func (mh *MediaHandler) DeleteMediaRating(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	err = mh.db.DeleteMediaRating(c.Request().Context(), database.DeleteMediaRatingParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "rating deleted"})
}

// GetMediaReadingProgress handles GET /api/media-items/:id/progress
func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	progress, err := mh.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusOK, map[string]interface{}{
				"current_page": 0,
				"total_pages":  nil,
			})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	resp := map[string]interface{}{
		"id":               progress.ID,
		"media_item_id":    progress.MediaItemID,
		"user_id":          progress.UserID,
		"current_page":     progress.CurrentPage,
		"total_pages":      progress.TotalPages,
		"last_read_at":     progress.LastReadAt,
		"percentage":       progress.Percentage,
		"character_offset": progress.CharacterOffset,
		"epubcfi":          progress.Epubcfi,
		"chapter":          progress.Chapter,
		"chapter_progress": progress.ChapterProgress,
		"format_group":     progress.FormatGroup,
		"total_characters": progress.TotalCharacters,
		"chapter_count":    progress.ChapterCount,
		"last_sync_device": progress.LastSyncDevice,
		"last_sync_source": progress.LastSyncSource,
		"last_sync_timestamp": progress.LastSyncTimestamp,
	}

	return c.JSON(http.StatusOK, resp)
}

// UpdateMediaReadingProgress handles PUT /api/media-items/:id/progress
func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req struct {
		CurrentPage     *int32   `json:"current_page"`
		TotalPages      *int32   `json:"total_pages"`
		Epubcfi         *string  `json:"epubcfi"`
		ContextText     *string  `json:"context_text"`
		Percentage      *float64 `json:"percentage"`
		Chapter         *int     `json:"chapter"`
		ChapterProgress *float64 `json:"chapter_progress"`
		CharacterOffset *int64   `json:"character_offset"`
		ReadingMode     *string  `json:"reading_mode"`
		ZoomLevel       *float64 `json:"zoom_level"`
		ScrollX         *float64 `json:"scroll_position_x"`
		ScrollY         *float64 `json:"scroll_position_y"`
	}
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}

	if mh.progressSvc != nil {
		saveReq := wsync.SaveProgressRequest{
			MediaItemID:     pgtype.UUID{Bytes: mediaUUID, Valid: true},
			UserID:          pgtype.UUID{Bytes: userUUID, Valid: true},
			Source:          "web",
			DeviceID:        pgtype.UUID{Bytes: userUUID, Valid: true},
			Percentage:      req.Percentage,
			Epubcfi:         req.Epubcfi,
			ContextText:     req.ContextText,
			CharacterOffset: req.CharacterOffset,
			Chapter:         req.Chapter,
			ChapterProgress: req.ChapterProgress,
			CurrentPage:     nil,
			TotalPages:      nil,
			ZoomLevel:       req.ZoomLevel,
			ScrollX:         req.ScrollX,
			ScrollY:         req.ScrollY,
			ReadingMode:     req.ReadingMode,
			DeviceType:      "web",
			DeviceName:      "Web",
			Broadcast:       true,
		}
		if req.CurrentPage != nil {
			cp := int(*req.CurrentPage)
			saveReq.CurrentPage = &cp
		}
		if req.TotalPages != nil {
			tp := int(*req.TotalPages)
			saveReq.TotalPages = &tp
		}

		if req.Percentage != nil && *req.Percentage < 0.005 {
			existing, err := mh.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
				MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
				UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
			})
			if err == nil && existing.Percentage.Valid && existing.Percentage.Float64 > 0.01 {
				return c.JSON(http.StatusOK, map[string]string{"status": "ignored"})
			}
		}

		result, err := mh.progressSvc.SaveProgress(c.Request().Context(), saveReq)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.JSON(http.StatusOK, result)
	}

	percentage := 0.0
	if req.Percentage != nil {
		percentage = *req.Percentage
	}
	epubcfi := ""
	if req.Epubcfi != nil {
		epubcfi = *req.Epubcfi
	}
	currentPage := int32(0)
	if req.CurrentPage != nil {
		currentPage = *req.CurrentPage
	}
	totalPages := int32(0)
	if req.TotalPages != nil {
		totalPages = *req.TotalPages
	}

	progress, err := mh.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
		MediaItemID:     pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:          pgtype.UUID{Bytes: userUUID, Valid: true},
		Percentage:      pgtype.Float8{Float64: percentage, Valid: true},
		CharacterOffset: pgtype.Int8{Valid: false},
		Epubcfi:         pgtype.Text{String: epubcfi, Valid: epubcfi != ""},
		Chapter:         pgtype.Int4{Valid: false},
		ChapterProgress: pgtype.Float8{Valid: false},
		ViewportX:       pgtype.Float8{Valid: false},
		ViewportY:       pgtype.Float8{Valid: false},
		ZoomLevel:       pgtype.Float8{Valid: false},
		ScrollPositionX: pgtype.Float8{Valid: false},
		ScrollPositionY: pgtype.Float8{Valid: false},
		PanelNumber:     pgtype.Int4{Valid: false},
		ReadingMode:     pgtype.Text{Valid: false},
		LastSyncDevice:  pgtype.Text{String: "web", Valid: true},
		LastSyncSource:  pgtype.Text{String: "web", Valid: true},
		CurrentPage:     pgtype.Int4{Int32: currentPage, Valid: true},
		TotalPages:      pgtype.Int4{Int32: totalPages, Valid: totalPages > 0},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, progress)
}

// DeleteMediaReadingProgress handles DELETE /api/media-items/:id/progress
func (mh *MediaHandler) DeleteMediaReadingProgress(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	err = mh.db.DeleteReadingProgress(c.Request().Context(), database.DeleteReadingProgressParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "reading progress deleted"})
}

// CreateMediaItem handles POST /api/media-items (admin only)
func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	if user.Role != "admin" {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
	}

	var req CreateMediaItemRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	if len(req.Tags) > 0 {
		req.Tags = utils.NormalizeTags(req.Tags)
	}

	if len(req.Contributors) > 0 {
		req.Contributors = utils.NormalizeContributors(req.Contributors)
	}

	tagsSearch := utils.NormalizeTagsSearch(req.Tags)
	contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)

	// Validate and normalize ISBN
	normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
	if err != nil && req.ISBN != "" {
		return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": "invalid ISBN format"})
	}

	// Use normalized ISBN if valid, otherwise empty string
	isbnValue := normalizedISBN
	if err != nil {
		isbnValue = ""
	}

	_, err = mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	// Validate library has folders before allowing media items
	hasFolders, err := mh.libraryService.HasFolders(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to validate library folders"})
	}
	if !hasFolders {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "Cannot add media items to a library with no folders. Please add at least one folder to the library first.",
		})
	}

	mediaItemID, err := mh.db.CreateMediaItem(c.Request().Context(), database.CreateMediaItemParams{
		LibraryID:          pgtype.UUID{Bytes: req.LibraryID, Valid: true},
		Title:              req.Title,
		Author:             pgtype.Text{String: req.Author, Valid: req.Author != ""},
		Isbn:               pgtype.Text{String: isbnValue, Valid: req.ISBN != ""},
		Description:        pgtype.Text{String: req.Description, Valid: req.Description != ""},
		FilePath:           req.FilePath,
		FileSize:           pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
		MimeType:           pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
		CoverImagePath:     pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
		Series:             pgtype.Text{String: req.Series, Valid: req.Series != ""},
		SeriesNumber:       pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
		Tags:               req.Tags,
		TagsSearch:         tagsSearch,
		Asin:               pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
		DatePublished:      pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
		Publisher:          pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
		Contributors:       req.Contributors,
		ContributorsSearch: contributorsSearch,
		AddedByAdminID:     user.ID,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaItemID.ID.Bytes, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusCreated, item)
}

// UpdateMediaItem handles PUT /api/media-items/:id (admin only)
func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	if user.Role != "admin" {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req UpdateMediaItemRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	if len(req.Tags) > 0 {
		req.Tags = utils.NormalizeTags(req.Tags)
	}

	if len(req.Contributors) > 0 {
		req.Contributors = utils.NormalizeContributors(req.Contributors)
	}

	tagsSearch := utils.NormalizeTagsSearch(req.Tags)
	contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)

	normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
	if err != nil && req.ISBN != "" {
		return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": "invalid ISBN format"})
	}

	isbnValue := normalizedISBN
	if err != nil {
		isbnValue = ""
	}

	if req.CoverAction == "" {
		req.CoverAction = "keep"
	}

	existing, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
	}

	coverPath := existing.CoverImagePath.String

	if req.CoverAction == "remove" {
		coverPath = ""
	} else if req.CoverAction == "upload" {
		file, err := c.FormFile("cover_file")
		if err == nil {
			savedPath, err := mh.saveCoverImage(*c, mediaUUID, file)
			if err != nil {
				return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to save cover image"})
			}
			coverPath = savedPath
		}
	}

	var alternateInfoBytes []byte
	if req.AlternateInfo != "" {
		alternateInfoBytes = []byte(req.AlternateInfo)
	}

	item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{
		ID:                 pgtype.UUID{Bytes: mediaUUID, Valid: true},
		Title:              req.Title,
		Author:             pgtype.Text{String: req.Author, Valid: req.Author != ""},
		Isbn:               pgtype.Text{String: isbnValue, Valid: req.ISBN != ""},
		Description:        pgtype.Text{String: req.Description, Valid: req.Description != ""},
		CoverImagePath:     pgtype.Text{String: coverPath, Valid: coverPath != ""},
		Series:             pgtype.Text{String: req.Series, Valid: req.Series != ""},
		SeriesNumber:       pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
		Tags:               req.Tags,
		TagsSearch:         tagsSearch,
		Asin:               pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
		DatePublished:      pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
		Publisher:          pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
		Contributors:       req.Contributors,
		ContributorsSearch: contributorsSearch,
		Language:           pgtype.Text{String: req.Language, Valid: req.Language != ""},
		Edition:            pgtype.Text{String: req.Edition, Valid: req.Edition != ""},
		PageCount:          pgtype.Int4{Int32: req.PageCount, Valid: req.PageCount > 0},
		Genre:              pgtype.Text{String: req.Genre, Valid: req.Genre != ""},
		CopyrightYear:      pgtype.Int4{Int32: req.CopyrightYear, Valid: req.CopyrightYear > 0},
		GoodreadsID:        pgtype.Text{String: req.GoodreadsID, Valid: req.GoodreadsID != ""},
		OpenlibraryID:      pgtype.Text{String: req.OpenlibraryID, Valid: req.OpenlibraryID != ""},
		GoogleBooksID:      pgtype.Text{String: req.GoogleBooksID, Valid: req.GoogleBooksID != ""},
		MangaType:          pgtype.Text{String: req.MangaType, Valid: req.MangaType != ""},
		ReadingDirection:   pgtype.Text{String: req.ReadingDirection, Valid: req.ReadingDirection != ""},
		SeriesCount:        pgtype.Int4{Int32: req.SeriesCount, Valid: req.SeriesCount > 0},
		Volume:             pgtype.Int4{Int32: req.Volume, Valid: req.Volume > 0},
		Imprint:            pgtype.Text{String: req.Imprint, Valid: req.Imprint != ""},
		AgeRating:          pgtype.Text{String: req.AgeRating, Valid: req.AgeRating != ""},
		WebUrl:             pgtype.Text{String: req.WebURL, Valid: req.WebURL != ""},
		MetadataNotes:      pgtype.Text{String: req.MetadataNotes, Valid: req.MetadataNotes != ""},
		CommunityRating:    pgtype.Float8{Float64: req.CommunityRating, Valid: req.CommunityRating > 0},
		StoryArc:           pgtype.Text{String: req.StoryArc, Valid: req.StoryArc != ""},
		IsBlackAndWhite:    pgtype.Bool{Bool: req.IsBlackAndWhite, Valid: req.IsBlackAndWhite},
		AlternateInfo:      alternateInfoBytes,
		ScanInformation:    pgtype.Text{String: req.ScanInformation, Valid: req.ScanInformation != ""},
		Summary:            pgtype.Text{String: req.Summary, Valid: req.Summary != ""},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	if c.Request().Header.Get("HX-Request") == "true" {
		c.Response().Header().Set("HX-Redirect", "/media/"+mediaID)
	}

	return c.JSON(http.StatusOK, item)
}

// DeleteMediaItem handles DELETE /api/media-items/:id (admin only)
func (mh *MediaHandler) DeleteMediaItem(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	if user.Role != "admin" {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	err = mh.db.DeleteMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.NoContent(http.StatusNoContent)
}

// GetMediaNotes handles GET /api/media-items/:id/notes
func (mh *MediaHandler) GetMediaNotes(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	notes, err := mh.db.GetMediaNotes(c.Request().Context(), database.GetMediaNotesParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, notes)
}

// CreateMediaNote handles POST /api/media-items/:id/notes
func (mh *MediaHandler) CreateMediaNote(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req CreateMediaNoteRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	var note database.MediaNotes
	if mh.annotationSvc != nil {
		result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
			MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
			UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
			Content:     req.Content,
			Position:    req.Position,
			Source:      "web",
			ModifiedAt:  time.Now(),
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		note = result.Note
	} else {
		var err error
		note, err = mh.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
			MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
			UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
			Content:     req.Content,
			Position:    pgtype.Text{String: req.Position, Valid: req.Position != ""},
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
	}

	return c.JSON(http.StatusCreated, note)
}

// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
func (mh *MediaHandler) GetMediaNote(c *echo.Context) error {
	noteID := c.Param("noteId")
	noteUUID, err := uuid.Parse(noteID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
	}

	note, err := mh.db.GetMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "note not found"})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, note)
}

// UpdateMediaNote handles PUT /api/media-items/:id/notes/:noteId
func (mh *MediaHandler) UpdateMediaNote(c *echo.Context) error {
	noteID := c.Param("noteId")
	noteUUID, err := uuid.Parse(noteID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
	}

	var req UpdateMediaNoteRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	note, err := mh.db.UpdateMediaNote(c.Request().Context(), database.UpdateMediaNoteParams{
		ID:       pgtype.UUID{Bytes: noteUUID, Valid: true},
		Content:  req.Content,
		Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, note)
}

// DeleteMediaNote handles DELETE /api/media-items/:id/notes/:noteId
func (mh *MediaHandler) DeleteMediaNote(c *echo.Context) error {
	noteID := c.Param("noteId")
	noteUUID, err := uuid.Parse(noteID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
	}

	if mh.annotationSvc != nil {
		err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
	} else {
		err = mh.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
	}
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.NoContent(http.StatusNoContent)
}

// GetMediaHighlights handles GET /api/media-items/:id/highlights
func (mh *MediaHandler) GetMediaHighlights(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	highlights, err := mh.db.GetMediaHighlights(c.Request().Context(), database.GetMediaHighlightsParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, highlights)
}

// CreateMediaHighlight handles POST /api/media-items/:id/highlights
func (mh *MediaHandler) CreateMediaHighlight(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req CreateMediaHighlightRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	var noteUUID pgtype.UUID
	if req.NoteID != "" {
		if noteID, err := uuid.Parse(req.NoteID); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
		} else {
			noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
		}
	}

	color := "#ffff00"
	if req.Color != "" {
		color = req.Color
	}

	pgMediaID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
	pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}

	if mh.annotationSvc != nil {
		result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
			MediaItemID:      pgMediaID,
			UserID:           pgUserID,
			SelectionText:    req.SelectionText,
			StartPosition:    req.StartPosition,
			EndPosition:      req.EndPosition,
			EpubcfiStart:     req.EpubcfiStart,
			EpubcfiEnd:       req.EpubcfiEnd,
			Color:            color,
			NoteText:         req.NoteText,
			PercentageStart:  req.PercentageStart,
			PercentageEnd:    req.PercentageEnd,
			ChapterReference: req.ChapterReference,
			Source:           "web",
			ModifiedAt:       time.Now(),
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.JSON(http.StatusCreated, result.Highlight)
	}

	highlight, err := mh.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
		MediaItemID:   pgMediaID,
		UserID:        pgUserID,
		SelectionText: req.SelectionText,
		StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
		EndPosition:   pgtype.Text{String: req.EndPosition, Valid: true},
		Color:         pgtype.Text{String: color, Valid: true},
		NoteID:        noteUUID,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusCreated, highlight)
}

// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
func (mh *MediaHandler) GetMediaHighlight(c *echo.Context) error {
	highlightID := c.Param("highlightId")
	highlightUUID, err := uuid.Parse(highlightID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
	}

	highlight, err := mh.db.GetMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, highlight)
}

// UpdateMediaHighlight handles PUT /api/media-items/:id/highlights/:highlightId
func (mh *MediaHandler) UpdateMediaHighlight(c *echo.Context) error {
	highlightID := c.Param("highlightId")
	highlightUUID, err := uuid.Parse(highlightID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
	}

	var req UpdateMediaHighlightRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	var noteUUID pgtype.UUID
	if req.NoteID != "" {
		if noteID, err := uuid.Parse(req.NoteID); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
		} else {
			noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
		}
	}

	color := "#ffff00"
	if req.Color != "" {
		color = req.Color
	}

	// Prefer the sync-aware path: the same selection text + CFI resolves to
	// the same dedup key, so this performs an LWW update of the existing row
	// (including note_text and CFI columns the plain query cannot touch).
	if mh.annotationSvc != nil {
		userID := c.Get("user_id").(string)
		userUUID, err := uuid.Parse(userID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
		}
		mediaID := c.Param("id")
		mediaUUID, err := uuid.Parse(mediaID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
		}
		result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
			MediaItemID:      pgtype.UUID{Bytes: mediaUUID, Valid: true},
			UserID:           pgtype.UUID{Bytes: userUUID, Valid: true},
			SelectionText:    req.SelectionText,
			StartPosition:    req.StartPosition,
			EndPosition:      req.EndPosition,
			EpubcfiStart:     req.EpubcfiStart,
			EpubcfiEnd:       req.EpubcfiEnd,
			Color:            color,
			NoteText:         req.NoteText,
			PercentageStart:  req.PercentageStart,
			PercentageEnd:    req.PercentageEnd,
			ChapterReference: req.ChapterReference,
			Source:           "web",
			ModifiedAt:       time.Now(),
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.JSON(http.StatusOK, result.Highlight)
	}

	highlight, err := mh.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
		ID:            pgtype.UUID{Bytes: highlightUUID, Valid: true},
		SelectionText: req.SelectionText,
		StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
		EndPosition:   pgtype.Text{String: req.EndPosition, Valid: true},
		Color:         pgtype.Text{String: color, Valid: true},
		NoteID:        noteUUID,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, highlight)
}

// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
func (mh *MediaHandler) DeleteMediaHighlight(c *echo.Context) error {
	highlightID := c.Param("highlightId")
	highlightUUID, err := uuid.Parse(highlightID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
	}

	pgHighlightID := pgtype.UUID{Bytes: highlightUUID, Valid: true}

	if mh.annotationSvc != nil {
		if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgHighlightID, "web"); err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.NoContent(http.StatusNoContent)
	}

	err = mh.db.DeleteMediaHighlight(c.Request().Context(), pgHighlightID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.NoContent(http.StatusNoContent)
}

// GetMediaBookmarks handles GET /api/media-items/:id/bookmarks
func (mh *MediaHandler) GetMediaBookmarks(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	bookmarks, err := mh.db.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
		MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:      pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, bookmarks)
}

// CreateMediaBookmark handles POST /api/media-items/:id/bookmarks
func (mh *MediaHandler) CreateMediaBookmark(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	mediaID := c.Param("id")
	mediaUUID, err := uuid.Parse(mediaID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
	}

	var req CreateMediaBookmarkRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	// The sync-aware path (dedup + LWW + tombstones) is preferred; fall back
	// to the plain query when the service isn't wired (e.g. some tests).
	if mh.annotationSvc != nil {
		result, err := mh.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
			MediaItemID:      pgtype.UUID{Bytes: mediaUUID, Valid: true},
			UserID:           pgtype.UUID{Bytes: userUUID, Valid: true},
			Title:            req.Title,
			Position:         req.Position,
			Notes:            req.Notes,
			PageNumber:       req.PageNumber,
			ChapterNumber:    req.ChapterNumber,
			CFIPosition:      req.CfiPosition,
			PercentageLoc:    req.Percentage,
			ChapterReference: req.ChapterReference,
			Source:           "web",
			ModifiedAt:       time.Now(),
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.JSON(http.StatusCreated, result.Bookmark)
	}

	bookmark, err := mh.db.CreateMediaBookmark(c.Request().Context(), database.CreateMediaBookmarkParams{
		MediaItemID:   pgtype.UUID{Bytes: mediaUUID, Valid: true},
		UserID:        pgtype.UUID{Bytes: userUUID, Valid: true},
		PageNumber:    pgtype.Int4{Int32: req.PageNumber, Valid: req.PageNumber > 0},
		ChapterNumber: pgtype.Int4{Int32: req.ChapterNumber, Valid: req.ChapterNumber > 0},
		CfiPosition:   pgtype.Text{String: req.CfiPosition, Valid: req.CfiPosition != ""},
		Title:         req.Title,
		Position:      pgtype.Text{String: req.Position, Valid: req.Position != ""},
		Notes:         pgtype.Text{String: req.Notes, Valid: req.Notes != ""},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}
	return c.JSON(http.StatusCreated, bookmark)
}

// UpdateMediaBookmark handles PUT /api/media-items/:id/bookmarks/:bookmarkId
func (mh *MediaHandler) UpdateMediaBookmark(c *echo.Context) error {
	userID := c.Get("user_id").(string)
	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
	}

	bookmarkID := c.Param("bookmarkId")
	bookmarkUUID, err := uuid.Parse(bookmarkID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid bookmark id"})
	}

	var req UpdateMediaBookmarkRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	bookmark, err := mh.db.UpdateMediaBookmark(c.Request().Context(), database.UpdateMediaBookmarkParams{
		ID:       pgtype.UUID{Bytes: bookmarkUUID, Valid: true},
		Title:    req.Title,
		Notes:    pgtype.Text{String: req.Notes, Valid: req.Notes != ""},
		Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
		UserID:   pgtype.UUID{Bytes: userUUID, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, bookmark)
}

// DeleteMediaBookmark handles DELETE /api/media-items/:id/bookmarks/:bookmarkId
func (mh *MediaHandler) DeleteMediaBookmark(c *echo.Context) error {
	bookmarkID := c.Param("bookmarkId")
	bookmarkUUID, err := uuid.Parse(bookmarkID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid bookmark id"})
	}

	pgBookmarkID := pgtype.UUID{Bytes: bookmarkUUID, Valid: true}

	if mh.annotationSvc != nil {
		if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgBookmarkID, "web"); err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		return c.NoContent(http.StatusNoContent)
	}

	if err := mh.db.DeleteMediaBookmark(c.Request().Context(), pgBookmarkID); err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.NoContent(http.StatusNoContent)
}

// SearchMediaItems handles GET /api/media-items/search
// Supports two modes:
// 1. Autocomplete: author=value, genre=value, etc. → returns field values for dropdowns
// 2. Search: q=value with optional filters → returns media items
func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error {
	// Safely get user from context
	userID, ok := c.Get("user").(database.Users)
	if !ok {
		return c.JSON(http.StatusUnauthorized, map[string]string{"error": "user not authenticated"})
	}

	// Detect autocomplete queries first (author=value, genre=value, etc.)
	if author := c.QueryParam("author"); author != "" {
		return mh.handleFieldValuesSearch(c, userID.ID, "author", author)
	}
	if genre := c.QueryParam("genre"); genre != "" {
		return mh.handleFieldValuesSearch(c, userID.ID, "genre", genre)
	}
	if tags := c.QueryParam("tags"); tags != "" {
		return mh.handleFieldValuesSearch(c, userID.ID, "tags", tags)
	}
	if series := c.QueryParam("series"); series != "" {
		return mh.handleFieldValuesSearch(c, userID.ID, "series", series)
	}
	if language := c.QueryParam("language"); language != "" {
		return mh.handleFieldValuesSearch(c, userID.ID, "language", language)
	}

	// Handle media item search with filters
	query := c.QueryParam("q")
	libraryID := c.QueryParam("library_id")

	// Validate library_id if provided
	var libUUID pgtype.UUID
	if libraryID != "" {
		lib, err := uuid.Parse(libraryID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
		}
		libUUID = pgtype.UUID{Bytes: lib, Valid: true}
	} else {
		libUUID = pgtype.UUID{Valid: false}
	}

	c.Logger().Info("Search parameters",
		"query", query,
		"library_id", libraryID,
		"libUUID_valid", libUUID.Valid,
		"libUUID_bytes", libUUID.Bytes)

	limit, _ := strconv.Atoi(c.QueryParam("limit"))
	if limit == 0 {
		limit = 50
	}
	offset, _ := strconv.Atoi(c.QueryParam("offset"))

	// Extract filter parameters
	authorFilter := c.QueryParam("author_filter")
	seriesFilter := c.QueryParam("series_filter")
	genreFilter := c.QueryParam("genre_filter")
	tagsFilter := c.QueryParam("tags_filter")
	languageFilter := c.QueryParam("language_filter")
	yearMin, _ := strconv.Atoi(c.QueryParam("year_min"))
	yearMax, _ := strconv.Atoi(c.QueryParam("year_max"))
	hasCover := false
	hasCoverValid := false
	if _, exists := c.QueryParams()["has_cover"]; exists {
		hasCover = c.QueryParam("has_cover") == "true"
		hasCoverValid = true
	}

	// Extract sort parameter
	sortParam := c.QueryParam("sort")
	if sortParam == "" {
		sortParam = "title ASC" // Default sort
	}

	// Build search params
	params := services.SearchParams{
		UserID:         userID.ID,
		LibraryID:      libUUID,
		SearchQuery:    query,
		AuthorFilter:   authorFilter,
		SeriesFilter:   seriesFilter,
		GenreFilter:    genreFilter,
		TagsFilter:     tagsFilter,
		LanguageFilter: languageFilter,
		YearMin:        yearMin,
		YearMax:        yearMax,
		HasCover:       pgtype.Bool{Bool: hasCover, Valid: hasCoverValid},
		Sort:           sortParam,
		Limit:          limit,
		Offset:         offset,
	}

	c.Logger().Info("SearchMediaItems called",
		"query", query,
		"library_id_provided", libraryID != "",
		"library_id_uuid", libUUID.Valid,
		"library_id_bytes", libUUID.Bytes)

	// Call shared search service
	results, _, err := mh.searchService.ExecuteSearch(c.Request().Context(), params)
	if err != nil {
		c.Logger().Error("search error", "error", err.Error())
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}
	if len(results) == 0 {
		return c.JSON(http.StatusNotFound, map[string]interface{}{
			"error":   "no results found",
			"query":   query,
			"results": []interface{}{},
		})
	}
	for i := range results {
		resolved := utils.ResolveMediaURL(results[i].LibraryID, results[i].CoverImagePath)
		results[i].CoverImagePath = pgtype.Text{String: resolved, Valid: resolved != ""}
	}
	return c.JSON(http.StatusOK, results)
}

// handleFieldValuesSearch handles autocomplete queries (author=value, genre=value, etc.)
// Returns distinct field values with counts and similarity scores for dropdown population
func (mh *MediaHandler) handleFieldValuesSearch(c *echo.Context, userID pgtype.UUID, fieldType, searchQuery string) error {
	libraryID := c.QueryParam("library_id")
	if libraryID == "" {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id is required"})
	}

	libUUID, err := uuid.Parse(libraryID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
	}

	limit, _ := strconv.Atoi(c.QueryParam("limit"))
	if limit == 0 {
		limit = 50
	}
	offset, _ := strconv.Atoi(c.QueryParam("offset"))

	params := services.FieldSearchParams{
		UserID:      userID,
		LibraryID:   pgtype.UUID{Bytes: libUUID, Valid: true},
		FieldType:   fieldType,
		SearchQuery: searchQuery,
		Limit:       limit,
		Offset:      offset,
	}

	results, err := mh.searchService.SearchFieldValues(c.Request().Context(), params)
	if err != nil {
		c.Logger().Error("field values search error", "error", err.Error(), "fieldType", fieldType)
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"results": results,
		"total":   len(results),
	})
}

// getFullFilePath returns the absolute filesystem path for a media item
// Uses LibraryService for resolution (one source of truth)
func (mh *MediaHandler) getFullFilePath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error) {
	if relativePath == "" {
		return "", fmt.Errorf("no file path")
	}

	// Check if already absolute (backward compatibility)
	if filepath.IsAbs(relativePath) {
		return relativePath, nil
	}

	// Use service for resolution (one source of truth)
	return mh.libraryService.ResolveMediaPath(ctx, libraryID, relativePath)
}

// ServeFile serves files (covers or books) via /uploads/library-{id}/path
// Requires JWT authentication
func (mh *MediaHandler) ServeFile(c *echo.Context) error {
	// URL format: /uploads/library-{libraryID}/{relativePath}
	// Get library ID directly from route parameter
	libraryIDStr := c.Param("id")
	libraryUUID, err := uuid.Parse(libraryIDStr)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library ID"})
	}

	// Get remaining path from URL
	rawPath := c.Param("*")
	relativePath, err := url.QueryUnescape(rawPath)
	if err != nil {
		relativePath = rawPath
	}

	if relativePath == "" {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid path"})
	}

	// Resolve using service
	fullPath, err := mh.getFullFilePath(c.Request().Context(), pgtype.UUID{Bytes: libraryUUID, Valid: true}, relativePath)
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
	}

	// Check if file exists
	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
	}

	ext := strings.ToLower(filepath.Ext(fullPath))
	contentType := services.MimeTypes[ext]
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	c.Response().Header().Set("Content-Type", contentType)
	c.Response().Header().Set("Cache-Control", "public, max-age=86400")
	http.ServeFile(c.Response(), c.Request(), fullPath)
	return nil
}

// numericToFloat converts pgtype.Numeric to float64, returning 0 if invalid
func numericToFloat(n pgtype.Float8) float64 {
	if n.Valid {
		return n.Float64
	}
	return 0
}

// jsonBytesToMap converts []byte JSONB to map[string]interface{}, returning nil if invalid
func jsonBytesToMap(b []byte) map[string]interface{} {
	if len(b) == 0 {
		return nil
	}
	var result map[string]interface{}
	if err := json.Unmarshal(b, &result); err != nil {
		return nil
	}
	return result
}

func (mh *MediaHandler) saveCoverImage(c echo.Context, mediaUUID uuid.UUID, file *multipart.FileHeader) (string, error) {
	src, err := file.Open()
	if err != nil {
		return "", fmt.Errorf("failed to open uploaded file: %w", err)
	}
	defer src.Close()

	imageData, err := io.ReadAll(src)
	if err != nil {
		return "", fmt.Errorf("failed to read uploaded file: %w", err)
	}

	if len(imageData) < 512 {
		return "", fmt.Errorf("file too small to be a valid image")
	}

	contentType := http.DetectContentType(imageData)
	if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/webp" {
		return "", fmt.Errorf("invalid image type: %s", contentType)
	}

	mediaItem, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
	if err != nil {
		return "", fmt.Errorf("media item not found: %w", err)
	}

	relativeFilePath := mediaItem.FilePath
	if relativeFilePath == "" {
		return "", fmt.Errorf("media item has no file path")
	}

	coverRelPath := relativeFilePath + ".cover.jpg"

	coverFullPath, err := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverRelPath)
	if err != nil {
		return "", fmt.Errorf("failed to resolve cover path: %w", err)
	}

	coverDir := filepath.Dir(coverFullPath)
	if err := os.MkdirAll(coverDir, 0755); err != nil {
		return "", fmt.Errorf("failed to create cover directory: %w", err)
	}

	if err := os.WriteFile(coverFullPath, imageData, 0644); err != nil {
		return "", fmt.Errorf("failed to write cover file: %w", err)
	}

	return coverRelPath, nil
}
