package handlers

import (
	"bookhoard/internal/database"
	"bookhoard/internal/services"
	wsync "bookhoard/internal/sync"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

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

type KOReaderHandler struct {
	db             *database.Queries
	connManager    *wsync.ConnectionManager
	queue          *wsync.SyncQueueProcessor
	progressSvc    *wsync.ProgressService
	annotationSvc  *wsync.AnnotationService
	libraryService LibraryPathResolver
	bookResolver   *services.BookResolver
}

type LibraryPathResolver interface {
	ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
}

func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor) *KOReaderHandler {
	return &KOReaderHandler{
		db:           db,
		connManager:  connManager,
		queue:        queue,
		bookResolver: services.NewBookResolver(db),
	}
}

func (h *KOReaderHandler) SetProgressService(svc *wsync.ProgressService) {
	h.progressSvc = svc
}

func (h *KOReaderHandler) SetAnnotationService(svc *wsync.AnnotationService) {
	h.annotationSvc = svc
}

func (h *KOReaderHandler) convertHighlightPositions(ctx context.Context, mediaItemID pgtype.UUID, pos0, pos1 string) (string, string) {
	if pos0 == "" || h.libraryService == nil {
		return "", ""
	}
	mediaItem, err := h.db.GetMediaItem(ctx, mediaItemID)
	if err != nil {
		return "", ""
	}
	epubPath, err := h.libraryService.ResolveMediaPath(ctx, mediaItem.LibraryID, mediaItem.FilePath)
	if err != nil || epubPath == "" {
		return "", ""
	}
	startLoc := wsync.ConvertToCanonical(wsync.LocatorSourceKOReader, pos0, 0, "", mediaItem.FormatGroup, epubPath, "")
	endLoc := wsync.ConvertToCanonical(wsync.LocatorSourceKOReader, pos1, 0, "", mediaItem.FormatGroup, epubPath, "")
	return startLoc.CFI, endLoc.CFI
}

func (h *KOReaderHandler) SetLibraryService(svc LibraryPathResolver) {
	h.libraryService = svc
}

type KOReaderProgressRequest struct {
	LibraryID *string                `json:"library_id,omitempty"`
	Books     []KOReaderBookProgress `json:"books" validate:"required"`
	SyncMode  string                 `json:"sync_mode,omitempty"`
}

type KOReaderBookProgress struct {
	UUID       string              `json:"uuid,omitempty"`
	SHA256     string              `json:"sha256,omitempty"`
	Title      string              `json:"title,omitempty"`
	Authors    []string            `json:"authors,omitempty"`
	Progress   float64             `json:"progress"`
	Percentage float64             `json:"percentage"`
	LastRead   string              `json:"last_read,omitempty"`
	FilePath   string              `json:"file_path,omitempty"`
	DeviceInfo KOReaderDeviceInfo  `json:"device_info,omitempty"`
	Bookmarks  []KOReaderBookmark  `json:"bookmarks,omitempty"`
	Highlights []KOReaderHighlight `json:"highlights,omitempty"`
	Notes      []KOReaderNote      `json:"notes,omitempty"`
	Chapter      *int                `json:"chapter,omitempty"`
	Character    *int64              `json:"character,omitempty"`
	Epubcfi      *string             `json:"epubcfi,omitempty"`
	ContextText  *string             `json:"context_text,omitempty"`
	Page         *int                `json:"page,omitempty"`
	TotalPages   *int                `json:"total_pages,omitempty"`
}

type KOReaderDeviceInfo struct {
	KOReaderVersion string `json:"koreader_version,omitempty"`
	DeviceModel     string `json:"device_model,omitempty"`
}

type KOReaderBookmark struct {
	Chapter    int      `json:"chapter,omitempty"`
	Datetime   string   `json:"datetime,omitempty"`
	Notes      string   `json:"notes,omitempty"`
	Pos0       string   `json:"pos0,omitempty"`
	Pos1       string   `json:"pos1,omitempty"`
	Page       int      `json:"page,omitempty"`
	Text       string   `json:"text,omitempty"`
	Type       string   `json:"type,omitempty"`
	Percentage *float64 `json:"percentage,omitempty"`
	BookSHA256 string   `json:"book_sha256,omitempty"`
}

type KOReaderHighlight struct {
	Chapter    int      `json:"chapter,omitempty"`
	Datetime   string   `json:"datetime,omitempty"`
	Notes      string   `json:"notes,omitempty"`
	Pos0       string   `json:"pos0,omitempty"`
	Pos1       string   `json:"pos1,omitempty"`
	Page       int      `json:"page,omitempty"`
	Text       string   `json:"text,omitempty"`
	Type       string   `json:"type,omitempty"`
	Color      string   `json:"color,omitempty"`
	Percentage *float64 `json:"percentage,omitempty"`
	BookSHA256 string   `json:"book_sha256,omitempty"`
}

type KOReaderNote struct {
	Chapter    int      `json:"chapter,omitempty"`
	Datetime   string   `json:"datetime,omitempty"`
	Notes      string   `json:"notes,omitempty"`
	Pos0       string   `json:"pos0,omitempty"`
	Pos1       string   `json:"pos1,omitempty"`
	Page       int      `json:"page,omitempty"`
	Text       string   `json:"text,omitempty"`
	Type       string   `json:"type,omitempty"`
	Percentage *float64 `json:"percentage,omitempty"`
	BookSHA256 string   `json:"book_sha256,omitempty"`
}

