package handlers

import (
	"bookhoard/internal/database"
	"bookhoard/internal/opds"
	"bookhoard/internal/services"
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

type OPDSHandler struct {
	db                *database.Queries
	libraryService    *services.LibraryService
	conversionService interface {
		ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*services.ConvertedKEPUB, error)
	}
	settings *database.SettingsRegistry
}

// SetSettings wires the tunable settings registry (OPDS page size).
func (h *OPDSHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }

// opdsDefaultPageSize returns the configured default page size (50 if unset).
func (h *OPDSHandler) opdsDefaultPageSize() int {
	if h.settings != nil {
		return h.settings.OpdsDefaultPageSize()
	}
	return 50
}

// opdsMaxPageSize returns the configured maximum page size (200 if unset).
func (h *OPDSHandler) opdsMaxPageSize() int {
	if h.settings != nil {
		return h.settings.OpdsMaxPageSize()
	}
	return 200
}

func NewOPDSHandler(db *database.Queries, libraryService *services.LibraryService, conversionService interface {
	ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*services.ConvertedKEPUB, error)
}) *OPDSHandler {
	return &OPDSHandler{
		db:                db,
		libraryService:    libraryService,
		conversionService: conversionService,
	}
}

// Helper function to get base URL from system config with request-derived fallback
func (h *OPDSHandler) getBaseURLs(c *echo.Context) (string, string, error) {
	var dbBaseURL string
	if config, err := h.db.GetSystemConfig(c.Request().Context(), "base_url"); err == nil {
		dbBaseURL = config.Value
	}
	baseURL := deriveBaseURL(c, dbBaseURL)
	opdsBaseURL := baseURL + "/opds"
	return baseURL, opdsBaseURL, nil
}

func (h *OPDSHandler) getAuthToken(c *echo.Context) string {
	token := c.QueryParam("token")
	if token == "" {
		token = strings.TrimPrefix(c.Request().Header.Get("Authorization"), "Bearer ")
	}
	return token
}

func appendToken(url, token string) string {
	if token == "" {
		return url
	}
	if strings.Contains(url, "?") {
		return url + "&token=" + token
	}
	return url + "?token=" + token
}

// catalogMediaType is the OPDS media type for an acquisition catalog feed.
const catalogMediaType = "application/atom+xml;profile=opds-catalog;kind=acquisition"

// addCatalogPaginationLinks adds OPDS pagination links (self, start, first,
// previous, next, last) and OpenSearch paging metadata (totalResults,
// itemsPerPage, startIndex) to a feed based on the current page position.
// catalogBase is the device catalog URL without query parameters. The token
// (device auth) is appended to every generated link.
func addCatalogPaginationLinks(feed *opds.Feed, catalogBase string, pageNum, perPageNum, totalItems int, token string) {
	totalPages := 0
	if totalItems > 0 {
		totalPages = (totalItems + perPageNum - 1) / perPageNum
	}
	startIdx := (pageNum - 1) * perPageNum

	pagedURL := func(page int) string {
		return appendToken(fmt.Sprintf("%s?page=%d&per_page=%d", catalogBase, page, perPageNum), token)
	}

	// self reflects the current page; start/first point to the first page
	feed.AddLink(pagedURL(pageNum), catalogMediaType, "self")
	feed.AddLink(pagedURL(1), catalogMediaType, "start")
	feed.AddLink(pagedURL(1), catalogMediaType, "first")
	if totalPages > 0 {
		feed.AddLink(pagedURL(totalPages), catalogMediaType, "last")
	}
	if pageNum > 1 {
		feed.AddLink(pagedURL(pageNum-1), catalogMediaType, "previous")
	}
	if pageNum < totalPages {
		feed.AddLink(pagedURL(pageNum+1), catalogMediaType, "next")
	}

	feed.SetPagination(totalItems, perPageNum, startIdx+1)
}

// resolveMimeType returns the mime type for a media item, preferring the stored
// mime_type, then format_mimetype, and finally falling back to EPUB.
func resolveMimeType(mime, formatMime pgtype.Text) string {
	if mime.Valid && mime.String != "" {
		return mime.String
	}
	if formatMime.Valid && formatMime.String != "" {
		return formatMime.String
	}
	return "application/epub+zip"
}

