package handlers

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

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

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

func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager) *KoboHandler {
	return &KoboHandler{db: db, connManager: connManager, bookResolver: services.NewBookResolver(db)}
}

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

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

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

// mapContentIdToBookhoardUUID maps Kobo ContentId to Bookhoard UUID with multiple fallback strategies
// Enhanced Kobo Sync - ContentId Mapping Logic
func (h *KoboHandler) mapContentIdToBookhoardUUID(ctx *echo.Context, contentId string, deviceID uuid.UUID) (uuid.UUID, error, string) {
	// Step 1: Try direct ContentId lookup in device_catalogs table
	catalog, err := h.db.GetDeviceCatalogByKoboContentId(ctx.Request().Context(), contentId)
	if err == nil && catalog.ID.Valid {
		// Found! Use canonical Bookhoard UUID
		return catalog.BookhoardUuid.Bytes, nil, "catalog_match"
	}

	// Step 2: ContentId not found - check if it looks like a SHA-256 hash
	if len(contentId) == 64 && looksLikeSHA256(contentId) {
		// Try to find media item by SHA-256 (format-aware: also checks
		// media_item_formats, so a converted/alternate format hash matches).
		mediaItem, _, err := h.bookResolver.ResolveBySHA256(ctx.Request().Context(), contentId)
		if err == nil {
			// Found by SHA-256! Create device catalog entry for future lookups
			_, _ = h.db.CreateDeviceCatalog(ctx.Request().Context(), database.CreateDeviceCatalogParams{
				DeviceID:       pgtype.UUID{Bytes: deviceID, Valid: true},
				MediaItemID:    pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true},
				BookhoardUuid:  pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true},
				KoboContentID:  contentId,
				ContentIDType:  pgtype.Text{String: "sha256", Valid: true},
				Available:      pgtype.Bool{Bool: true, Valid: true},
				DeliveryDate:   pgtype.Timestamptz{Time: time.Now(), Valid: true},
				DeliveryMethod: pgtype.Text{String: "sync", Valid: true},
			})
			return mediaItem.ID.Bytes, nil, "sha256_match"
		}
	}

	// Step 3: Try to parse as UUID directly
	if parsedUUID, err := uuid.Parse(contentId); err == nil {
		// Check if this UUID exists in media_items
		mediaItem, err := h.db.GetMediaItem(ctx.Request().Context(), pgtype.UUID{Bytes: parsedUUID, Valid: true})
		if err == nil {
			// Create device catalog entry
			_, _ = h.db.CreateDeviceCatalog(ctx.Request().Context(), database.CreateDeviceCatalogParams{
				DeviceID:       pgtype.UUID{Bytes: deviceID, Valid: true},
				MediaItemID:    pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true},
				BookhoardUuid:  pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true},
				KoboContentID:  contentId,
				ContentIDType:  pgtype.Text{String: "bookhoard_uuid", Valid: true},
				Available:      pgtype.Bool{Bool: true, Valid: true},
				DeliveryDate:   pgtype.Timestamptz{Time: time.Now(), Valid: true},
				DeliveryMethod: pgtype.Text{String: "sync", Valid: true},
			})
			return parsedUUID, nil, "uuid_match"
		}
	}

	// Step 4: Not found - return error for unlinked book
	return uuid.Nil, fmt.Errorf("unlinked book: ContentId %s not found", contentId), "unlinked"
}

