package router

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"time"

	"bookhoard/internal/database"
	"bookhoard/internal/handlers"
	"bookhoard/internal/services"
	"bookhoard/internal/utils"
	"bookhoard/templates"

	"github.com/golang-jwt/jwt/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/labstack/echo/v5"

	"github.com/google/uuid"
)

func renderErrorPage(c *echo.Context, message string, errorType string) error {
	var buf bytes.Buffer
	err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf)
	if err != nil {
		log.Printf("renderErrorPage failed to render template: %v", err)
		return c.HTML(http.StatusInternalServerError, "Internal server error")
	}
	return c.HTML(http.StatusInternalServerError, buf.String())
}

func registerFrontendRoutes(cfg *Config) {
	e := cfg.Echo

	// ============================================================================
	// FRONTEND ROUTES - DO NOT DELETE
	// These routes serve Server-Side Rendered (SSR) HTML pages for the web UI.
	// They are NOT API endpoints and should NOT be removed during refactors.
	// All authenticated frontend routes use the jwtMiddleware to validate tokens.
	// ============================================================================

	// Use existing JWT middleware (sets database user object in context)
	jwtMiddleware := createJWTMiddleware(cfg)

	// Protected route group for API routes
	protected := e.Group("/api", jwtMiddleware)

	// ============================================================================
	// PUBLIC FRONTEND ROUTES (No authentication required)
	// ============================================================================

	// Public routes for login and registration pages
	e.GET("/login", func(c *echo.Context) error {
		var buf bytes.Buffer
		sessionExpired := c.QueryParam("session") == "expired"
		deleted := c.QueryParam("deleted") == "true"
		err := templates.Login(sessionExpired, deleted).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	e.GET("/register", func(c *echo.Context) error {
		var buf bytes.Buffer
		err := templates.Register().Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Root route - landing page with smart login detection
	e.GET("/", func(c *echo.Context) error {
		var buf bytes.Buffer
		var err error

		tokenString := c.Request().Header.Get("Authorization")
		if tokenString != "" && len(tokenString) > 7 && tokenString[:7] == "Bearer " {
			tokenString = tokenString[7:]
		} else {
			cookie, err := c.Cookie("token")
			if err == nil {
				tokenString = cookie.Value
			}
		}

		loggedIn := false
		if tokenString != "" {
			token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
				return []byte(cfg.Cfg.JWTSecret), nil
			})
			loggedIn = err == nil && token.Valid
		}

		err = templates.Index(loggedIn).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// ============================================================================
	// PROTECTED FRONTEND ROUTES (Authentication required)
	// ============================================================================

	// Protected frontend routes (no /api prefix)
	frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg))

	// Series browse page
	frontendProtected.GET("/series", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string

		libRes := resolveLibrary(c, cfg, user.ID)
		libraryID := libRes.LibraryID
		libData := libRes.Libraries
		if libRes.IsAll {
			errorMsg = ""
		}

		perSeriesPage := 24
		page := 1
		if p := c.QueryParam("page"); p != "" {
			if v, err := strconv.Atoi(p); err == nil && v > 0 {
				page = v
			}
		}
		offset := (page - 1) * perSeriesPage

		var seriesCards []templates.SeriesCardData
		totalPages := 1

		if errorMsg == "" {
			seriesList, total, err := handlers.GetSeriesCardsData(c.Request().Context(), cfg.Queries, libRes.LibUUID, perSeriesPage, offset)
			if err != nil {
				log.Printf("GetSeriesCardsData failed: %v", err)
				errorMsg = "Error loading series"
			} else {
				totalPages = (total + perSeriesPage - 1) / perSeriesPage
				if totalPages < 1 {
					totalPages = 1
				}
				seriesCards = make([]templates.SeriesCardData, 0, len(seriesList))
				for _, s := range seriesList {
					covers := s.CoverPaths
					if covers == nil {
						covers = []string{}
					}
					seriesCards = append(seriesCards, templates.SeriesCardData{
						Name:          s.Name,
						BookCount:     s.BookCount,
						TotalInSeries: s.TotalInSeries,
						CoverPaths:    covers,
					})
				}
			}
		}

		if seriesCards == nil {
			seriesCards = []templates.SeriesCardData{}
		}

		var buf bytes.Buffer
		err = templates.Series(user, seriesCards, libData, libraryID, totalPages, page, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Series detail page (books in a specific series)
	frontendProtected.GET("/series/detail", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string

		seriesName := c.QueryParam("name")
		if seriesName == "" {
			return renderErrorPage(c, "Series name required", "bad_request")
		}

		var bookInfoList []handlers.BookInfo

		svc := services.NewSeriesService(cfg.Queries)
		books, err := svc.GetSeriesBooks(c.Request().Context(), seriesName)
		if err != nil {
			log.Printf("GetSeriesBooks failed: %v", err)
			errorMsg = "Error loading series books"
		} else {
			bookInfoList = make([]handlers.BookInfo, 0, len(books))
			for _, item := range books {
				itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
				bookInfoList = append(bookInfoList, handlers.BookInfo{
					MediaItemID:    itemUUID.String(),
					Title:          item.Title,
					Author:         textToString(item.Author),
					CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
				})
			}
		}

		if bookInfoList == nil {
			bookInfoList = []handlers.BookInfo{}
		}

		seriesUserUUID, _ := uuid.Parse(user.ID)
		bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: seriesUserUUID, Valid: true}, bookInfoList)

		var buf bytes.Buffer
		err = templates.BrowseDetail(user, "📚", "Series", seriesName, seriesName, "/series", "All Series", "📚", "This series doesn't have any books yet", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	frontendProtected.GET("/tags/detail", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string

		tagName := c.QueryParam("name")
		if tagName == "" {
			return renderErrorPage(c, "Tag name required", "bad_request")
		}

		libRes := resolveLibrary(c, cfg, user.ID)
		libraryID := libRes.LibraryID

		var bookInfoList []handlers.BookInfo

		if libraryID != "" && errorMsg == "" {
			books, err := cfg.Queries.GetBooksByTag(c.Request().Context(), database.GetBooksByTagParams{
				LibraryID: libRes.LibUUID,
				Column2:   tagName,
			})
			if err != nil {
				log.Printf("GetBooksByTag failed: %v", err)
				errorMsg = "Error loading tag books"
			} else {
				bookInfoList = make([]handlers.BookInfo, 0, len(books))
				for _, item := range books {
					itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
					bookInfoList = append(bookInfoList, handlers.BookInfo{
						MediaItemID:    itemUUID.String(),
						Title:          item.Title,
						Author:         textToString(item.Author),
						CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
					})
				}
			}
		}

		if bookInfoList == nil {
			bookInfoList = []handlers.BookInfo{}
		}

		tagUserUUID, _ := uuid.Parse(user.ID)
		bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: tagUserUUID, Valid: true}, bookInfoList)

		var buf bytes.Buffer
		err = templates.BrowseDetail(user, "🏷️", "Tag", tagName, tagName, "/bookshelf", "Bookshelf", "🏷️", "No books found with this tag", bookInfoList, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	frontendProtected.GET("/bookshelf", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string

		libRes := resolveLibrary(c, cfg, user.ID)
		libraryID := libRes.LibraryID
		libData := libRes.Libraries

		// Fetch saved filters for SSR
		userUUID, _ := uuid.Parse(user.ID)
		var savedFilters []database.SavedFilters
		savedFilters, err = cfg.Queries.GetSavedFilters(c.Request().Context(), database.GetSavedFiltersParams{
			UserID:       pgtype.UUID{Bytes: userUUID, Valid: true},
			ResourceType: "media-items",
		})
		if err != nil {
			log.Printf("GetSavedFilters failed: %v", err)
			savedFilters = []database.SavedFilters{}
		}

		// Fetch first page of books for SSR
		var bookInfoList []handlers.BookInfo
		totalCount := 0
		limit := 50
		offset := 0

		if errorMsg == "" {
			// Check URL params for pagination
			if limitStr := c.QueryParam("limit"); limitStr != "" {
				if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
					limit = l
				}
			}
			if offsetStr := c.QueryParam("offset"); offsetStr != "" {
				if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
					offset = o
				}
			}

			params := services.SearchParams{
				UserID:         pgtype.UUID{Bytes: userUUID, Valid: true},
				LibraryID:      libRes.LibUUID,
				SearchQuery:    "",
				AuthorFilter:   "",
				SeriesFilter:   "",
				GenreFilter:    "",
				TagsFilter:     "",
				LanguageFilter: "",
				YearMin:        0,
				YearMax:        0,
				HasCover:       pgtype.Bool{Valid: false},
				Sort:           "created_at DESC",
				Limit:          limit,
				Offset:         offset,
			}
			var results []database.SearchMediaItemsUnifiedRow
			results, totalCount, err = cfg.MediaHandler.ExecuteSearch(c.Request().Context(), params)
			if err != nil {
				log.Printf("ExecuteSearch failed: %v", err)
			} else {
				bookInfoList = make([]handlers.BookInfo, len(results))
				for i, book := range results {
					bookUUID, _ := uuid.FromBytes(book.ID.Bytes[0:16])
					bookLibUUID, _ := uuid.FromBytes(book.LibraryID.Bytes[0:16])
					bookInfoList[i] = handlers.BookInfo{
						MediaItemID:    bookUUID.String(),
						Title:          book.Title,
						Author:         textToString(book.Author),
						CoverImagePath: utils.ResolveMediaURL(pgtype.UUID{Bytes: bookLibUUID, Valid: true}, book.CoverImagePath),
					}
				}
			}
		}

		var buf bytes.Buffer
		bookInfoList = handlers.MarkActiveConflicts(c.Request().Context(), cfg.Queries, pgtype.UUID{Bytes: userUUID, Valid: true}, bookInfoList)
		err = templates.BookShelf(user, libData, libraryID, errorMsg, savedFilters, bookInfoList, limit, offset, totalCount).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Dashboard page
	frontendProtected.GET("/dashboard", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string

		libRes := resolveLibrary(c, cfg, user.ID)
		libraryID := libRes.LibraryID

		userUUID, _ := uuid.Parse(user.ID)
		pgLibUUID := libRes.LibUUID

		prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, pgLibUUID)
		if err != nil {
			log.Printf("GetDashboardPreferences failed: %v", err)
			prefs = database.UserDashboardPreferences{
				HiddenCollections: []string{},
				CollectionOrder:   []string{},
				ItemsPerSection:   pgtype.Int4{Int32: 20, Valid: true},
			}
		}
		limit := 20
		if prefs.ItemsPerSection.Int32 > 0 {
			limit = int(prefs.ItemsPerSection.Int32)
		}

		// Get ALL sections (unfiltered) for the modal
		allSections, err := cfg.DashboardService.GetDashboardSections(
			c.Request().Context(),
			userUUID,
			pgLibUUID,
			limit,
			prefs.CollectionOrder,
			[]string{}, // No filtering - get all sections
		)
		if err != nil {
			log.Printf("Dashboard sections query failed: %v", err)
			allSections = []services.DashboardSection{}
			errorMsg = "Error loading dashboard"
		}

		// Get only visible sections for the dashboard display
		visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)

		userPgID := pgtype.UUID{Bytes: userUUID, Valid: true}
		sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID))
		allSectionsData := handlers.BuildSections(allSections, libraryID)

		var buf bytes.Buffer
		err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Collections page
	frontendProtected.GET("/collections", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var collections []database.Collections

		collections, err = cfg.CollectionHandler.GetCollectionsData(c)
		if err != nil {
			log.Printf("GetCollectionsData failed: %v", err)
			collections = []database.Collections{}
			errorMsg = "Error loading collections"
		}

		colData := make([]templates.CollectionData, len(collections))
		for i, col := range collections {
			colData[i] = templates.CollectionData{
				ID:          uuid.UUID(col.ID.Bytes).String(),
				Name:        col.Name,
				Description: getText(col.Description),
				Color:       getText(col.Color),
				Icon:        getText(col.Icon),
			}
		}

		var buf bytes.Buffer
		err = templates.Collection(user, colData, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Collection create modal
	frontendProtected.GET("/collections/create-modal", func(c *echo.Context) error {
		var buf bytes.Buffer
		err := templates.CollectionModal(templates.CollectionData{}).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Collection edit modal
	frontendProtected.GET("/collections/:id/edit-modal", func(c *echo.Context) error {
		// Parse collection ID
		collectionID := c.Param("id")
		collUUID, err := uuid.Parse(collectionID)
		if err != nil {
			return c.HTML(http.StatusBadRequest, "<div>Invalid collection ID</div>")
		}

		// Fetch collection data
		collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
		if err != nil {
			return c.HTML(http.StatusNotFound, "<div>Collection not found</div>")
		}

		// Convert to template type
		colData := templates.CollectionData{
			ID:          uuid.UUID(collection.ID.Bytes).String(),
			Name:        collection.Name,
			Description: getText(collection.Description),
			Color:       getText(collection.Color),
			Icon:        getText(collection.Icon),
		}

		// Ensure default color if empty
		if colData.Color == "" {
			colData.Color = "blue"
		}

		var buf bytes.Buffer
		err = templates.CollectionModal(colData).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Restore system collection modal
	frontendProtected.GET("/collections/restore-modal", func(c *echo.Context) error {
		var buf bytes.Buffer
		err := templates.RestoreSystemCollectionModal().Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Collection detail page (works for both system and user collections)
	frontendProtected.GET("/collections/:id", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		// Parse collection ID from URL
		collectionID := c.Param("id")
		collUUID, err := uuid.Parse(collectionID)
		if err != nil {
			return renderErrorPage(c, "Invalid collection ID", "invalid_id")
		}
		// Fetch collection details
		collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
		if err != nil {
			if err.Error() == "no rows in result set" {
				return renderErrorPage(c, "Collection not found", "not_found")
			}
			return renderErrorPage(c, "Error loading collection", "collection_load_error")
		}
		// Fetch books in collection
		userUUID, _ := uuid.Parse(user.ID)
		var books []handlers.BookInfo

		libRes := resolveLibrary(c, cfg, user.ID)
		libraryID := libRes.LibraryID

		if collection.QueryType.Valid && collection.QueryType.String != "" {
			dashboardSvc := services.NewDashboardService(cfg.Queries)
			sections, secErr := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libRes.LibUUID, 1000, []string{}, []string{})
			if secErr != nil {
				return renderErrorPage(c, "Error loading books", "books_load_error")
			}

			for _, section := range sections {
				if section.CollectionID.String() == collectionID {
					bookCards := make([]handlers.BookInfo, len(section.Items))
					for i, item := range section.Items {
						itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
						bookCards[i] = handlers.BookInfo{
							MediaItemID:    itemUUID.String(),
							Title:          item.Title,
							Author:         getText(item.Author),
							CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
						}
					}
					books = bookCards
					break
				}
			}
		} else {
			if libraryID != "" && !libRes.IsAll {
				libUUID, parseErr := uuid.Parse(libraryID)
				if parseErr != nil {
					return renderErrorPage(c, "Invalid library ID", "invalid_library_id")
				}

				collItems, collErr := cfg.Queries.GetCollectionItemsForDashboard(c.Request().Context(),
					database.GetCollectionItemsForDashboardParams{
						CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true},
						LibraryID:    pgtype.UUID{Bytes: libUUID, Valid: true},
						Limit:        pgtype.Int4{Int32: 1000, Valid: true},
					})
				if collErr != nil {
					books = []handlers.BookInfo{}
				} else {
					var validItems []database.GetCollectionItemsForDashboardRow
					for _, item := range collItems {
						if !item.Excluded.Valid || !item.Excluded.Bool {
							validItems = append(validItems, item)
						}
					}

					bookCards := make([]handlers.BookInfo, len(validItems))
					for i, item := range validItems {
						itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
						bookCards[i] = handlers.BookInfo{
							MediaItemID:    itemUUID.String(),
							Title:          item.Title,
							Author:         getText(item.Author),
							CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
						}
					}
					books = bookCards
				}
			} else {
				collItems, collErr := cfg.Queries.GetCollectionItems(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
				if collErr != nil {
					books = []handlers.BookInfo{}
				}

				bookCards := make([]handlers.BookInfo, len(collItems))
				for i, item := range collItems {
					itemUUID, _ := uuid.FromBytes(item.MediaItemID.Bytes[0:16])
					bookCards[i] = handlers.BookInfo{
						MediaItemID:    itemUUID.String(),
						Title:          item.Title,
						Author:         getText(item.Author),
						CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
					}
				}
				books = bookCards
			}
		}
		// Build collection data
		colData := templates.CollectionData{
			ID:          collectionID,
			Name:        collection.Name,
			Description: collection.Description.String,
			Color:       collection.Color.String,
			Icon:        collection.Icon.String,
			IsSystem:    collection.IsSystemCollection.Bool,
		}

		var buf bytes.Buffer
		err = templates.CollectionDetail(user, colData, books, libraryID, libRes.Libraries).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Custom Section Builder page
	frontendProtected.GET("/custom-section", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var libraries []database.GetUserVisibleLibrariesRow

		userUUID, _ := uuid.Parse(user.ID)
		libraries, err = cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
		if err != nil {
			log.Printf("GetUserVisibleLibraries failed: %v", err)
			libraries = []database.GetUserVisibleLibrariesRow{}
			errorMsg = "Error loading libraries"
		}

		libData := make([]templates.LibraryData, len(libraries))
		for i, lib := range libraries {
			libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
			libData[i] = templates.LibraryData{
				ID:          libUUID.String(),
				Name:        lib.Name,
				Description: getText(lib.Description),
				TypeName:    lib.TypeName,
			}
		}

		var buf bytes.Buffer
		err = templates.CustomSectionBuilder(user, libData, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Progress page
	frontendProtected.GET("/progress", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var progressData []handlers.ProgressWithMedia

		progressData, err = cfg.ScannerHandler.GetAllProgressData(c)
		if err != nil {
			log.Printf("GetAllProgressData failed: %v", err)
			progressData = []handlers.ProgressWithMedia{}
			errorMsg = "Error loading progress"
		}

		var buf bytes.Buffer
		err = templates.Progress(user, progressData, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Devices page
	frontendProtected.GET("/devices", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var devices []handlers.DeviceInfo
		var pendingList []templates.PendingRegistrationData

		devices, err = cfg.DeviceHandler.GetDevicesData(c)
		if err != nil {
			log.Printf("GetDevicesData failed: %v", err)
			devices = []handlers.DeviceInfo{}
			errorMsg = "Error loading devices"
		}

		pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
		if err != nil {
			log.Printf("GetPendingRegistrationsData failed: %v", err)
			pendingList = []templates.PendingRegistrationData{}
			if errorMsg == "" {
				errorMsg = "Error loading pending registrations"
			}
		} else {
			pendingList = convertPending(pendingMaps)
		}

		// Get base URL from database config with fallback to config/env var
		baseURL := cfg.getBaseURL(c.Request().Context())

		var buf bytes.Buffer
		err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Conflicts page
	frontendProtected.GET("/conflicts", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var conflictsData []handlers.ConflictDetailResponse
		var total, unresolved int

		conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
		if err != nil {
			log.Printf("GetConflictsData failed: %v", err)
			conflictsData = []handlers.ConflictDetailResponse{}
			total = 0
			unresolved = 0
			errorMsg = "Error loading conflicts"
		}

		var buf bytes.Buffer
		err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Analytics page
	frontendProtected.GET("/analytics", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		var buf bytes.Buffer
		err = templates.Analytics(user).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Profile page (all users)
	frontendProtected.GET("/profile", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var buf bytes.Buffer
		err = templates.Profile(user).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// ============================================================================
	// ADMIN FRONTEND ROUTES
	// ============================================================================

	// Admin routes
	frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		stats := getAdminStats(c.Request().Context(), cfg)
		var buf bytes.Buffer
		err = templates.Admin(user, stats).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		stats := getAdminStats(c.Request().Context(), cfg)
		var buf bytes.Buffer
		err = templates.Admin(user, stats).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		// Fetch all libraries (admin-only)
		libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context())
		if err != nil {
			log.Printf("ListLibrariesData failed: %v", err)
			libraries = []database.ListLibrariesRow{}
		}

		// Convert to template LibraryData
		libData := make([]templates.LibraryData, len(libraries))
		for i, lib := range libraries {
			libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
			folders, _ := cfg.LibraryService.GetLibraryFolders(c.Request().Context(), lib.ID)
			libData[i] = templates.LibraryData{
				ID:          libUUID.String(),
				Name:        lib.Name,
				Description: getText(lib.Description),
				TypeName:    lib.TypeName,
				TypeValue:   lib.TypeName,
				FolderCount: len(folders),
			}
		}

		// Fetch all users for visibility management
		users, err := cfg.Queries.ListUsers(c.Request().Context())
		if err != nil {
			log.Printf("ListUsers failed: %v", err)
			users = []database.ListUsersRow{}
		}

		// Convert to template User types
		userData := make([]templates.User, len(users))
		for i, u := range users {
			userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
			userData[i] = templates.User{
				ID:       userUUID.String(),
				Username: u.Username,
				Email:    u.Email,
				Role:     u.Role,
			}
		}

		var buf bytes.Buffer
		err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	frontendProtected.GET("/admin/libraries/:id/issues", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		libraryID := c.Param("id")
		libUUID, err := uuid.Parse(libraryID)
		if err != nil {
			return renderErrorPage(c, "Invalid library ID", "invalid_id")
		}
		// Fetch stats
		stats, err := cfg.Queries.GetProcessingIssueStats(
			c.Request().Context(),
			uuidToPGType(libUUID),
		)
		if err != nil {
			return renderErrorPage(c, "Error loading stats", "stats_load_error")
		}
		// Fetch issues
		dbIssues, err := cfg.Queries.ListProcessingIssuesByLibrary(
			c.Request().Context(),
			uuidToPGType(libUUID),
		)
		if err != nil {
			return renderErrorPage(c, "Error loading issues", "issues_load_error")
		}
		// Convert to template data
		issues := make([]templates.ProcessingIssueData, len(dbIssues))
		for i, issue := range dbIssues {
			issueUUID, _ := uuid.FromBytes(issue.ID.Bytes[0:16])
			issues[i] = templates.ProcessingIssueData{
				ID:               issueUUID.String(),
				MediaItemID:      uuid.UUID(issue.MediaItemID.Bytes).String(),
				Title:            issue.Title,
				FilePath:         issue.FilePath,
				FormatGroup:      issue.FormatGroup,
				LibraryTypeName:  issue.LibraryTypeName,
				IssueType:        issue.IssueType,
				IssueDescription: issue.IssueDescription,
				Severity:         issue.Severity,
				CreatedAt:        issue.CreatedAt.Time,
			}
		}
		statsData := templates.IssueStats{
			ErrorCount:   stats.ErrorCount,
			WarningCount: stats.WarningCount,
			InfoCount:    stats.InfoCount,
		}
		var buf bytes.Buffer
		err = templates.AdminProcessingIssues(user, libraryID, issues, statsData).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	// Admin hash conflicts page: content-duplicate groups flagged during hash
	// backfill or rescan, resolved by keeping all copies or merging into one.
	frontendProtected.GET("/admin/hash-conflicts", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		pending, err := cfg.Queries.ListPendingHashConflicts(c.Request().Context())
		if err != nil {
			return renderErrorPage(c, "Error loading hash conflicts", "conflicts_load_error")
		}

		conflicts := make([]templates.HashConflictData, 0, len(pending))
		for _, p := range pending {
			conflict := templates.HashConflictData{
				ID:          uuid.UUID(p.ID.Bytes).String(),
				LibraryName: p.LibraryName,
				SHA256:      p.FileSha256,
				SHAShort:    p.FileSha256[:16] + "…",
				CreatedAt:   p.CreatedAt.Time.Format("Jan 2, 2006"),
				Items:       []templates.HashConflictItemData{},
			}

			items, err := cfg.Queries.ListMediaItemsBySHA256AndLibrary(c.Request().Context(), database.ListMediaItemsBySHA256AndLibraryParams{
				FileSha256: pgtype.Text{String: p.FileSha256, Valid: true},
				LibraryID:  p.LibraryID,
			})
			if err != nil {
				continue
			}

			for _, mi := range items {
				counts, err := cfg.Queries.GetMediaItemUsageCounts(c.Request().Context(), mi.ID)
				if err != nil {
					counts = database.GetMediaItemUsageCountsRow{}
				}
				totalData := counts.ProgressCount + counts.HighlightsCount + counts.BookmarksCount + counts.NotesCount + counts.CollectionsCount
				conflict.Items = append(conflict.Items, templates.HashConflictItemData{
					ID:             uuid.UUID(mi.ID.Bytes).String(),
					Title:          mi.Title,
					Author:         mi.Author.String,
					FilePath:       mi.FilePath,
					FileSize:       mi.FileSize.Int64,
					UsageSummary:   fmt.Sprintf("%d progress, %d highlights, %d bookmarks, %d notes, %d collections",
						counts.ProgressCount, counts.HighlightsCount, counts.BookmarksCount, counts.NotesCount, counts.CollectionsCount),
					HasReadingData: totalData > 0,
				})
			}

			conflicts = append(conflicts, conflict)
		}

		var buf bytes.Buffer
		err = templates.AdminHashConflicts(user, conflicts).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	// Admin users page
	frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		// Fetch all users
		users, err := cfg.Queries.ListUsers(c.Request().Context())
		if err != nil {
			return renderErrorPage(c, "Error loading users", "users_load_error")
		}

		// Count admins for UI protection
		adminCount := 0
		for _, u := range users {
			if u.Role == "admin" {
				adminCount++
			}
		}

		// Convert to template users
		templateUsers := make([]templates.User, len(users))
		for i, u := range users {
			templateUsers[i] = templates.User{
				ID:        uuid.UUID(u.ID.Bytes).String(),
				Username:  u.Username,
				Email:     u.Email,
				Role:      u.Role,
				Theme:     getText(u.Theme),
				FirstName: getText(u.FirstName),
				LastName:  getText(u.LastName),
				CreatedAt: u.CreatedAt.Time,
			}
		}

		var buf bytes.Buffer
		err = templates.AdminUsers(user, templateUsers, adminCount).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	// Admin: Get profile modal for editing user
	frontendProtected.GET("/admin/settings", handlers.AdminMiddleware(func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		ctx := c.Request().Context()

		// Fetch current system configuration - just base_url
		baseURL := cfg.getBaseURL(ctx)

		systemConfig := map[string]string{
			"base_url":         baseURL,
			"default_timezone": "UTC",
		}

		defaultTimezone, err := cfg.Queries.GetSystemTimezone(ctx)
		if err == nil && defaultTimezone != "" {
			systemConfig["default_timezone"] = defaultTimezone
		}

		// Fetch scan settings
		scanSettings := templates.ScanSettingsData{
			AutoScanEnabled:         true,
			ScanPollIntervalSeconds: 60,
		}
		if val, err := cfg.Queries.GetSystemSetting(ctx, "auto_scan_enabled"); err == nil {
			scanSettings.AutoScanEnabled = val == "true"
		}
		if val, err := cfg.Queries.GetSystemSetting(ctx, "scan_poll_interval_seconds"); err == nil {
			if n, err := strconv.Atoi(val); err == nil {
				scanSettings.ScanPollIntervalSeconds = n
			}
		}

		// Load tunable settings entries from the registry. Exclude keys that
		// already have their own dedicated UI cards (timezone dropdown, scan
		// settings) so they aren't listed twice.
		dedicatedUI := map[string]bool{
			"default_timezone":          true,
			"scan_poll_interval_seconds": true,
			"auto_scan_enabled":         true,
		}
		var tunableSettings []templates.SettingEntry
		if cfg.Settings != nil {
			for _, e := range cfg.Settings.All() {
				if dedicatedUI[e.Key] {
					continue
				}
				tunableSettings = append(tunableSettings, templates.SettingEntry{
					Key:             e.Key,
					Value:           e.Value,
					Type:            e.Type,
					Min:             e.Min,
					Max:             e.Max,
					RequiresRestart: e.RequiresRestart,
					Category:        e.Category,
					Group:           e.Group,
					Description:     e.Description,
					IsDefault:       e.IsDefault,
				})
			}
		}
		liveGroups, restartGroups := templates.GroupTunableSettings(tunableSettings)

		var buf bytes.Buffer
		err = templates.AdminSettings(user, systemConfig, scanSettings, liveGroups, restartGroups, "").Render(ctx, &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c *echo.Context) error {
		// Get target user ID from URL
		targetUserID := c.Param("id")
		parsedUUID, err := uuid.Parse(targetUserID)
		if err != nil {
			return c.HTML(http.StatusBadRequest, "<div>Invalid user ID</div>")
		}

		// Fetch target user
		targetUser, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(parsedUUID))
		if err != nil {
			return c.HTML(http.StatusNotFound, "<div>User not found</div>")
		}

		// Convert to template user
		templateUser := templates.User{
			ID:        uuid.UUID(targetUser.ID.Bytes).String(),
			Username:  targetUser.Username,
			Email:     targetUser.Email,
			Role:      targetUser.Role,
			Theme:     getText(targetUser.Theme),
			FirstName: getText(targetUser.FirstName),
			LastName:  getText(targetUser.LastName),
			CreatedAt: targetUser.CreatedAt.Time,
		}

		// Render modal
		var buf bytes.Buffer
		err = templates.ProfileModal(templateUser).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	}))

	// Admin library HTMX endpoints
	registerAdminLibraryRoutes(cfg, frontendProtected)
	// Admin settings HTMX endpoints
	registerAdminSettingsRoutes(cfg, frontendProtected)

	// ============================================================================
	// LEGACY API ROUTES (for backward compatibility)
	// ============================================================================

	// Keep legacy routes under /api for existing API consumers
	protected.GET("/devices-page", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}
		var errorMsg string
		var devices []handlers.DeviceInfo
		var pendingList []templates.PendingRegistrationData

		devices, err = cfg.DeviceHandler.GetDevicesData(c)
		if err != nil {
			log.Printf("GetDevicesData failed: %v", err)
			devices = []handlers.DeviceInfo{}
			errorMsg = "Error loading devices"
		}

		pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
		if err != nil {
			log.Printf("GetPendingRegistrationsData failed: %v", err)
			pendingList = []templates.PendingRegistrationData{}
			if errorMsg == "" {
				errorMsg = "Error loading pending registrations"
			}
		} else {
			pendingList = convertPending(pendingMaps)
		}

		// Get base URL from database config with fallback to config/env var
		baseURL := cfg.getBaseURL(c.Request().Context())

		var buf bytes.Buffer
		err = templates.Devices(user, devices, pendingList, errorMsg, baseURL).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	protected.GET("/conflicts-page", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		var errorMsg string
		var conflictsData []handlers.ConflictDetailResponse
		var total, unresolved int

		conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
		if err != nil {
			log.Printf("GetConflictsData failed: %v", err)
			conflictsData = []handlers.ConflictDetailResponse{}
			total = 0
			unresolved = 0
			errorMsg = "Error loading conflicts"
		}

		var buf bytes.Buffer
		err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	// Book detail page
	frontendProtected.GET("/media/:uuid", func(c *echo.Context) error {
		user, err := getTemplateUserWithTheme(c, cfg)
		if err != nil {
			return renderErrorPage(c, "Error loading user", "user_load_error")
		}

		// Parse media UUID from URL
		mediaUUID, err := uuid.Parse(c.Param("uuid"))
		if err != nil {
			return renderErrorPage(c, "Invalid media ID", "invalid_id")
		}
		pgMediaUUID := uuidToPGType(mediaUUID)

		// Get user UUID for queries
		userUUID, _ := uuid.Parse(user.ID)
		pgUserID := uuidToPGType(userUUID)

		// Fetch media item (embeds ALL metadata)
		mediaItem, err := cfg.Queries.GetMediaItem(c.Request().Context(), pgMediaUUID)
		if err != nil {
			if err.Error() == "no rows in result set" {
				return renderErrorPage(c, "Book not found", "not_found")
			}
			return renderErrorPage(c, "Error loading book", "database_error")
		}

		// Resolve cover image path
		if mediaItem.CoverImagePath.Valid && mediaItem.CoverImagePath.String != "" {
			resolvedPath := utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
			mediaItem.CoverImagePath = pgtype.Text{String: resolvedPath, Valid: true}
		}

		// Fetch rating
		var rating *database.MediaRatings
		userRating, err := cfg.Queries.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{
			MediaItemID: pgMediaUUID,
			UserID:      pgUserID,
		})
		if err == nil {
			rating = &userRating
		}

		// Fetch collections
		collections, _ := cfg.Queries.GetCollectionsForBook(c.Request().Context(), pgMediaUUID)

		// Fetch reading progress
		var progress *database.ReadingProgress
		readingProgress, err := cfg.Queries.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
			MediaItemID: pgMediaUUID,
			UserID:      pgUserID,
		})
		if err == nil {
			progress = &readingProgress
		}

		// Fetch active conflict (if any)
		var activeConflict *handlers.ConflictDetailResponse
		conflicts, err := cfg.Queries.ListSyncConflictsByMediaItem(c.Request().Context(),
			database.ListSyncConflictsByMediaItemParams{
				MediaItemID: pgMediaUUID,
				UserID:      pgUserID,
			})
		if err == nil && len(conflicts) > 0 {
			for _, conf := range conflicts {
				if conf.ResolutionStatus.Valid && conf.ResolutionStatus.String == "unresolved" {
					var conflictData map[string]handlers.ConflictSourceData
					if err := json.Unmarshal(conf.ConflictData, &conflictData); err == nil {
						activeConflict = &handlers.ConflictDetailResponse{
							ID:               uuid.UUID(conf.ID.Bytes).String(),
							MediaItemID:      uuid.UUID(conf.MediaItemID.Bytes).String(),
							MediaItemTitle:   mediaItem.Title,
							ConflictType:     conf.ConflictType,
							ConflictData:     conflictData,
							ResolutionStatus: conf.ResolutionStatus.String,
							CreatedAt:        conf.CreatedAt.Time,
						}
					}
					break
				}
			}
		}

		// Count notes and highlights
		notes, _ := cfg.Queries.GetMediaNotes(c.Request().Context(), database.GetMediaNotesParams{
			MediaItemID: pgMediaUUID,
			UserID:      pgUserID,
		})
		highlights, _ := cfg.Queries.GetMediaHighlights(c.Request().Context(), database.GetMediaHighlightsParams{
			MediaItemID: pgMediaUUID,
			UserID:      pgUserID,
		})

		// Assemble response (no field duplication!)
		detail := handlers.MediaDetail{
			MediaItems:      mediaItem, // Embedded - ALL fields available
			Rating:          rating,
			Collections:     collections,
			ReadingProgress: progress,
			ActiveConflict:  activeConflict,
			NotesCount:      len(notes),
			HighlightsCount: len(highlights),
		}

		// Render template
		var buf bytes.Buffer
		err = templates.BookDetail(user, detail, "").Render(c.Request().Context(), &buf)
		if err != nil {
			return err
		}
		return c.HTML(http.StatusOK, buf.String())
	})

	e.GET("/health", cfg.GetHealth)
}

// ============================================================================
// HEALTH CHECK
// ============================================================================

// Health check
func (cfg *Config) GetHealth(c *echo.Context) error {
	ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
	defer cancel()
	if err := pingDB(cfg, ctx); err != nil {
		return c.JSON(http.StatusServiceUnavailable, map[string]string{
			"status": "unhealthy",
			"error":  err.Error(),
		})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"status":   "healthy",
		"database": "connected",
		"scan": map[string]interface{}{
			"scan_in_progress": cfg.Worker.HasActiveScans(),
			"active_jobs":      cfg.Worker.GetActiveJobCount(),
		},
	})
}