// isComicArchive reports whether a format group represents a comic/manga
// archive (cbz/cbr/cb7/cbt). Comic archives are served in their native format
// and should not be offered as EPUB/KEPUB/PDF conversions.
func isComicArchive(formatGroup string) bool {
	return strings.EqualFold(formatGroup, "comic_archive")
}

// formatLabelFromPath derives a short format label (e.g. "epub", "cbz") from a
// file path's extension, defaulting to "epub" when it cannot be determined.
func formatLabelFromPath(path string) string {
	ext := strings.ToLower(filepath.Ext(path))
	switch ext {
	case ".epub":
		return "epub"
	case ".pdf":
		return "pdf"
	case ".cbz":
		return "cbz"
	case ".cbr":
		return "cbr"
	case ".cb7":
		return "cb7"
	case ".cbt":
		return "cbt"
	case ".mobi":
		return "mobi"
	case ".azw", ".azw3":
		return "azw3"
	case ".txt":
		return "txt"
	case "":
		return "epub"
	default:
		return strings.TrimPrefix(ext, ".")
	}
}

// GetDeviceCatalog returns the OPDS catalog feed for a device
func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
	deviceID := c.Param("deviceId")

	page := c.QueryParam("page")
	perPage := c.QueryParam("per_page")
	includeFormat := c.QueryParam("include_format")

	// Parse pagination parameters
	pageNum := 1
	if page != "" {
		if num, err := strconv.Atoi(page); err == nil && num > 0 {
			pageNum = num
		}
	}

	perPageNum := h.opdsDefaultPageSize()
	maxPerPage := h.opdsMaxPageSize()
	if perPage != "" {
		if num, err := strconv.Atoi(perPage); err == nil && num > 0 && num <= maxPerPage {
			perPageNum = num
		}
	}

	// Get base URLs
	baseURL, opdsBaseURL, err := h.getBaseURLs(c)
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
	}

	// Parse device ID
	deviceUUID, err := uuid.Parse(deviceID)
	if err != nil {
		return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
	}

	// Verify device exists
	device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
	}

	// Get user's visible libraries
	userID := device.UserID.Bytes
	userUUID := uuid.UUID(userID)

	libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
	}

	// Get media items
	var allItems []database.ListMediaItemsByLibraryRow
	for _, lib := range libraries {
		items, err := h.db.ListMediaItemsByLibrary(c.Request().Context(), pgtype.UUID{Bytes: lib.ID.Bytes, Valid: true})
		if err != nil {
			continue
		}
		allItems = append(allItems, items...)
	}

	// Filter by format if specified
	if includeFormat != "" {
		filteredItems := []database.ListMediaItemsByLibraryRow{}
		for _, item := range allItems {
			if strings.EqualFold(item.FormatGroup, includeFormat) {
				filteredItems = append(filteredItems, item)
			}
		}
		allItems = filteredItems
	}

	// Pagination
	totalItems := len(allItems)
	startIdx := (pageNum - 1) * perPageNum
	endIdx := startIdx + perPageNum

	if startIdx >= totalItems {
		allItems = []database.ListMediaItemsByLibraryRow{}
	} else if endIdx > totalItems {
		allItems = allItems[startIdx:]
	} else {
		allItems = allItems[startIdx:endIdx]
	}

	// Create OPDS feed
	feed := opds.NewFeed(
		fmt.Sprintf("urn:uuid:%s", deviceID),
		"Bookhoard Library",
	)

	// Feed links, including OPDS pagination links (first/previous/next/last) and
	// OpenSearch paging metadata (totalResults/itemsPerPage/startIndex).
	token := h.getAuthToken(c)
	catalogBase := fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID)
	addCatalogPaginationLinks(feed, catalogBase, pageNum, perPageNum, totalItems, token)

	// OpenSearch: the search link points to an OpenSearch description document
	// (served by the same /search endpoint when no query is supplied) so that
	// OPDS clients like KOReader can discover how to formulate search requests.
	searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search", opdsBaseURL, deviceID), token)
	feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")

	// Add entries
	for _, item := range allItems {
		bookUUID := uuid.UUID(item.ID.Bytes).String()
		title := item.Title
		author := ""
		if item.Author.Valid {
			author = item.Author.String
		}

		updated := item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z")

		entry := opds.NewEntry(
			fmt.Sprintf("urn:uuid:%s", bookUUID),
			title,
			author,
			updated,
		)

		// Add description
		if item.Description.Valid {
			entry.SetSummary(item.Description.String)
		}

		// Add acquisition link using the item's real mime type
		downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
		entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))

		// Only offer reflowable conversions (kepub/pdf) for ebooks; comic
		// archives are served as-is in their native format.
		if !isComicArchive(item.FormatGroup) {
			if device.DeviceType == "kobo" {
				kepubURL := downloadURL + "&format=kepub"
				entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip")
			}

			pdfURL := downloadURL + "&format=pdf"
			entry.AddAlternateLink(pdfURL, "application/pdf")
		}

		// Add canonical identifier
		entry.SetIdentifier(bookUUID)

		// Add SHA-256 metadata
		if item.FileSha256.Valid {
			entry.AddMetadata("bookhoard:sha256", item.FileSha256.String)
		}

		// Add collections as categories
		collections, err := h.db.GetCollectionsForBook(c.Request().Context(), pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
		if err == nil {
			collectionScheme := fmt.Sprintf("%s/collections", baseURL)
			for _, col := range collections {
				if col.UserID.Valid && col.UserID.Bytes == userUUID {
					entry.AddCategory(collectionScheme, col.Name)
				}
			}
		}

		feed.AddEntry(entry)
	}

	// Generate XML
	xmlString, err := feed.GenerateXMLString()
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
	}

	c.Response().Header().Set("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition")
	return c.String(http.StatusOK, xmlString)
}