// mapBookhoardUUIDToKoboContentId maps Bookhoard UUID to Kobo ContentId
// Creates new entry in device_catalogs if not exists
func (h *KoboHandler) mapBookhoardUUIDToKoboContentId(c *echo.Context, bookhoardUUID uuid.UUID, deviceID uuid.UUID) (string, error) {
	// Check if catalog entry already exists
	catalog, err := h.db.GetDeviceCatalogByBookhoardUUID(c.Request().Context(), database.GetDeviceCatalogByBookhoardUUIDParams{
		DeviceID:      pgtype.UUID{Bytes: deviceID, Valid: true},
		BookhoardUuid: pgtype.UUID{Bytes: bookhoardUUID, Valid: true},
	})
	if err == nil && catalog.ID.Valid {
		return catalog.KoboContentID, nil
	}

	// Get media item to check for existing Kobo content ID
	mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: bookhoardUUID, Valid: true})
	if err != nil {
		return "", err
	}

	// Generate Kobo ContentId based on priority:
	// 1. Use existing kobo_content_id from media_items
	// 2. Use existing entitlement_id from media_items
	// 3. Generate new "kobo_" prefixed UUID
	var koboContentId string
	contentIdType := "bookhoard_generated"

	if mediaItem.KoboContentID.Valid && mediaItem.KoboContentID.String != "" {
		koboContentId = mediaItem.KoboContentID.String
		contentIdType = "kobo_metadata"
	} else if mediaItem.EntitlementID.Valid && mediaItem.EntitlementID.String != "" {
		koboContentId = mediaItem.EntitlementID.String
		contentIdType = "entitlement_id"
	} else {
		koboContentId = "kobo_" + uuid.New().String()
		contentIdType = "kobo_generated"
	}

	// Create device catalog entry
	_, err = h.db.CreateDeviceCatalog(c.Request().Context(), database.CreateDeviceCatalogParams{
		DeviceID:       pgtype.UUID{Bytes: deviceID, Valid: true},
		MediaItemID:    pgtype.UUID{Bytes: bookhoardUUID, Valid: true},
		BookhoardUuid:  pgtype.UUID{Bytes: bookhoardUUID, Valid: true},
		KoboContentID:  koboContentId,
		ContentIDType:  pgtype.Text{String: contentIdType, Valid: true},
		Available:      pgtype.Bool{Bool: true, Valid: true},
		DeliveryDate:   pgtype.Timestamptz{Time: time.Now(), Valid: true},
		DeliveryMethod: pgtype.Text{String: "opds", Valid: true},
	})

	if err != nil {
		return "", err
	}

	return koboContentId, nil
}

// getCollectionMetadataForBook retrieves collection names for a book
func (h *KoboHandler) getCollectionMetadataForBook(c *echo.Context, bookhoardUUID uuid.UUID, deviceID uuid.UUID) ([]string, error) {
	device := c.Get("device").(database.Devices)
	pgDeviceID := pgtype.UUID{Bytes: device.ID.Bytes, Valid: true}

	// Get collections for this book
	collections, err := h.db.GetCollectionsForBook(c.Request().Context(), pgtype.UUID{Bytes: bookhoardUUID, Valid: true})
	if err != nil {
		return []string{}, nil
	}

	var collectionNames []string

	// For each collection, check if there's a device-specific shelf mapping
	for _, collection := range collections {
		mapping, err := h.db.GetDeviceShelfMapping(c.Request().Context(), database.GetDeviceShelfMappingParams{
			DeviceID:     pgDeviceID,
			CollectionID: pgtype.UUID{Bytes: collection.ID.Bytes, Valid: true},
		})
		if err == nil && mapping.ID.Valid && mapping.DeviceShelfName.Valid {
			// Use device-specific shelf name
			collectionNames = append(collectionNames, mapping.DeviceShelfName.String)
		} else if collection.Name != "" {
			// Fall back to collection name
			collectionNames = append(collectionNames, collection.Name)
		}
	}

	return collectionNames, nil
}

// looksLikeSHA256 checks if a string looks like a SHA-256 hash
func looksLikeSHA256(s string) bool {
	if len(s) != 64 {
		return false
	}
	matched, _ := regexp.MatchString("^[0-9a-fA-F]{64}$", s)
	return matched
}

type KoboDeviceInfo struct {
	DeviceID     string `json:"DeviceId"`
	Model        string `json:"Model"`
	SerialNumber string `json:"SerialNumber"`
	Firmware     string `json:"Firmware,omitempty"`
}