type KOReaderSyncResponse struct {
	SyncStatus    string                  `json:"sync_status"`
	BooksSynced   int                     `json:"books_synced"`
	BookResults   []KOReaderBookSyncResult `json:"book_results,omitempty"`
	Conflicts     []KOReaderConflict      `json:"conflicts,omitempty"`
	Timestamp     string                  `json:"timestamp"`
	DeviceUpdated bool                    `json:"device_updated"`
}

type KOReaderBookSyncResult struct {
	SHA256   string `json:"sha256"`
	BookUUID string `json:"book_uuid"`
	Synced   bool   `json:"synced"`
}

type KOReaderConflict struct {
	BookUUID       string  `json:"book_uuid"`
	ConflictType   string  `json:"conflict_type"`
	DeviceProgress float64 `json:"device_progress"`
	ServerProgress float64 `json:"server_progress"`
	Resolution     string  `json:"resolution"`
}

type KOReaderMetadata struct {
	UUID        string               `json:"uuid"`
	SHA256      string               `json:"sha256,omitempty"`
	Title       string               `json:"title"`
	Authors     []string             `json:"authors"`
	Progress    KOReaderProgressData `json:"progress"`
	Annotations KOReaderAnnotations  `json:"annotations"`
	LastSync    string               `json:"last_sync"`
}

type KOReaderProgressData struct {
	Percentage         float64  `json:"percentage"`
	Character          *int64   `json:"character,omitempty"`
	Epubcfi            *string  `json:"epubcfi,omitempty"`
	KoreaderXPointer   *string  `json:"koreader_xpointer,omitempty"`
	Chapter            *int     `json:"chapter,omitempty"`
	ChapterProgress    *float64 `json:"chapter_progress,omitempty"`
	Page               *int     `json:"page,omitempty"`
	TotalPages         *int     `json:"total_pages,omitempty"`
}

type KOReaderAnnotations struct {
	Highlights        []KOReaderHighlight `json:"highlights,omitempty"`
	Notes             []KOReaderNote      `json:"notes,omitempty"`
	Bookmarks         []KOReaderBookmark  `json:"bookmarks,omitempty"`
	DeletedHighlights []map[string]interface{} `json:"deleted_highlights,omitempty"`
	DeletedBookmarks  []map[string]interface{} `json:"deleted_bookmarks,omitempty"`
}

type KOReaderLibraryResponse struct {
	LibrarySync []KOReaderLibraryBook `json:"library_sync"`
	TotalBooks  int                   `json:"total_books"`
	LastSync    string                `json:"last_sync"`
}

type KOReaderLibraryBook struct {
	UUID           string  `json:"uuid"`
	SHA256         string  `json:"sha256,omitempty"`
	Title          string  `json:"title"`
	Author         string  `json:"author"`
	ContentType    string  `json:"content_type"`
	PercentRead    float64 `json:"percent_read"`
	PagesRemaining *int    `json:"pages_remaining,omitempty"`
	BookmarkCount  int     `json:"bookmark_count"`
	LastModified   string  `json:"last_modified"`
}

func (h *KOReaderHandler) SyncProgress(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes

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

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

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}

	syncMode := req.SyncMode
	if syncMode == "" {
		syncMode = "immediate"
	}

	if syncMode == "checkpoint" {
		return h.handleCheckpointSync(c, device, pgUserID, req)
	}

	booksSynced := 0
	conflicts := []KOReaderConflict{}
	bookResults := []KOReaderBookSyncResult{}

	for _, book := range req.Books {
		mediaItemID, _ := h.resolveBookToMediaItem(c, device.ID, pgUserID, book)
		if !mediaItemID.Valid {
			bookResults = append(bookResults, KOReaderBookSyncResult{
				SHA256: book.SHA256,
				Synced: false,
			})
			continue
		}

		err := h.updateProgressForBook(c, device.ID, pgUserID, mediaItemID, book)
		synced := err == nil
		if synced {
			booksSynced++
		}
		bookResults = append(bookResults, KOReaderBookSyncResult{
			SHA256:   book.SHA256,
			BookUUID: uuid.UUID(mediaItemID.Bytes).String(),
			Synced:   synced,
		})
	}

	_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to update device timestamp",
		})
	}

	if syncMode == "immediate" {
		return c.JSON(http.StatusAccepted, KOReaderSyncResponse{
			SyncStatus:    "accepted",
			BooksSynced:   booksSynced,
			BookResults:   bookResults,
			Conflicts:     conflicts,
			Timestamp:     time.Now().Format(time.RFC3339),
			DeviceUpdated: true,
		})
	}

	return c.JSON(http.StatusOK, KOReaderSyncResponse{
		SyncStatus:    "completed",
		BooksSynced:   booksSynced,
		BookResults:   bookResults,
		Conflicts:     conflicts,
		Timestamp:     time.Now().Format(time.RFC3339),
		DeviceUpdated: true,
	})
}