// SearchDeviceCatalog searches the OPDS catalog for a device.
//
// When no "q" query parameter is supplied it returns an OpenSearch description
// document (application/opensearchdescription+xml) so that OPDS clients such as
// KOReader can discover the search URL template (which contains the
// {searchTerms} placeholder). When "q" is supplied it returns an OPDS
// acquisition feed of matching books.
func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
	deviceID := c.Param("deviceId")
	query := c.QueryParam("q")

	// Get base URLs
	baseURL, opdsBaseURL, err := h.getBaseURLs(c)
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
	}

	// Parse device ID
	deviceUUID, err := uuid.Parse(deviceID)
	if err != nil {
		return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
	}

	// Verify device exists
	device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
	}

	// Get user's visible libraries
	userID := device.UserID.Bytes
	_, err = h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
	}

	token := h.getAuthToken(c)

	// No query: serve the OpenSearch description document so clients can learn
	// the search template (contains the {searchTerms} placeholder).
	if query == "" {
		searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID), token)
		desc := opds.NewSearchDescription(
			"Bookhoard",
			"Search the Bookhoard library",
			searchURL,
		)
		xmlString, err := desc.GenerateXMLString()
		if err != nil {
			return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate search description"))
		}
		c.Response().Header().Set("Content-Type", "application/opensearchdescription+xml")
		return c.String(http.StatusOK, xmlString)
	}

	// Search media items
	allItems, err := h.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
		UserID:        pgtype.UUID{Bytes: userID, Valid: true},
		SearchPattern: pgtype.Text{String: "%" + query + "%", Valid: true},
		Offset:        pgtype.Int4{Int32: 0, Valid: true},
		Limit:         pgtype.Int4{Int32: 1000, Valid: true},
	})
	if err != nil {
		allItems = []database.SearchMediaItemsRow{}
	}

	// Create OPDS feed
	feed := opds.NewFeed(
		fmt.Sprintf("urn:uuid:%s", deviceID),
		fmt.Sprintf("Search: %s", query),
	)

	// Add feed links
	catalogURL := appendToken(fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID), token)
	feed.AddLink(catalogURL, catalogMediaType, "start")

	searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q=%s", opdsBaseURL, deviceID, query), token)
	feed.AddLink(searchURL, catalogMediaType, "self")

	// OpenSearch paging metadata (search results are a single page)
	feed.SetPagination(len(allItems), len(allItems), 1)

	// Add entries (same as catalog)
	userUUID := uuid.UUID(userID)
	for _, item := range allItems {
		bookUUID := uuid.UUID(item.ID.Bytes).String()
		title := item.Title
		author := ""
		if item.Author.Valid {
			author = item.Author.String
		}

		updated := item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z")

		entry := opds.NewEntry(
			fmt.Sprintf("urn:uuid:%s", bookUUID),
			title,
			author,
			updated,
		)

		if item.Description.Valid {
			entry.SetSummary(item.Description.String)
		}

		downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
		entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))

		// Only offer kepub conversion for ebooks; comic archives are served
		// as-is in their native format.
		if !isComicArchive(item.FormatGroup) && device.DeviceType == "kobo" {
			kepubURL := downloadURL + "&format=kepub"
			entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip")
		}

		entry.SetIdentifier(bookUUID)

		if item.FileSha256.Valid {
			entry.AddMetadata("bookhoard:sha256", item.FileSha256.String)
		}

		collections, err := h.db.GetCollectionsForBook(c.Request().Context(), pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
		if err == nil {
			collectionScheme := fmt.Sprintf("%s/collections", baseURL)
			for _, col := range collections {
				if col.UserID.Valid && col.UserID.Bytes == userUUID {
					entry.AddCategory(collectionScheme, col.Name)
				}
			}
		}

		feed.AddEntry(entry)
	}

	xmlString, err := feed.GenerateXMLString()
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
	}

	c.Response().Header().Set("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition")
	return c.String(http.StatusOK, xmlString)
}