type KoboReadingSync struct {
	ContentId            string  `json:"ContentId"`
	PercentRead          float64 `json:"PercentRead"`
	EntitlementId        string  `json:"EntitlementId"`
	RemainingTimeMinutes int     `json:"RemainingTimeMinutes"`
	FirstReadTime        string  `json:"FirstReadTime,omitempty"`
	LastModified         string  `json:"LastModified"`
	ContentType          string  `json:"ContentType,omitempty"`
}

type KoboBookmarkSync struct {
	BookmarkId    string `json:"BookmarkId"`
	ContentId     string `json:"ContentId"`
	BookmarkText  string `json:"BookmarkText"`
	BookmarkType  string `json:"BookmarkType"`
	BookmarkTitle string `json:"BookmarkTitle"`
	DateCreated   string `json:"DateCreated"`
	Chapter       int    `json:"Chapter,omitempty"`
	Hidden        bool   `json:"Hidden,omitempty"`
}

type KoboMarkupRequest struct {
	ReadingSync  []KoboReadingSync  `json:"ReadingSync"`
	BookmarkSync []KoboBookmarkSync `json:"BookmarkSync,omitempty"`
}

type KoboLibraryBook struct {
	ContentId      string   `json:"ContentId"`
	ContentType    string   `json:"ContentType"`
	Title          string   `json:"Title"`
	Author         string   `json:"Author"`
	PercentRead    float64  `json:"PercentRead"`
	PagesRemaining *int     `json:"PagesRemaining,omitempty"`
	BookmarkCount  int      `json:"BookmarkCount"`
	LastModified   string   `json:"LastModified"`
	EntitlementId  string   `json:"EntitlementId,omitempty"`
	Revision       int      `json:"Revision"`
	MimeType       string   `json:"MimeType"`
	FileSize       int64    `json:"FileSize"`
	Categories     []string `json:"Categories,omitempty"`
	BookhoardUUID  string   `json:"BookhoardUUID,omitempty"`
}

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

type KoboInitResponse struct {
	Resources map[string]interface{} `json:"Resources"`
	UserKey   string                 `json:"UserKey"`
}

type KoboSyncStatus struct {
	Status            string                 `json:"Status"`
	MarkupsSynced     int                    `json:"MarkupsSynced"`
	BookmarksSynced   int                    `json:"BookmarksSynced"`
	DeletedAnnotations []KoboDeletedAnnotation `json:"DeletedAnnotations,omitempty"`
}

type KoboDeletedAnnotation struct {
	ContentId  string `json:"ContentId"`
	BookmarkId string `json:"BookmarkId"`
	Type       string `json:"Type"`
}

type KoboServerSyncData struct {
	ContentId    string             `json:"ContentId"`
	PercentRead  float64            `json:"PercentRead"`
	Bookmarks    []KoboBookmarkSync `json:"Bookmarks,omitempty"`
	Highlights   []KoboBookmarkSync `json:"Highlights,omitempty"`
	LastModified string             `json:"LastModified"`
}

type KoboServerSyncResponse struct {
	BooksSynced    int `json:"BooksSynced"`
	BookmarksSent  int `json:"BookmarksSent"`
	HighlightsSent int `json:"HighlightsSent"`
}

type KoboAnalyticsTest struct {
	ContentId        string  `json:"ContentId"`
	ReadingEvent     string  `json:"ReadingEvent"`
	RemainingTimeMin int     `json:"RemainingTimeMin"`
	PercentRead      float64 `json:"PercentRead"`
}