func (h *KOReaderHandler) resolveBookToMediaItem(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, book KOReaderBookProgress) (pgtype.UUID, float64) {
	ctx := c.Request().Context()

	// Priority 1: UUID provided (highest confidence - 1.0)
	if book.UUID != "" {
		mediaUUID, err := uuid.Parse(book.UUID)
		if err == nil {
			mediaItem, err := h.db.GetMediaItem(ctx, pgtype.UUID{Bytes: mediaUUID, Valid: true})
			if err == nil {
				// Create device file alias if FilePath is also provided
				if book.FilePath != "" {
					h.createDeviceFileAlias(c, deviceID, mediaItem.ID, book)
				}
				return mediaItem.ID, 1.0
			}
		}
	}

	// Priority 2: SHA-256 provided (medium confidence - 0.9)
	// Uses the shared BookResolver, which also checks per-format hashes
	// (media_item_formats) so a converted file (KEPUB/PDF) matches too.
	if book.SHA256 != "" && len(book.SHA256) == 64 {
		mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, book.SHA256)
		if err == nil {
			// Create device file alias if FilePath is provided
			if book.FilePath != "" {
				h.createDeviceFileAlias(c, deviceID, mediaItem.ID, book)
			}
			return mediaItem.ID, 0.9
		}
	}

	// Priority 3: FilePath provided (check existing alias or create new - confidence 0.7)
	if book.FilePath != "" {
		// Check if alias already exists
		alias, err := h.db.GetDeviceFileAlias(ctx, database.GetDeviceFileAliasParams{
			DeviceID: deviceID,
			FilePath: book.FilePath,
		})
		if err == nil {
			return alias.MediaItemID, alias.ConfidenceScore.Float64
		}

		// Try to find by file path (search any library)
		mediaItem, err := h.db.GetMediaItemByFilePathAnyLibrary(ctx, book.FilePath)
		if err == nil {
			// Create new alias
			confidence := 0.7
			h.createDeviceFileAlias(c, deviceID, mediaItem.ID, book)
			return mediaItem.ID, confidence
		}
	}

	// Priority 4: Title + Author match (lowest confidence - 0.5)
	if book.Title != "" {
		mediaItems, err := h.db.ListMediaItems(ctx, database.ListMediaItemsParams{
			Limit:  100,
			Offset: 0,
		})
		if err == nil {
			for _, mi := range mediaItems {
				if mi.Title == book.Title {
					// Check author match if provided
					if len(book.Authors) > 0 && mi.Author.Valid {
						if mi.Author.String == book.Authors[0] {
							// Create device file alias if FilePath is provided
							if book.FilePath != "" {
								h.createDeviceFileAlias(c, deviceID, mi.ID, book)
							}
							return mi.ID, 0.5
						}
					} else {
						// Title only match
						if book.FilePath != "" {
							h.createDeviceFileAlias(c, deviceID, mi.ID, book)
						}
						return mi.ID, 0.4
					}
				}
			}
		}
	}

	return pgtype.UUID{}, 0.0
}

func (h *KOReaderHandler) createDeviceFileAlias(c *echo.Context, deviceID pgtype.UUID, mediaItemID pgtype.UUID, book KOReaderBookProgress) {
	ctx := c.Request().Context()

	if book.FilePath == "" {
		return
	}

	sha256 := book.SHA256
	if sha256 == "" {
		// Try to get SHA256 from media item
		mediaItem, err := h.db.GetMediaItem(ctx, mediaItemID)
		if err == nil && mediaItem.FileSha256.Valid {
			sha256 = mediaItem.FileSha256.String
		}
	}

	confidence := 0.7
	// Check if alias already exists
	_, err := h.db.GetDeviceFileAlias(ctx, database.GetDeviceFileAliasParams{
		DeviceID: deviceID,
		FilePath: book.FilePath,
	})
	if err != nil {
		// Create new alias
		_, _ = h.db.CreateDeviceFileAlias(ctx, database.CreateDeviceFileAliasParams{
			MediaItemID:     mediaItemID,
			DeviceID:        deviceID,
			FilePath:        book.FilePath,
			FileSha256:      pgtype.Text{String: sha256, Valid: sha256 != ""},
			ConfidenceScore: pgtype.Float8{Float64: confidence, Valid: true},
		})
	}
}

func (h *KOReaderHandler) handleCheckpointSync(c *echo.Context, device database.Devices, userID pgtype.UUID, req KOReaderProgressRequest) error {
	booksEnqueued := 0
	bookResults := []KOReaderBookSyncResult{}

	for _, book := range req.Books {
		mediaItemID, _ := h.resolveBookToMediaItem(c, device.ID, userID, book)
		if !mediaItemID.Valid {
			bookResults = append(bookResults, KOReaderBookSyncResult{
				SHA256: book.SHA256,
				Synced: false,
			})
			continue
		}

		err := h.enqueueProgressForBook(c, device.ID, userID, mediaItemID, book)
		synced := err == nil
		if synced {
			booksEnqueued++
		}
		h.processBookAnnotations(c.Request().Context(), device.ID, userID, mediaItemID, book)
		bookResults = append(bookResults, KOReaderBookSyncResult{
			SHA256:   book.SHA256,
			BookUUID: uuid.UUID(mediaItemID.Bytes).String(),
			Synced:   synced,
		})
	}

	_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to update device timestamp",
		})
	}

	return c.JSON(http.StatusAccepted, KOReaderSyncResponse{
		SyncStatus:  "checkpoint_enqueued",
		BooksSynced: booksEnqueued,
		BookResults: bookResults,
		Timestamp:   time.Now().Format(time.RFC3339),
	})
}