// DownloadBook downloads a book with optional format conversion
func (h *OPDSHandler) DownloadBook(c *echo.Context) error {
	deviceID := c.Param("deviceId")
	bookID := c.Param("bookId")
	format := c.QueryParam("format") // epub, kepub, pdf, cbz

	// Default to epub
	if format == "" {
		format = "epub"
	}

	// Parse IDs (middleware guarantees valid UUIDs)
	deviceUUID, _ := uuid.Parse(deviceID)
	bookUUID, _ := uuid.Parse(bookID)

	// Verify device exists
	device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
	}

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

	// Get user's visible libraries
	userID := device.UserID.Bytes
	libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"})
	}

	// Check if book is in visible library
	visible := false
	for _, lib := range libraries {
		if lib.ID.Bytes == mediaItem.LibraryID.Bytes {
			visible = true
			break
		}
	}

	if !visible {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "book not accessible"})
	}

	// Get format information
	filePath := ""
	var fileSha256 string
	mimeType := ""

	if format == "kepub" {
		kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{
			MediaItemID: pgtype.UUID{Bytes: bookUUID, Valid: true},
			FormatType:  "kepub",
		})

		if err == nil && kepubFormat.FilePath.Valid {
			filePath = kepubFormat.FilePath.String
			if kepubFormat.FileSha256.Valid {
				fileSha256 = kepubFormat.FileSha256.String
			}
			if kepubFormat.MimeType.Valid {
				mimeType = kepubFormat.MimeType.String
			}
		} else {
			if h.conversionService != nil {
				resolvedPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
				if err != nil {
					return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
				}
				converted, err := h.conversionService.ConvertEPUBToKEPUB(c.Request().Context(), pgtype.UUID{Bytes: bookUUID, Valid: true}, resolvedPath)
				if err != nil {
					return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("KEPUB conversion failed: %v", err)})
				}
				filePath = converted.Path
				fileSha256 = converted.SHA256
				mimeType = "application/vnd.kobo+xml+zip"
			} else {
				filePath, err = h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
				if err != nil {
					return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
				}
				if mediaItem.MimeType.Valid {
					mimeType = mediaItem.MimeType.String
				}
			}
		}
	} else if format == "pdf" {
		// Check for PDF format
		pdfFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{
			MediaItemID: pgtype.UUID{Bytes: bookUUID, Valid: true},
			FormatType:  "pdf",
		})
		if err == nil && pdfFormat.FilePath.Valid {
			filePath = pdfFormat.FilePath.String
			if pdfFormat.FileSha256.Valid {
				fileSha256 = pdfFormat.FileSha256.String
			}
			if pdfFormat.MimeType.Valid {
				mimeType = pdfFormat.MimeType.String
			}
		} else {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "pdf format not available"})
		}
	} else {
		// Default: EPUB
		var err error
		filePath, err = h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "file not found"})
		}
		if mediaItem.MimeType.Valid {
			mimeType = mediaItem.MimeType.String
		}
		// Always expose the primary content hash so clients (e.g. the koreader
		// plugin) learn the canonical SHA-256 from the download response itself,
		// not just from the feed metadata.
		if mediaItem.FileSha256.Valid {
			fileSha256 = mediaItem.FileSha256.String
		}
	}

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

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

	// Get file info
	fileInfo, err := file.Stat()
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get file info"})
	}

	// Set headers
	filename := filepath.Base(filePath)
	c.Response().Header().Set("Content-Type", mimeType)
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
	c.Response().Header().Set("X-Bookhoard-UUID", bookUUID.String())

	if format == "kepub" && fileSha256 != "" {
		c.Response().Header().Set("X-Bookhoard-KEPUB-SHA256", fileSha256)
	} else if fileSha256 != "" {
		c.Response().Header().Set("X-Bookhoard-SHA256", fileSha256)
	}

	// Stream file
	http.ServeContent(c.Response(), c.Request(), filename, fileInfo.ModTime(), file)
	return nil
}