func (h *KoboHandler) Initialization(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes
	deviceID := device.ID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
	deviceUUID := uuid.UUID(deviceID)

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

	librarySync := []KoboLibraryBook{}
	for _, item := range mediaItems {
		bookhoardUUID := uuid.UUID(item.ID.Bytes)

		progress, _ := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
			MediaItemID: pgtype.UUID{Bytes: item.ID.Bytes, Valid: true},
			UserID:      pgUserID,
		})

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

		if progress.ID.Valid {
			percentRead = progress.Percentage.Float64 * 100
			if progress.LastReadAt.Valid {
				lastModified = progress.LastReadAt.Time.Format(time.RFC3339)
			}
			if progress.TotalPages.Valid && progress.CurrentPage.Valid {
				remaining := int(progress.TotalPages.Int32 - progress.CurrentPage.Int32)
				pagesRemaining = &remaining
			}
		}

		bookmarkCount := 0
		annotations, _ := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
			MediaItemID: pgtype.UUID{Bytes: item.ID.Bytes, Valid: true},
			UserID:      pgUserID,
		})
		bookmarkCount = len(annotations)

		author := ""
		if item.Author.Valid {
			author = item.Author.String
		}

		// Use ContentId mapping instead of direct UUID
		koboContentId, err := h.mapBookhoardUUIDToKoboContentId(c, bookhoardUUID, deviceUUID)
		if err != nil {
			// Fallback to entitlement_id or generate new one
			if item.EntitlementID.Valid && item.EntitlementID.String != "" {
				koboContentId = item.EntitlementID.String
			} else {
				koboContentId = "kobo_" + bookhoardUUID.String()
			}
		}

		mimeType := item.MimeType.String
		if !item.MimeType.Valid {
			mimeType = ""
		}

		fileSize := int64(0)
		if item.FileSize.Valid {
			fileSize = item.FileSize.Int64
		}

		revision := 1
		if item.RevisionNumber.Valid {
			revision = int(item.RevisionNumber.Int32)
		}

		contentType := "6"
		if strings.Contains(mimeType, "pdf") {
			contentType = "5"
		}

		// Get collection metadata for this book
		collections, _ := h.getCollectionMetadataForBook(c, bookhoardUUID, deviceUUID)

		librarySync = append(librarySync, KoboLibraryBook{
			ContentId:      koboContentId,
			ContentType:    contentType,
			Title:          item.Title,
			Author:         author,
			PercentRead:    percentRead,
			PagesRemaining: pagesRemaining,
			BookmarkCount:  bookmarkCount,
			LastModified:   lastModified,
			EntitlementId:  koboContentId,
			Revision:       revision,
			MimeType:       mimeType,
			FileSize:       fileSize,
			Categories:     collections,
			BookhoardUUID:  bookhoardUUID.String(),
		})
	}

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

func (h *KoboHandler) LibrarySync(c *echo.Context) error {
	return h.Initialization(c)
}