func (h *KOReaderHandler) enqueueProgressForBook(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, mediaItemID pgtype.UUID, book KOReaderBookProgress) error {
	if h.queue == nil {
		return fmt.Errorf("sync queue not available")
	}

	update := &wsync.ProgressUpdate{
		DeviceID:    deviceID,
		MediaItemID: mediaItemID,
		UserID:      userID,
		Percentage:  book.Percentage,
		Epubcfi:     book.Epubcfi,
		ContextText: book.ContextText,
		Chapter:     book.Chapter,
		Character:   book.Character,
		Page:        book.Page,
		TotalPages:  book.TotalPages,
		Source:      "koreader",
		SyncMode:    "checkpoint",
	}

	return h.queue.EnqueueProgress(update)
}

func (h *KOReaderHandler) processBookAnnotations(ctx context.Context, deviceID, userID, mediaItemID pgtype.UUID, book KOReaderBookProgress) {
	if h.annotationSvc == nil {
		return
	}

	for _, hl := range book.Highlights {
		startPos := hl.Pos0
		endPos := hl.Pos1
		epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, startPos, endPos)

		pctStart := 0.0
		if hl.Percentage != nil {
			pctStart = *hl.Percentage
		}

		deviceData, _ := json.Marshal(map[string]interface{}{
			"datetime": hl.Datetime,
			"pos0":     hl.Pos0,
			"pos1":     hl.Pos1,
			"page":     hl.Page,
		})

		h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
			MediaItemID:      mediaItemID,
			UserID:           userID,
			SelectionText:    hl.Text,
			StartPosition:    startPos,
			EndPosition:      endPos,
			Color:            hl.Color,
			NoteText:         hl.Notes,
			PercentageStart:  pctStart,
			EpubcfiStart:     epubcfiStart,
			EpubcfiEnd:       epubcfiEnd,
			Source:           "koreader",
			DeviceSyncData:   deviceData,
		})
	}

	for _, note := range book.Notes {
		startPos := note.Pos0
		endPos := note.Pos1
		epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, startPos, endPos)

		pctStart := 0.0
		if note.Percentage != nil {
			pctStart = *note.Percentage
		}

		deviceData, _ := json.Marshal(map[string]interface{}{
			"datetime": note.Datetime,
			"pos0":     note.Pos0,
			"pos1":     note.Pos1,
			"page":     note.Page,
		})

		h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
			MediaItemID:      mediaItemID,
			UserID:           userID,
			SelectionText:    note.Text,
			StartPosition:    startPos,
			EndPosition:      endPos,
			NoteText:         note.Notes,
			PercentageStart:  pctStart,
			EpubcfiStart:     epubcfiStart,
			EpubcfiEnd:       epubcfiEnd,
			Source:           "koreader",
			DeviceSyncData:   deviceData,
		})
	}

	for _, bookmark := range book.Bookmarks {
		position := ""
		if bookmark.Pos0 != "" {
			position = bookmark.Pos0
		} else if bookmark.Page > 0 {
			position = fmt.Sprintf("page:%d", bookmark.Page)
		}

		deviceData, _ := json.Marshal(map[string]interface{}{
			"datetime": bookmark.Datetime,
			"pos0":     bookmark.Pos0,
			"page":     bookmark.Page,
		})

		h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
			MediaItemID:    mediaItemID,
			UserID:         userID,
			Title:          bookmark.Text,
			Position:       position,
			ChapterNumber:  int32(bookmark.Chapter),
			Source:         "koreader",
			DeviceSyncData: deviceData,
		})
	}
}