// GetCoverImage serves a book's cover image
func (h *OPDSHandler) GetCoverImage(c *echo.Context) error {
	deviceID := c.Param("deviceId")
	bookID := c.Param("bookId")

	// Parse IDs (middleware guarantees valid UUIDs)
	deviceUUID, _ := uuid.Parse(deviceID)
	bookUUID, _ := uuid.Parse(bookID)

	// Verify device exists
	device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
	}

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

	// Get user's visible libraries
	userID := device.UserID.Bytes
	libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"})
	}

	// Check if book is in visible library
	visible := false
	for _, lib := range libraries {
		if lib.ID.Bytes == mediaItem.LibraryID.Bytes {
			visible = true
			break
		}
	}

	if !visible {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "book not accessible"})
	}

	// Check if cover exists
	if !mediaItem.CoverImagePath.Valid || mediaItem.CoverImagePath.String == "" {
		return c.NoContent(http.StatusNoContent)
	}

	coverPath := mediaItem.CoverImagePath.String

	// Resolve relative path using library service
	fullPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverPath)
	if err != nil {
		return c.NoContent(http.StatusNoContent)
	}

	// Check if file exists
	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		return c.NoContent(http.StatusNoContent)
	}

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

	// Get file info
	fileInfo, err := file.Stat()
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get file info"})
	}

	// Determine content type
	ext := strings.ToLower(filepath.Ext(fullPath))
	contentType := "image/jpeg"
	if ext == ".png" {
		contentType = "image/png"
	} else if ext == ".webp" {
		contentType = "image/webp"
	}

	// Set cache headers
	c.Response().Header().Set("Content-Type", contentType)
	c.Response().Header().Set("Cache-Control", "public, max-age=31536000")

	// Stream image
	http.ServeContent(c.Response(), c.Request(), "", fileInfo.ModTime(), file)
	return nil
}

// GetDeviceNavigation returns the OPDS navigation feed for a device
func (h *OPDSHandler) GetDeviceNavigation(c *echo.Context) error {
	deviceID := c.Param("deviceId")

	// Get base URLs
	_, opdsBaseURL, err := h.getBaseURLs(c)
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
	}

	// Parse device ID (middleware guarantees valid UUID)
	deviceUUID, _ := uuid.Parse(deviceID)

	// Verify device exists
	_, err = h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
	}

	// Create OPDS navigation feed
	feed := opds.NewFeed(
		fmt.Sprintf("urn:uuid:%s", deviceID),
		"Bookhoard Library Navigation",
	)

	// Add feed links
	catalogURL := fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID)
	feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "start")
	feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "self")

	// Add navigation entry for catalog
	catalogEntry := opds.NewEntry(
		fmt.Sprintf("urn:uuid:%s-catalog", deviceID),
		"All Books",
		"",
		feed.Updated,
	)
	catalogEntry.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "subsection")
	catalogEntry.SetSummary("Browse all books in the library")
	feed.AddEntry(catalogEntry)

	// Generate XML
	xmlString, err := feed.GenerateXMLString()
	if err != nil {
		return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
	}

	c.Response().Header().Set("Content-Type", "application/atom+xml;profile=opds-catalog;kind=navigation")
	return c.String(http.StatusOK, xmlString)
}