func (h *KoboHandler) Markup(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes
	deviceID := device.ID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
	deviceUUID := uuid.UUID(deviceID)

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

	markupsSynced := 0
	bookmarksSynced := 0
	unlinkedBooks := 0
	processedBooks := make(map[pgtype.UUID]string)

	for _, readingSync := range req.ReadingSync {
		bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, readingSync.ContentId, deviceUUID)
		if err != nil || bookhoardUUID == uuid.Nil {
			unlinkedBooks++
			continue
		}

		pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
		processedBooks[pgMediaUUID] = readingSync.ContentId
		percentage := readingSync.PercentRead / 100.0

		// Kobo only sends a percentage. For fixed-layout & comic formats the page
		// index is the canonical locator, so derive it from the known page count.
		var currentPage, totalPages *int
		if mediaItem, mErr := h.db.GetMediaItem(c.Request().Context(), pgMediaUUID); mErr == nil {
			if mediaItem.FormatGroup == string(wsync.FormatGroupFixedLayout) || mediaItem.FormatGroup == string(wsync.FormatGroupComicArchive) {
				if mediaItem.PageCount.Valid && mediaItem.PageCount.Int32 > 0 {
					total := int(mediaItem.PageCount.Int32)
					page := wsync.PercentageToPage(percentage, total)
					currentPage = &page
					totalPages = &total
				}
			}
		}

		if h.progressSvc != nil {
			_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
				MediaItemID: pgMediaUUID,
				UserID:      pgUserID,
				Source:      "kobo",
				DeviceID:    pgtype.UUID{Bytes: deviceID, Valid: true},
				Percentage:  &percentage,
				CurrentPage: currentPage,
				TotalPages:  totalPages,
				DeviceType:  "kobo",
				DeviceName:  device.DeviceName,
				Broadcast:   true,
			})
		} else {
			_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
				MediaItemID:    pgMediaUUID,
				UserID:         pgUserID,
				Percentage:     pgtype.Float8{Float64: percentage, Valid: true},
				LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
				LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
			})
		}

		if err == nil {
			markupsSynced++
		}
	}

	for _, bookmarkSync := range req.BookmarkSync {
		// Use ContentId mapping with fallback logic
		bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, bookmarkSync.ContentId, deviceUUID)
		if err != nil || bookhoardUUID == uuid.Nil {
			// Unlinked book - skip
			continue
		}

		pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
		processedBooks[pgMediaUUID] = bookmarkSync.ContentId

		switch bookmarkSync.BookmarkType {
		case "annotation":
			if bookmarkSync.BookmarkText != "" {
				if h.annotationSvc != nil {
					deviceData, _ := json.Marshal(map[string]interface{}{
						"bookmark_id":  bookmarkSync.BookmarkId,
						"date_created": bookmarkSync.DateCreated,
					})

					result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
						MediaItemID:    pgMediaUUID,
						UserID:         pgUserID,
						SelectionText:  bookmarkSync.BookmarkText,
						StartPosition:  bookmarkSync.BookmarkId,
						EndPosition:    bookmarkSync.BookmarkId,
						Color:          "#ffff00",
						NoteText:       bookmarkSync.BookmarkTitle,
						Source:         "kobo",
						DeviceSyncData: deviceData,
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						bookmarksSynced++
					}
				} else {
					h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
						MediaItemID:   pgMediaUUID,
						UserID:        pgUserID,
						SelectionText: bookmarkSync.BookmarkText,
						StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
						EndPosition:   pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
						Color:         pgtype.Text{String: "#ffff00", Valid: true},
					})
					bookmarksSynced++
				}
			}
		case "bookmark":
			if bookmarkSync.BookmarkText != "" {
				if h.annotationSvc != nil {
					deviceData, _ := json.Marshal(map[string]interface{}{
						"bookmark_id":  bookmarkSync.BookmarkId,
						"date_created": bookmarkSync.DateCreated,
					})

					result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
						MediaItemID:    pgMediaUUID,
						UserID:         pgUserID,
						Title:          bookmarkSync.BookmarkText,
						Position:       bookmarkSync.BookmarkId,
						ChapterNumber:  int32(bookmarkSync.Chapter),
						Source:         "kobo",
						DeviceSyncData: deviceData,
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						bookmarksSynced++
					}
				} else {
					h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
						MediaItemID: pgMediaUUID,
						UserID:      pgUserID,
						Content:     bookmarkSync.BookmarkText,
						Position:    pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
					})
					bookmarksSynced++
				}
			}
		case "last-read-place":
			if bookmarkSync.BookmarkId != "" {
				var epubcfi string

				if strings.HasPrefix(bookmarkSync.BookmarkId, "epubcfi(") {
					epubcfi = strings.TrimPrefix(bookmarkSync.BookmarkId, "epubcfi(")
					epubcfi = strings.TrimSuffix(epubcfi, ")")
				}

				chapter := bookmarkSync.Chapter
				chapterProgress := 0.5

				var convertedCFI *string
				var contextText *string

				if epubcfi != "" && h.libraryService != nil {
					mediaItem, mErr := h.db.GetMediaItem(c.Request().Context(), pgMediaUUID)
					if mErr == nil {
						formatGroup := wsync.FormatGroup(mediaItem.FormatGroup)
						if formatGroup != wsync.FormatGroupFixedLayout && formatGroup != wsync.FormatGroupComicArchive {
							convertedCFI, contextText = h.convertKoboCFIToStandard(c, mediaItem, epubcfi)
						}
					}
				}

				if convertedCFI != nil {
					epubcfi = *convertedCFI
				}

				if h.progressSvc != nil {
					_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
						MediaItemID:     pgMediaUUID,
						UserID:          pgUserID,
						Source:          "kobo",
						DeviceID:        pgtype.UUID{Bytes: deviceID, Valid: true},
						Epubcfi:         &epubcfi,
						ContextText:     contextText,
						Chapter:         &chapter,
						ChapterProgress: &chapterProgress,
						DeviceType:      "kobo",
						DeviceName:      device.DeviceName,
						Broadcast:       false,
					})
				} else {
					_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
						MediaItemID:     pgMediaUUID,
						UserID:          pgUserID,
						Epubcfi:         pgtype.Text{String: epubcfi, Valid: epubcfi != ""},
						Chapter:         pgtype.Int4{Int32: int32(bookmarkSync.Chapter), Valid: true},
						ChapterProgress: pgtype.Float8{Float64: 0.5, Valid: true},
						LastSyncDevice:  pgtype.Text{String: "kobo", Valid: true},
						LastSyncSource:  pgtype.Text{String: "kobo", Valid: true},
					})
				}
				if err != nil {
					fmt.Printf("Failed to store last-read-place: %v", err)
				}
				bookmarksSynced++
			}
		}
	}

	_, 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",
		})
	}

	response := KoboSyncStatus{
		Status:          "Success",
		MarkupsSynced:   markupsSynced,
		BookmarksSynced: bookmarksSynced,
	}

	if h.annotationSvc != nil && len(processedBooks) > 0 {
		cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
		for mediaItemID, contentId := range processedBooks {
			tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
				MediaItemID: mediaItemID,
				UserID:      pgUserID,
				DeletedAt:   cutoff,
			})
			for _, ts := range tombstones {
				var dd map[string]interface{}
				if len(ts.DeviceSyncData) > 0 {
					json.Unmarshal(ts.DeviceSyncData, &dd)
				}
				bookmarkID, _ := dd["bookmark_id"].(string)
				if bookmarkID == "" {
					continue
				}
				response.DeletedAnnotations = append(response.DeletedAnnotations, KoboDeletedAnnotation{
					ContentId:  contentId,
					BookmarkId: bookmarkID,
					Type:       ts.AnnotationType,
				})
			}
		}
	}

	// Include unlinked books count if any
	if unlinkedBooks > 0 {
		// For now, just log it. In production, this should trigger an alert
		// and the unlinked books should be stored for manual resolution
		response.Status = "Partial"
	}

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