func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, mediaItemID pgtype.UUID, book KOReaderBookProgress) error {
	ctx := c.Request().Context()

	deviceInfo := book.DeviceInfo
	deviceModel := deviceInfo.DeviceModel
	if deviceModel == "" {
		deviceModel = "KOReader Device"
	}

	if h.progressSvc != nil {
		epubcfi := book.Epubcfi
		if epubcfi != nil && wsync.IsCREXPointer(*epubcfi) {
			log.Printf("Bookhoard: CRE→CFI attempting conversion for %s", *epubcfi)
			mediaItem, err := h.db.GetMediaItem(ctx, mediaItemID)
			if err != nil {
				log.Printf("Bookhoard: CRE→CFI failed to get media item: %v", err)
			} else if mediaItem.FormatGroup == string(wsync.FormatGroupFixedLayout) ||
				mediaItem.FormatGroup == string(wsync.FormatGroupComicArchive) {
				// Image-based fixed content (fixed-layout comic EPUBs, PDF,
				// comic archives) has no extractable text, so CRE→CFI conversion
				// cannot succeed. The page index (page/total_pages) is the
				// canonical locator. Keep the incoming xpointer for device-native
				// restore; the web reader restores by page.
				log.Printf("Bookhoard: CRE→CFI skipped for %s format", mediaItem.FormatGroup)
			} else if h.libraryService == nil {
				log.Printf("Bookhoard: CRE→CFI libraryService is nil, skipping conversion")
			} else {
				epubPath, resolveErr := h.libraryService.ResolveMediaPath(ctx, mediaItem.LibraryID, mediaItem.FilePath)
				if resolveErr != nil {
					log.Printf("Bookhoard: CRE→CFI failed to resolve media path: %v", resolveErr)
				} else if epubPath == "" {
					log.Printf("Bookhoard: CRE→CFI resolved empty epub path for %s", mediaItem.FilePath)
				} else {
					log.Printf("Bookhoard: CRE→CFI resolved epub path: %s", epubPath)
					converter := wsync.NewCFIConverter(epubPath)
					pct := 0.0
					if book.Percentage >= 0 {
						pct = book.Percentage
					}
					contextText := ""
					if book.ContextText != nil {
						contextText = *book.ContextText
					}
					result, convErr := converter.ConvertCREToStandard(*epubcfi, pct, contextText)
					if convErr != nil {
						log.Printf("Bookhoard: CRE→CFI conversion error: %v", convErr)
					} else if result != nil {
						if result.EPUBCFI != "" {
							convertedCFI := result.EPUBCFI
							epubcfi = &convertedCFI
							log.Printf("Bookhoard: CRE→CFI converted to epubcfi: %s", convertedCFI)
						} else if result.Href != "" {
							convertedHref := result.Href
							epubcfi = &convertedHref
							log.Printf("Bookhoard: CRE→CFI converted to href: %s", convertedHref)
						} else {
							log.Printf("Bookhoard: CRE→CFI conversion: %s precision for %s", result.Precision, *epubcfi)
						}
					}
				}
			}
		}

		saveReq := wsync.SaveProgressRequest{
			MediaItemID:     mediaItemID,
			UserID:          userID,
			Source:          "koreader",
			DeviceID:        deviceID,
			Percentage:      &book.Percentage,
			Epubcfi:         epubcfi,
			ContextText:     book.ContextText,
			Chapter:         book.Chapter,
			CharacterOffset: book.Character,
			CurrentPage:     book.Page,
			TotalPages:      book.TotalPages,
			DeviceType:      "koreader",
			DeviceName:      deviceModel,
			Broadcast:       true,
		}

		_, err := h.progressSvc.SaveProgress(ctx, saveReq)
		if err != nil {
			return err
		}
		h.processBookAnnotations(ctx, deviceID, userID, mediaItemID, book)
		return nil
	}

	_, err := h.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
		MediaItemID:     mediaItemID,
		UserID:          userID,
		Percentage:      pgtype.Float8{Float64: book.Percentage, Valid: true},
		Epubcfi:         textPtrToPgText(book.Epubcfi),
		Chapter:         intPtrToPgInt4(book.Chapter),
		ChapterProgress: pgtype.Float8{Float64: book.Percentage, Valid: true},
		CharacterOffset: int64PtrToPgInt8(book.Character),
		CurrentPage:     intPtrToPgInt4(book.Page),
		TotalPages:      intPtrToPgInt4(book.TotalPages),
		LastSyncDevice:  pgtype.Text{String: "koreader", Valid: true},
		LastSyncSource:  pgtype.Text{String: "koreader", Valid: true},
		ViewportY:       pgtype.Float8{},
		ScrollPositionX: pgtype.Float8{},
		ScrollPositionY: pgtype.Float8{},
		PanelNumber:     pgtype.Int4{},
		ReadingMode:     pgtype.Text{},
		ZoomLevel:       pgtype.Float8{},
	})
	if err != nil {
		return err
	}

	h.connManager.BroadcastProgressUpdate(
		mediaItemID.Bytes,
		book.Percentage,
		wsync.SourceDevice{
			ID:   uuid.UUID(deviceID.Bytes).String(),
			Name: deviceModel,
			Type: "koreader",
		},
	)

	h.processBookAnnotations(ctx, deviceID, userID, mediaItemID, book)

	return nil
}

func textPtrToPgText(s *string) pgtype.Text {
	if s != nil {
		return pgtype.Text{String: *s, Valid: true}
	}
	return pgtype.Text{}
}

func intPtrToPgInt4(i *int) pgtype.Int4 {
	if i != nil {
		return pgtype.Int4{Int32: int32(*i), Valid: true}
	}
	return pgtype.Int4{}
}

func int64PtrToPgInt8(i *int64) pgtype.Int8 {
	if i != nil {
		return pgtype.Int8{Int64: *i, Valid: true}
	}
	return pgtype.Int8{}
}