// ListFormats lists available formats for a book
func (h *OPDSHandler) ListFormats(c *echo.Context) error {
	deviceID := c.Param("deviceId")
	bookID := c.Param("bookId")

	// Parse IDs (middleware guarantees valid UUIDs)
	deviceUUID, _ := uuid.Parse(deviceID)
	bookUUID, _ := uuid.Parse(bookID)

	// Verify device exists
	device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
	}

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

	// Get user's visible libraries
	userID := device.UserID.Bytes
	libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get libraries"})
	}

	// Check if book is in visible library
	visible := false
	for _, lib := range libraries {
		if lib.ID.Bytes == mediaItem.LibraryID.Bytes {
			visible = true
			break
		}
	}

	if !visible {
		return c.JSON(http.StatusForbidden, map[string]string{"error": "book not accessible"})
	}

	// Get all formats
	formats, err := h.db.GetMediaItemFormats(c.Request().Context(), pgtype.UUID{Bytes: bookUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get formats"})
	}

	type FormatInfo struct {
		FormatType string `json:"format_type"`
		FilePath   string `json:"file_path,omitempty"`
		FileSha256 string `json:"file_sha256,omitempty"`
		FileSize   int64  `json:"file_size_bytes,omitempty"`
		MimeType   string `json:"mime_type,omitempty"`
		Available  bool   `json:"available"`
	}

	formatList := []FormatInfo{}

	// Add the primary/native format (always available if media item exists)
	fileSize := int64(0)
	if mediaItem.FileSize.Valid {
		fileSize = mediaItem.FileSize.Int64
	}

	formatList = append(formatList, FormatInfo{
		FormatType: formatLabelFromPath(mediaItem.FilePath),
		FilePath:   mediaItem.FilePath,
		FileSha256: func() string {
			if mediaItem.FileSha256.Valid {
				return mediaItem.FileSha256.String
			} else {
				return ""
			}
		}(),
		FileSize: fileSize,
		MimeType: func() string {
			if mediaItem.MimeType.Valid {
				return mediaItem.MimeType.String
			} else {
				return "application/epub+zip"
			}
		}(),
		Available: true,
	})

	// Add other formats
	for _, format := range formats {
		fileSize := int64(0)
		if format.FileSizeBytes.Valid {
			fileSize = format.FileSizeBytes.Int64
		}

		formatList = append(formatList, FormatInfo{
			FormatType: format.FormatType,
			FilePath: func() string {
				if format.FilePath.Valid {
					return format.FilePath.String
				} else {
					return ""
				}
			}(),
			FileSha256: func() string {
				if format.FileSha256.Valid {
					return format.FileSha256.String
				} else {
					return ""
				}
			}(),
			FileSize: fileSize,
			MimeType: func() string {
				if format.MimeType.Valid {
					return format.MimeType.String
				} else {
					return ""
				}
			}(),
			Available: format.FilePath.Valid,
		})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"media_item_id": bookUUID.String(),
		"formats":       formatList,
	})
}

// RegisterOPDS registers a device for OPDS access
func (h *OPDSHandler) RegisterOPDS(c *echo.Context) error {
	deviceID := c.Param("deviceId")

	// Parse device ID
	deviceUUID, err := uuid.Parse(deviceID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device id"})
	}

	// Verify device exists
	_, err = h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
	if err != nil {
		return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"})
	}

	// Get base URLs
	_, opdsBaseURL, err := h.getBaseURLs(c)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get config"})
	}

	// Generate OPDS token
	tokenBytes := make([]byte, 32)
	if _, err := rand.Read(tokenBytes); err != nil {
		tokenBytes = []byte(deviceID + "-" + uuid.New().String())
	}
	token := hex.EncodeToString(tokenBytes)[:64]

	// Create OPDS token record (valid for 1 year)
	expiresAt := time.Now().AddDate(1, 0, 0)
	_, err = h.db.CreateOpdsToken(c.Request().Context(), database.CreateOpdsTokenParams{
		DeviceID:  pgtype.UUID{Bytes: deviceUUID, Valid: true},
		Token:     token,
		TokenType: pgtype.Text{String: "device", Valid: true},
		ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create OPDS token"})
	}

	catalogURL := fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID)

	return c.JSON(http.StatusOK, map[string]interface{}{
		"opds_token": map[string]interface{}{
			"token":      token,
			"token_type": "device",
			"expires_at": expiresAt.Format(time.RFC3339),
		},
		"opds_catalog_url": catalogURL,
		"refresh_interval": 3600,
	})
}