func (h *KoboHandler) Bookmark(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes
	deviceID := device.ID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
	deviceUUID := uuid.UUID(deviceID)

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

	bookmarksSynced := 0

	for _, bookmarkSync := range req.BookmarkSync {
		// Use ContentId mapping with fallback logic
		bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, bookmarkSync.ContentId, deviceUUID)
		if err != nil || bookhoardUUID == uuid.Nil {
			// Unlinked book - skip
			continue
		}

		pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}

		switch bookmarkSync.BookmarkType {
		case "annotation":
			if bookmarkSync.BookmarkText != "" {
				if h.annotationSvc != nil {
					deviceData, _ := json.Marshal(map[string]interface{}{
						"bookmark_id":  bookmarkSync.BookmarkId,
						"date_created": bookmarkSync.DateCreated,
					})

					result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
						MediaItemID:    pgMediaUUID,
						UserID:         pgUserID,
						SelectionText:  bookmarkSync.BookmarkText,
						StartPosition:  bookmarkSync.BookmarkId,
						EndPosition:    bookmarkSync.BookmarkId,
						Color:          "#ffff00",
						NoteText:       bookmarkSync.BookmarkTitle,
						Source:         "kobo",
						DeviceSyncData: deviceData,
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						bookmarksSynced++
					}
				} else {
					h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
						MediaItemID:   pgMediaUUID,
						UserID:        pgUserID,
						SelectionText: bookmarkSync.BookmarkText,
						StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
						EndPosition:   pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
						Color:         pgtype.Text{String: "#ffff00", Valid: true},
					})
					bookmarksSynced++
				}
			}
		case "bookmark":
			if bookmarkSync.BookmarkText != "" {
				if h.annotationSvc != nil {
					deviceData, _ := json.Marshal(map[string]interface{}{
						"bookmark_id":  bookmarkSync.BookmarkId,
						"date_created": bookmarkSync.DateCreated,
					})

					result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
						MediaItemID:    pgMediaUUID,
						UserID:         pgUserID,
						Title:          bookmarkSync.BookmarkText,
						Position:       bookmarkSync.BookmarkId,
						ChapterNumber:  int32(bookmarkSync.Chapter),
						Source:         "kobo",
						DeviceSyncData: deviceData,
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						bookmarksSynced++
					}
				} else {
					h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
						MediaItemID: pgMediaUUID,
						UserID:      pgUserID,
						Content:     bookmarkSync.BookmarkText,
						Position:    pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
					})
					bookmarksSynced++
				}
			}
		}
	}

	_, 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.StatusOK, KoboSyncStatus{
		Status:          "Success",
		BookmarksSynced: bookmarksSynced,
		MarkupsSynced:   0,
	})
}