func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes

	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}
	pgUserID := pgtype.UUID{Bytes: userID, 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",
		})
	}

	progress, err := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
		MediaItemID: pgBookUUID,
		UserID:      pgUserID,
	})

	if err != nil {
		return c.JSON(http.StatusOK, map[string]interface{}{
			"uuid":        bookUUID,
			"title":       mediaItem.Title,
			"author":      mediaItem.Author,
			"progress":    nil,
			"annotations": map[string][]interface{}{},
		})
	}

	progressData := KOReaderProgressData{
		Percentage: progress.Percentage.Float64,
	}

	// CFI/xpointer are meaningless for image-based fixed-layout content; the
	// page index is the canonical locator. Only return them for reflowable docs.
	formatGroup := wsync.FormatGroup(mediaItem.FormatGroup)
	isFixed := formatGroup == wsync.FormatGroupFixedLayout ||
		formatGroup == wsync.FormatGroupComicArchive

	if !isFixed {
		if progress.Epubcfi.Valid {
			progressData.Epubcfi = &progress.Epubcfi.String
		}
		if progress.Epubcfi.Valid && wsync.IsStandardEPUBCFI(progress.Epubcfi.String) {
			h.convertCFIToXPointer(c, mediaItem, progress, &progressData)
		}
	}
	if progress.Chapter.Valid {
		progress := int(progress.Chapter.Int32)
		progressData.Chapter = &progress
	}
	if progress.ChapterProgress.Valid {
		progress := progress.ChapterProgress.Float64
		progressData.ChapterProgress = &progress
	}
	if progress.CharacterOffset.Valid {
		progress := progress.CharacterOffset.Int64
		progressData.Character = &progress
	}
	if progress.CurrentPage.Valid {
		progress := int(progress.CurrentPage.Int32)
		progressData.Page = &progress
	}
	if progress.TotalPages.Valid {
		progress := int(progress.TotalPages.Int32)
		progressData.TotalPages = &progress
	}

	annotations, err := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
		MediaItemID: pgBookUUID,
		UserID:      pgUserID,
	})

	annotationsResponse := KOReaderAnnotations{
		Highlights: []KOReaderHighlight{},
		Notes:      []KOReaderNote{},
		Bookmarks:  []KOReaderBookmark{},
	}

	for _, ann := range annotations {
		if ann.AnnotationType == "highlight" {
			pos0 := ann.StartPosition.String
			pos1 := ann.EndPosition.String
			if ann.EpubcfiStart.Valid && ann.EpubcfiStart.String != "" {
				if converted := h.reverseConvertCFI(c, mediaItem, ann.EpubcfiStart.String); converted != "" {
					pos0 = converted
				}
			}
			if ann.EpubcfiEnd.Valid && ann.EpubcfiEnd.String != "" {
				if converted := h.reverseConvertCFI(c, mediaItem, ann.EpubcfiEnd.String); converted != "" {
					pos1 = converted
				}
			}
			highlight := KOReaderHighlight{
				Text:     ann.SelectionText,
				Pos0:     pos0,
				Pos1:     pos1,
				Color:    ann.Color.String,
				Datetime: ann.CreatedAt.Time.Format(time.RFC3339),
			}
			if ann.NoteText.Valid && ann.NoteText.String != "" {
				highlight.Notes = ann.NoteText.String
			}
			annotationsResponse.Highlights = append(annotationsResponse.Highlights, highlight)
		} else if ann.AnnotationType == "note" {
			annotationsResponse.Notes = append(annotationsResponse.Notes, KOReaderNote{
				Text:     ann.SelectionText,
				Pos0:     ann.StartPosition.String,
				Datetime: ann.CreatedAt.Time.Format(time.RFC3339),
			})
		}
	}

	bookmarks, _ := h.db.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
		MediaItemID: pgBookUUID,
		UserID:      pgUserID,
	})
	for _, bm := range bookmarks {
		pos0 := bm.Position.String
		if pos0 == "" && bm.CfiPosition.Valid {
			pos0 = bm.CfiPosition.String
		}
		koreaderBookmark := KOReaderBookmark{
			Text:     bm.Title,
			Pos0:     pos0,
			Pos1:     pos0,
			Datetime: bm.CreatedAt.Time.Format(time.RFC3339),
		}
		if bm.Notes.Valid && bm.Notes.String != "" {
			koreaderBookmark.Notes = bm.Notes.String
		}
		if bm.ChapterNumber.Valid {
			koreaderBookmark.Chapter = int(bm.ChapterNumber.Int32)
		}
		annotationsResponse.Bookmarks = append(annotationsResponse.Bookmarks, koreaderBookmark)
	}

	cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
	tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
		MediaItemID: pgBookUUID,
		UserID:      pgUserID,
		DeletedAt:   cutoff,
	})
	for _, ts := range tombstones {
		var dd map[string]interface{}
		if len(ts.DeviceSyncData) > 0 {
			json.Unmarshal(ts.DeviceSyncData, &dd)
		}
		if dd == nil {
			dd = map[string]interface{}{}
		}
		dd["dedup_key"] = ts.DedupKey.String
		if ts.AnnotationType == "highlight" {
			annotationsResponse.DeletedHighlights = append(annotationsResponse.DeletedHighlights, dd)
		} else if ts.AnnotationType == "bookmark" {
			annotationsResponse.DeletedBookmarks = append(annotationsResponse.DeletedBookmarks, dd)
		}
	}

	lastSync := "never"
	if progress.LastSyncTimestamp.Valid {
		lastSync = progress.LastSyncTimestamp.Time.Format(time.RFC3339)
	}

	metadata := KOReaderMetadata{
		UUID:        bookUUID.String(),
		SHA256:      mediaItem.FileSha256.String,
		Title:       mediaItem.Title,
		Authors:     []string{mediaItem.Author.String},
		Progress:    progressData,
		Annotations: annotationsResponse,
		LastSync:    lastSync,
	}

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

func (h *KOReaderHandler) convertCFIToXPointer(c *echo.Context, mediaItem database.MediaItems, progress database.GetUniversalProgressRow, progressData *KOReaderProgressData) {
	if h.libraryService == nil {
		log.Printf("Bookhoard: CFI→CRE libraryService is nil, skipping reverse conversion")
		return
	}

	epubPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
	if err != nil {
		log.Printf("Bookhoard: CFI→CRE failed to resolve media path: %v", err)
		return
	}
	if epubPath == "" {
		log.Printf("Bookhoard: CFI→CRE resolved empty epub path for %s", mediaItem.FilePath)
		return
	}

	converter := wsync.NewCFIConverter(epubPath)
	contextText := ""
	if progress.ContextText.Valid {
		contextText = progress.ContextText.String
	}
	pct := progress.Percentage.Float64

	result, err := converter.ConvertStandardToCRE(progress.Epubcfi.String, pct, contextText)
	if err != nil {
		log.Printf("Bookhoard: CFI→CRE conversion error: %v", err)
		return
	}
	if result != nil && result.XPointer != "" {
		progressData.KoreaderXPointer = &result.XPointer
		log.Printf("Bookhoard: CFI→CRE converted to XPointer: %s", result.XPointer)
	}
}

func (h *KOReaderHandler) reverseConvertCFI(c *echo.Context, mediaItem database.MediaItems, epubcfi string) string {
	if h.libraryService == nil || epubcfi == "" {
		return ""
	}
	epubPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
	if err != nil || epubPath == "" {
		return ""
	}
	loc := wsync.ConvertFromCanonical(wsync.LocatorSourceKOReader, epubcfi, 0, "", mediaItem.FormatGroup, epubPath, "")
	if loc.Position != "" && loc.Position != epubcfi {
		return loc.Position
	}
	return ""
}

func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}

	mediaItems, err := h.db.GetUserMediaItemsForSync(c.Request().Context(), pgUserID)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{
			"error": "failed to fetch library",
		})
	}

	libraryBooks := []KOReaderLibraryBook{}

	for _, item := range mediaItems {
		pgItemUUID := pgtype.UUID{Bytes: item.ID.Bytes, Valid: true}

		progress, err := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
			MediaItemID: pgItemUUID,
			UserID:      pgUserID,
		})

		percentRead := 0.0
		var pagesRemaining *int
		bookmarkCount := 0
		lastModified := time.Now().Format(time.RFC3339)

		if err == nil {
			percentRead = progress.Percentage.Float64
			if progress.TotalPages.Valid && progress.CurrentPage.Valid {
				pages := int(progress.TotalPages.Int32 - progress.CurrentPage.Int32)
				pagesRemaining = &pages
			}
			if progress.LastReadAt.Valid {
				lastModified = progress.LastReadAt.Time.Format(time.RFC3339)
			}
		}

		annotations, _ := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
			MediaItemID: pgItemUUID,
			UserID:      pgUserID,
		})
		bookmarkCount = len(annotations)

		libraryBooks = append(libraryBooks, KOReaderLibraryBook{
			UUID:           uuid.UUID(item.ID.Bytes).String(),
			SHA256:         item.FileSha256.String,
			Title:          item.Title,
			Author:         item.Author.String,
			ContentType:    "6",
			PercentRead:    percentRead * 100,
			PagesRemaining: pagesRemaining,
			BookmarkCount:  bookmarkCount,
			LastModified:   lastModified,
		})
	}

	return c.JSON(http.StatusOK, KOReaderLibraryResponse{
		LibrarySync: libraryBooks,
		TotalBooks:  len(libraryBooks),
		LastSync:    time.Now().Format(time.RFC3339),
	})
}