func (h *KoboHandler) AnalyticsGettests(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes
	deviceID := device.ID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
	deviceUUID := uuid.UUID(deviceID)

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

	for _, test := range req {
		bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, test.ContentId, deviceUUID)
		if err != nil || bookhoardUUID == uuid.Nil {
			continue
		}

		pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
		percentage := test.PercentRead / 100.0

		if h.progressSvc != nil {
			_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
				MediaItemID: pgMediaUUID,
				UserID:      pgUserID,
				Source:      "kobo",
				DeviceID:    pgtype.UUID{Bytes: deviceID, Valid: true},
				Percentage:  &percentage,
				DeviceType:  "kobo",
				DeviceName:  device.DeviceName,
				Broadcast:   true,
			})
		} else {
			_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
				MediaItemID:    pgMediaUUID,
				UserID:         pgUserID,
				Percentage:     pgtype.Float8{Float64: percentage, Valid: true},
				LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
				LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
			})
		}
	}

	_, 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.StatusOK, map[string]interface{}{
		"Status": "Success",
	})
}

func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
	device := c.Get("device").(database.Devices)
	userID := device.UserID.Bytes
	deviceID := device.ID.Bytes

	pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
	deviceUUID := uuid.UUID(deviceID)

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

	booksSynced := 0
	bookmarksSent := 0
	highlightsSent := 0

	for _, syncData := range req {
		bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, syncData.ContentId, deviceUUID)
		if err != nil || bookhoardUUID == uuid.Nil {
			continue
		}

		pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
		percentage := syncData.PercentRead / 100.0

		if h.progressSvc != nil {
			_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
				MediaItemID: pgMediaUUID,
				UserID:      pgUserID,
				Source:      "bookhoard",
				DeviceID:    pgtype.UUID{Bytes: deviceID, Valid: true},
				Percentage:  &percentage,
				DeviceType:  "kobo",
				DeviceName:  device.DeviceName,
				Broadcast:   false,
			})
		} else {
			_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
				MediaItemID:    pgMediaUUID,
				UserID:         pgUserID,
				Percentage:     pgtype.Float8{Float64: percentage, Valid: true},
				LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
				LastSyncSource: pgtype.Text{String: "bookhoard", Valid: true},
			})
		}

		if err == nil {
			booksSynced++
		}

		for _, bookmark := range syncData.Bookmarks {
			if bookmark.BookmarkType == "bookmark" {
				if h.annotationSvc != nil {
					result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
						MediaItemID: pgMediaUUID,
						UserID:      pgUserID,
						Title:       bookmark.BookmarkText,
						Position:    bookmark.BookmarkId,
						Source:      "kobo",
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						bookmarksSent++
					}
				} else {
					h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
						MediaItemID: pgMediaUUID,
						UserID:      pgUserID,
						Content:     bookmark.BookmarkText,
						Position:    pgtype.Text{String: bookmark.BookmarkId, Valid: true},
					})
					bookmarksSent++
				}
			} else if bookmark.BookmarkType == "annotation" {
				if h.annotationSvc != nil {
					result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
						MediaItemID:   pgMediaUUID,
						UserID:        pgUserID,
						SelectionText: bookmark.BookmarkText,
						StartPosition: bookmark.BookmarkId,
						EndPosition:   bookmark.BookmarkId,
						Color:         "#ffff00",
						Source:        "kobo",
					})
					if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
						highlightsSent++
					}
				} else {
					h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
						MediaItemID:   pgMediaUUID,
						UserID:        pgUserID,
						SelectionText: bookmark.BookmarkText,
						StartPosition: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
						EndPosition:   pgtype.Text{String: bookmark.BookmarkId, Valid: true},
						Color:         pgtype.Text{String: "#ffff00", Valid: true},
					})
					highlightsSent++
				}
			}
		}

		for _, highlight := range syncData.Highlights {
			if h.annotationSvc != nil {
				result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
					MediaItemID:   pgMediaUUID,
					UserID:        pgUserID,
					SelectionText: highlight.BookmarkText,
					StartPosition: highlight.BookmarkId,
					EndPosition:   highlight.BookmarkId,
					Color:         "#ffff00",
					Source:        "kobo",
				})
				if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
					highlightsSent++
				}
			} else {
				h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
					MediaItemID:   pgMediaUUID,
					UserID:        pgUserID,
					SelectionText: highlight.BookmarkText,
					StartPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
					EndPosition:   pgtype.Text{String: highlight.BookmarkId, Valid: true},
					Color:         pgtype.Text{String: "#ffff00", Valid: true},
				})
				highlightsSent++
			}
		}
	}

	_, 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.StatusOK, KoboServerSyncResponse{
		BooksSynced:    booksSynced,
		BookmarksSent:  bookmarksSent,
		HighlightsSent: highlightsSent,
	})
}

func (h *KoboHandler) convertKoboCFIToStandard(c *echo.Context, mediaItem database.MediaItems, kepubCFI string) (*string, *string) {
	epubPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
	if err != nil {
		log.Printf("Bookhoard: KEPUB→CFI failed to resolve EPUB path: %v", err)
		return nil, nil
	}
	if epubPath == "" {
		log.Printf("Bookhoard: KEPUB→CFI resolved empty EPUB path for %s", mediaItem.FilePath)
		return nil, nil
	}

	kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{
		MediaItemID: pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true},
		FormatType:  "kepub",
	})
	if err != nil || !kepubFormat.FilePath.Valid {
		return nil, nil
	}

	converter := wsync.NewKEPUBCFIConverter(epubPath, kepubFormat.FilePath.String)
	result, err := converter.ConvertKEPUBCFIToStandard(kepubCFI, 0.0, "")
	if err != nil {
		log.Printf("Bookhoard: KEPUB→CFI conversion error: %v", err)
		return nil, nil
	}

	var cfi *string
	if result.CFI != "" {
		cfi = &result.CFI
		log.Printf("Bookhoard: KEPUB→CFI converted (precision=%s)", result.Precision)
	}

	var ctx *string
	if result.ExtractedContext != "" {
		ctx = &result.ExtractedContext
	}

	return cfi, ctx
}