func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes

	var req struct {
		BookUUID   string              `json:"book_uuid,omitempty"`
		BookSHA256 string              `json:"book_sha256,omitempty"`
		Bookmarks  []KOReaderBookmark  `json:"bookmarks"`
		Notes      []KOReaderNote      `json:"notes"`
		Highlights []KOReaderHighlight `json:"highlights"`
	}

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

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

	ctx := c.Request().Context()
	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}

	// Resolve media item ID using priority matching
	var pgBookUUID pgtype.UUID
	if req.BookUUID != "" {
		// Use UUID directly
		bookUUID, err := uuid.Parse(req.BookUUID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "invalid book UUID",
			})
		}
		pgBookUUID = pgtype.UUID{Bytes: bookUUID, Valid: true}
	} else if req.BookSHA256 != "" && len(req.BookSHA256) == 64 {
		// Use SHA-256 to find book (format-aware: also checks media_item_formats)
		mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, req.BookSHA256)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{
				"error": "book not found by SHA-256",
			})
		}
		pgBookUUID = mediaItem.ID
	} else {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "either book_uuid or book_sha256 is required",
		})
	}

	bookmarksSynced := 0
	notesSynced := 0
	highlightsSynced := 0

	for _, bookmark := range req.Bookmarks {
		mediaItemID := pgBookUUID

		// If bookmark has its own SHA-256, use it for matching
		if bookmark.BookSHA256 != "" && len(bookmark.BookSHA256) == 64 {
			mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, bookmark.BookSHA256)
			if err == nil {
				mediaItemID = mediaItem.ID
			}
		}

		position := ""
		if bookmark.Pos0 != "" {
			position = bookmark.Pos0
		} else if bookmark.Page > 0 {
			position = fmt.Sprintf("page:%d", bookmark.Page)
		}

		if h.annotationSvc != nil {
			deviceData, _ := json.Marshal(map[string]interface{}{
				"datetime": bookmark.Datetime,
				"pos0":     bookmark.Pos0,
				"page":     bookmark.Page,
			})

			result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
				MediaItemID:    mediaItemID,
				UserID:         pgUserID,
				Title:          bookmark.Text,
				Position:       position,
				ChapterNumber:  int32(bookmark.Chapter),
				Source:         "koreader",
				DeviceSyncData: deviceData,
			})
			if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
				bookmarksSynced++
			}
		} else {
			_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
				MediaItemID: mediaItemID,
				UserID:      pgUserID,
				Content:     bookmark.Text,
				Position:    pgtype.Text{String: position, Valid: position != ""},
			})

			if err == nil {
				bookmarksSynced++
			}
		}
	}

	for _, note := range req.Notes {
		mediaItemID := pgBookUUID

		// If note has its own SHA-256, use it for matching
		if note.BookSHA256 != "" && len(note.BookSHA256) == 64 {
			mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, note.BookSHA256)
			if err == nil {
				mediaItemID = mediaItem.ID
			}
		}

		position := ""
		if note.Pos0 != "" {
			position = note.Pos0
		} else if note.Page > 0 {
			position = fmt.Sprintf("page:%d", note.Page)
		}

		if h.annotationSvc != nil {
			deviceData, _ := json.Marshal(map[string]interface{}{
				"datetime": note.Datetime,
				"pos0":     note.Pos0,
				"page":     note.Page,
			})

			result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
				MediaItemID:    mediaItemID,
				UserID:         pgUserID,
				Content:        note.Notes,
				Position:       position,
				Source:         "koreader",
				DeviceSyncData: deviceData,
			})
			if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
				notesSynced++
			}
		} else {
			_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
				MediaItemID: mediaItemID,
				UserID:      pgUserID,
				Content:     note.Notes,
				Position:    pgtype.Text{String: position, Valid: position != ""},
			})

			if err == nil {
				notesSynced++
			}
		}
	}

	for _, highlight := range req.Highlights {
		mediaItemID := pgBookUUID

		// If highlight has its own SHA-256, use it for matching
		if highlight.BookSHA256 != "" && len(highlight.BookSHA256) == 64 {
			mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx, highlight.BookSHA256)
			if err == nil {
				mediaItemID = mediaItem.ID
			}
		}

		startPos := highlight.Pos0
		endPos := highlight.Pos1
		if startPos == "" && highlight.Page > 0 {
			startPos = fmt.Sprintf("page:%d", highlight.Page)
			endPos = startPos
		}

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

		if h.annotationSvc != nil {
			epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, highlight.Pos0, highlight.Pos1)

			pctStart := 0.0
			if highlight.Percentage != nil {
				pctStart = *highlight.Percentage
			}

			deviceData, _ := json.Marshal(map[string]interface{}{
				"datetime": highlight.Datetime,
				"pos0":     highlight.Pos0,
				"pos1":     highlight.Pos1,
				"page":     highlight.Page,
			})

			result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
				MediaItemID:     mediaItemID,
				UserID:          pgUserID,
				SelectionText:   highlight.Text,
				StartPosition:   startPos,
				EndPosition:     endPos,
				Color:           color,
				NoteText:        highlight.Notes,
				PercentageStart: pctStart,
				EpubcfiStart:    epubcfiStart,
				EpubcfiEnd:      epubcfiEnd,
				Source:          "koreader",
				DeviceSyncData:  deviceData,
			})
			if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
				highlightsSynced++
			}
		} else {
			_, err := h.db.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
				MediaItemID:   mediaItemID,
				UserID:        pgUserID,
				SelectionText: highlight.Text,
				StartPosition: pgtype.Text{String: startPos, Valid: startPos != ""},
				EndPosition:   pgtype.Text{String: endPos, Valid: endPos != ""},
				Color:         pgtype.Text{String: color, Valid: true},
			})

			if err == nil {
				highlightsSynced++
			}
		}
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"sync_status":       "completed",
		"bookmarks_synced":  bookmarksSynced,
		"notes_synced":      notesSynced,
		"highlights_synced": highlightsSynced,
		"total_synced":      bookmarksSynced + notesSynced + highlightsSynced,
		"timestamp":         time.Now().Format(time.RFC3339),
	})
}
