package api

import (
	"crypto/md5"
	"database/sql"
	"fmt"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	argon2 "github.com/alexedwards/argon2id"
	"github.com/gabriel-vasile/mimetype"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
	"golang.org/x/exp/slices"
	"reichard.io/bbank/database"
	"reichard.io/bbank/metadata"
	"reichard.io/bbank/search"
	"reichard.io/bbank/utils"
)

type queryParams struct {
	Page     *int64  `form:"page"`
	Limit    *int64  `form:"limit"`
	Document *string `form:"document"`
}

type searchParams struct {
	Query    *string `form:"query"`
	BookType *string `form:"book_type"`
}

type requestDocumentEdit struct {
	Title       *string               `form:"title"`
	Author      *string               `form:"author"`
	Description *string               `form:"description"`
	ISBN10      *string               `form:"isbn_10"`
	ISBN13      *string               `form:"isbn_13"`
	RemoveCover *string               `form:"remove_cover"`
	CoverGBID   *string               `form:"cover_gbid"`
	CoverFile   *multipart.FileHeader `form:"cover_file"`
}

type requestDocumentIdentify struct {
	Title  *string `form:"title"`
	Author *string `form:"author"`
	ISBN   *string `form:"isbn"`
}

type requestSettingsEdit struct {
	Password    *string `form:"password"`
	NewPassword *string `form:"new_password"`
	TimeOffset  *string `form:"time_offset"`
}

type requestDocumentAdd struct {
	ID       *string `form:"id"`
	Title    *string `form:"title"`
	Author   *string `form:"author"`
	BookType *string `form:"book_type"`
}

func (api *API) webManifest(c *gin.Context) {
	c.Header("Content-Type", "application/manifest+json")
	c.File("./assets/manifest.json")
}

func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any) func(*gin.Context) {
	// Merge Optional Template Data
	var templateVarsBase = gin.H{}
	if len(args) > 0 {
		templateVarsBase = args[0]
	}
	templateVarsBase["RouteName"] = routeName
	templateVarsBase["SearchEnabled"] = api.Config.SearchEnabled

	return func(c *gin.Context) {
		var userID string
		if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
			userID = rUser.(string)
		}

		// Copy Base & Update
		templateVars := gin.H{}
		for k, v := range templateVarsBase {
			templateVars[k] = v
		}
		templateVars["User"] = userID

		// Potential URL Parameters
		qParams := bindQueryParams(c)

		if routeName == "documents" {
			documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
				UserID: userID,
				Offset: (*qParams.Page - 1) * *qParams.Limit,
				Limit:  *qParams.Limit,
			})
			if err != nil {
				log.Error("[createAppResourcesRoute] GetDocumentsWithStats DB Error:", err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			if err = api.getDocumentsWordCount(documents); err != nil {
				log.Error("[createAppResourcesRoute] Unable to Get Word Counts: ", err)
			}

			templateVars["Data"] = documents
		} else if routeName == "document" {
			var rDocID requestDocumentID
			if err := c.ShouldBindUri(&rDocID); err != nil {
				log.Error("[createAppResourcesRoute] Invalid URI Bind")
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
				UserID:     userID,
				DocumentID: rDocID.DocumentID,
			})
			if err != nil {
				log.Error("[createAppResourcesRoute] GetDocumentWithStats DB Error:", err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			templateVars["RelBase"] = "../"
			templateVars["Data"] = document
			templateVars["TotalTimeLeftSeconds"] = (document.Pages - document.Page) * document.SecondsPerPage
		} else if routeName == "activity" {
			activityFilter := database.GetActivityParams{
				UserID: userID,
				Offset: (*qParams.Page - 1) * *qParams.Limit,
				Limit:  *qParams.Limit,
			}

			if qParams.Document != nil {
				activityFilter.DocFilter = true
				activityFilter.DocumentID = *qParams.Document
			}

			activity, err := api.DB.Queries.GetActivity(api.DB.Ctx, activityFilter)
			if err != nil {
				log.Error("[createAppResourcesRoute] GetActivity DB Error:", err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			templateVars["Data"] = activity
		} else if routeName == "home" {
			start := time.Now()
			read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, userID)
			log.Info("GetDailyReadStats Performance: ", time.Since(start))

			start = time.Now()
			database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, userID)
			log.Info("GetDatabaseInfo Performance: ", time.Since(start))

			streaks, _ := api.DB.Queries.GetUserStreaks(api.DB.Ctx, userID)
			wpn_leaderboard, _ := api.DB.Queries.GetWPMLeaderboard(api.DB.Ctx)

			templateVars["Data"] = gin.H{
				"Streaks":        streaks,
				"GraphData":      read_graph_data,
				"DatabaseInfo":   database_info,
				"WPMLeaderboard": wpn_leaderboard,
			}
		} else if routeName == "settings" {
			user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID)
			if err != nil {
				log.Error("[createAppResourcesRoute] GetUser DB Error:", err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID)
			if err != nil {
				log.Error("[createAppResourcesRoute] GetDevices DB Error:", err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
				return
			}

			templateVars["Data"] = gin.H{
				"Settings": gin.H{
					"TimeOffset": *user.TimeOffset,
				},
				"Devices": devices,
			}
		} else if routeName == "search" {
			var sParams searchParams
			c.BindQuery(&sParams)

			// Only Handle Query
			if sParams.BookType != nil && !slices.Contains([]string{"NON_FICTION", "FICTION"}, *sParams.BookType) {
				templateVars["SearchErrorMessage"] = "Invalid Book Type"
			} else if sParams.Query != nil && *sParams.Query == "" {
				templateVars["SearchErrorMessage"] = "Invalid Query"
			} else if sParams.BookType != nil && sParams.Query != nil {
				var bType search.BookType = search.BOOK_FICTION
				if *sParams.BookType == "NON_FICTION" {
					bType = search.BOOK_NON_FICTION
				}

				// Search
				searchResults := search.SearchBook(*sParams.Query, bType)
				templateVars["Data"] = searchResults
				templateVars["BookType"] = *sParams.BookType
			}
		} else if routeName == "login" {
			templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
		}

		c.HTML(http.StatusOK, routeName, templateVars)
	}
}

func (api *API) getDocumentCover(c *gin.Context) {
	var rDoc requestDocumentID
	if err := c.ShouldBindUri(&rDoc); err != nil {
		log.Error("[getDocumentCover] Invalid URI Bind")
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Validate Document Exists in DB
	document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
	if err != nil {
		log.Error("[getDocumentCover] GetDocument DB Error:", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Handle Identified Document
	if document.Coverfile != nil {
		if *document.Coverfile == "UNKNOWN" {
			c.File("./assets/no-cover.jpg")
			return
		}

		// Derive Path
		safePath := filepath.Join(api.Config.DataPath, "covers", *document.Coverfile)

		// Validate File Exists
		_, err = os.Stat(safePath)
		if err != nil {
			log.Error("[getDocumentCover] File Should But Doesn't Exist:", err)
			c.File("./assets/no-cover.jpg")
			return
		}

		c.File(safePath)
		return
	}

	// --- Attempt Metadata ---

	var coverDir string = filepath.Join(api.Config.DataPath, "covers")
	var coverFile string = "UNKNOWN"

	// Identify Documents & Save Covers
	metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
		Title:  document.Title,
		Author: document.Author,
	})

	if err == nil && len(metadataResults) > 0 && metadataResults[0].ID != nil {
		firstResult := metadataResults[0]

		// Save Cover
		fileName, err := metadata.CacheCover(*firstResult.ID, coverDir, document.ID, false)
		if err == nil {
			coverFile = *fileName
		}

		// Store First Metadata Result
		if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
			DocumentID:  document.ID,
			Title:       firstResult.Title,
			Author:      firstResult.Author,
			Description: firstResult.Description,
			Gbid:        firstResult.ID,
			Olid:        nil,
			Isbn10:      firstResult.ISBN10,
			Isbn13:      firstResult.ISBN13,
		}); err != nil {
			log.Error("[getDocumentCover] AddMetadata DB Error:", err)
		}
	}

	// Upsert Document
	if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
		ID:        document.ID,
		Coverfile: &coverFile,
	}); err != nil {
		log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
	}

	// Return Unknown Cover
	if coverFile == "UNKNOWN" {
		c.File("./assets/no-cover.jpg")
		return
	}

	coverFilePath := filepath.Join(coverDir, coverFile)
	c.File(coverFilePath)
}

func (api *API) documentReader(c *gin.Context) {
	rUser, _ := c.Get("AuthorizedUser")

	var rDoc requestDocumentID
	if err := c.ShouldBindUri(&rDoc); err != nil {
		log.Error("[documentReader] Invalid URI Bind")
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, database.GetProgressParams{
		DocumentID: rDoc.DocumentID,
		UserID:     rUser.(string),
	})

	if err != nil && err != sql.ErrNoRows {
		log.Error("[documentReader] UpsertDocument DB Error:", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
		UserID:     rUser.(string),
		DocumentID: rDoc.DocumentID,
	})
	if err != nil {
		log.Error("[documentReader] GetDocumentWithStats DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	c.HTML(http.StatusOK, "reader", gin.H{
		"SearchEnabled": api.Config.SearchEnabled,
		"Progress":      progress.Progress,
		"Data":          document,
		"RelBase":       "../../",
	})
}

func (api *API) editDocument(c *gin.Context) {
	var rDocID requestDocumentID
	if err := c.ShouldBindUri(&rDocID); err != nil {
		log.Error("[createAppResourcesRoute] Invalid URI Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	var rDocEdit requestDocumentEdit
	if err := c.ShouldBind(&rDocEdit); err != nil {
		log.Error("[createAppResourcesRoute] Invalid Form Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Validate Something Exists
	if rDocEdit.Author == nil &&
		rDocEdit.Title == nil &&
		rDocEdit.Description == nil &&
		rDocEdit.ISBN10 == nil &&
		rDocEdit.ISBN13 == nil &&
		rDocEdit.RemoveCover == nil &&
		rDocEdit.CoverGBID == nil &&
		rDocEdit.CoverFile == nil {
		log.Error("[createAppResourcesRoute] Missing Form Values")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Handle Cover
	var coverFileName *string
	if rDocEdit.RemoveCover != nil && *rDocEdit.RemoveCover == "on" {
		s := "UNKNOWN"
		coverFileName = &s
	} else if rDocEdit.CoverFile != nil {
		// Validate Type & Derive Extension on MIME
		uploadedFile, err := rDocEdit.CoverFile.Open()
		if err != nil {
			log.Error("[createAppResourcesRoute] File Error")
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
			return
		}

		fileMime, err := mimetype.DetectReader(uploadedFile)
		if err != nil {
			log.Error("[createAppResourcesRoute] MIME Error")
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
			return
		}
		fileExtension := fileMime.Extension()

		// Validate Extension
		if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
			log.Error("[uploadDocumentFile] Invalid FileType: ", fileExtension)
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
			return
		}

		// Generate Storage Path
		fileName := fmt.Sprintf("%s%s", rDocID.DocumentID, fileExtension)
		safePath := filepath.Join(api.Config.DataPath, "covers", fileName)

		// Save
		err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
		if err != nil {
			log.Error("[createAppResourcesRoute] File Error")
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
			return
		}

		coverFileName = &fileName
	} else if rDocEdit.CoverGBID != nil {
		var coverDir string = filepath.Join(api.Config.DataPath, "covers")
		fileName, err := metadata.CacheCover(*rDocEdit.CoverGBID, coverDir, rDocID.DocumentID, true)
		if err == nil {
			coverFileName = fileName
		}
	}

	// Update Document
	if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
		ID:          rDocID.DocumentID,
		Title:       api.sanitizeInput(rDocEdit.Title),
		Author:      api.sanitizeInput(rDocEdit.Author),
		Description: api.sanitizeInput(rDocEdit.Description),
		Isbn10:      api.sanitizeInput(rDocEdit.ISBN10),
		Isbn13:      api.sanitizeInput(rDocEdit.ISBN13),
		Coverfile:   coverFileName,
	}); err != nil {
		log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	c.Redirect(http.StatusFound, "./")
	return
}

func (api *API) deleteDocument(c *gin.Context) {
	var rDocID requestDocumentID
	if err := c.ShouldBindUri(&rDocID); err != nil {
		log.Error("[deleteDocument] Invalid URI Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}
	changed, err := api.DB.Queries.DeleteDocument(api.DB.Ctx, rDocID.DocumentID)
	if err != nil {
		log.Error("[deleteDocument] DeleteDocument DB Error")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}
	if changed == 0 {
		log.Error("[deleteDocument] DeleteDocument DB Error")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
		return
	}

	c.Redirect(http.StatusFound, "../")
}

func (api *API) identifyDocument(c *gin.Context) {
	rUser, _ := c.Get("AuthorizedUser")

	var rDocID requestDocumentID
	if err := c.ShouldBindUri(&rDocID); err != nil {
		log.Error("[identifyDocument] Invalid URI Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	var rDocIdentify requestDocumentIdentify
	if err := c.ShouldBind(&rDocIdentify); err != nil {
		log.Error("[identifyDocument] Invalid Form Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Disallow Empty Strings
	if rDocIdentify.Title != nil && strings.TrimSpace(*rDocIdentify.Title) == "" {
		rDocIdentify.Title = nil
	}
	if rDocIdentify.Author != nil && strings.TrimSpace(*rDocIdentify.Author) == "" {
		rDocIdentify.Author = nil
	}
	if rDocIdentify.ISBN != nil && strings.TrimSpace(*rDocIdentify.ISBN) == "" {
		rDocIdentify.ISBN = nil
	}

	// Validate Values
	if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
		log.Error("[identifyDocument] Invalid Form")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Template Variables
	templateVars := gin.H{
		"RelBase":       "../../",
		"SearchEnabled": api.Config.SearchEnabled,
	}

	// Get Metadata
	metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
		Title:  rDocIdentify.Title,
		Author: rDocIdentify.Author,
		ISBN10: rDocIdentify.ISBN,
		ISBN13: rDocIdentify.ISBN,
	})
	if err == nil && len(metadataResults) > 0 {
		firstResult := metadataResults[0]

		// Store First Metadata Result
		if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
			DocumentID:  rDocID.DocumentID,
			Title:       firstResult.Title,
			Author:      firstResult.Author,
			Description: firstResult.Description,
			Gbid:        firstResult.ID,
			Olid:        nil,
			Isbn10:      firstResult.ISBN10,
			Isbn13:      firstResult.ISBN13,
		}); err != nil {
			log.Error("[identifyDocument] AddMetadata DB Error:", err)
		}

		templateVars["Metadata"] = firstResult
	} else {
		log.Warn("[identifyDocument] Metadata Error")
		templateVars["MetadataError"] = "No Metadata Found"
	}

	document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
		UserID:     rUser.(string),
		DocumentID: rDocID.DocumentID,
	})
	if err != nil {
		log.Error("[identifyDocument] GetDocumentWithStats DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	templateVars["Data"] = document
	templateVars["TotalTimeLeftSeconds"] = (document.Pages - document.Page) * document.SecondsPerPage

	c.HTML(http.StatusOK, "document", templateVars)
}

func (api *API) saveNewDocument(c *gin.Context) {
	var rDocAdd requestDocumentAdd
	if err := c.ShouldBind(&rDocAdd); err != nil {
		log.Error("[saveNewDocument] Invalid Form Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Validate Form Exists
	if rDocAdd.ID == nil ||
		rDocAdd.BookType == nil ||
		rDocAdd.Title == nil ||
		rDocAdd.Author == nil {
		log.Error("[saveNewDocument] Missing Form Values")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	var bType search.BookType = search.BOOK_FICTION
	if *rDocAdd.BookType == "NON_FICTION" {
		bType = search.BOOK_NON_FICTION
	}

	// Save Book
	tempFilePath, err := search.SaveBook(*rDocAdd.ID, bType)
	if err != nil {
		log.Warn("[saveNewDocument] Temp File Error: ", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Calculate Partial MD5 ID
	partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
	if err != nil {
		log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Derive Extension on MIME
	fileMime, err := mimetype.DetectFile(tempFilePath)
	fileExtension := fileMime.Extension()

	// Derive Filename
	var fileName string
	if *rDocAdd.Author != "" {
		fileName = fileName + *rDocAdd.Author
	} else {
		fileName = fileName + "Unknown"
	}

	if *rDocAdd.Title != "" {
		fileName = fileName + " - " + *rDocAdd.Title
	} else {
		fileName = fileName + " - Unknown"
	}

	// Remove Slashes
	fileName = strings.ReplaceAll(fileName, "/", "")

	// Derive & Sanitize File Name
	fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, partialMD5, fileExtension))

	// Generate Storage Path
	safePath := filepath.Join(api.Config.DataPath, "documents", fileName)

	// Move File
	if err := os.Rename(tempFilePath, safePath); err != nil {
		log.Warn("[saveNewDocument] Move Temp File Error: ", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Get MD5 Hash
	fileHash, err := getFileMD5(safePath)
	if err != nil {
		log.Error("[saveNewDocument] Hash Failure:", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Upsert Document
	if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
		ID:       partialMD5,
		Title:    rDocAdd.Title,
		Author:   rDocAdd.Author,
		Md5:      fileHash,
		Filepath: &fileName,
	}); err != nil {
		log.Error("[saveNewDocument] UpsertDocument DB Error:", err)
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
}

func (api *API) editSettings(c *gin.Context) {
	rUser, _ := c.Get("AuthorizedUser")

	var rUserSettings requestSettingsEdit
	if err := c.ShouldBind(&rUserSettings); err != nil {
		log.Error("[editSettings] Invalid Form Bind")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Validate Something Exists
	if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.TimeOffset == nil {
		log.Error("[editSettings] Missing Form Values")
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	templateVars := gin.H{
		"User": rUser,
	}
	newUserSettings := database.UpdateUserParams{
		UserID: rUser.(string),
	}

	// Set New Password
	if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
		password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
		authorized := api.authorizeCredentials(rUser.(string), password)
		if authorized == true {
			password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
			hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
			if err != nil {
				templateVars["PasswordErrorMessage"] = "Unknown Error"
			} else {
				templateVars["PasswordMessage"] = "Password Updated"
				newUserSettings.Password = &hashedPassword
			}
		} else {
			templateVars["PasswordErrorMessage"] = "Invalid Password"
		}
	}

	// Set Time Offset
	if rUserSettings.TimeOffset != nil {
		templateVars["TimeOffsetMessage"] = "Time Offset Updated"
		newUserSettings.TimeOffset = rUserSettings.TimeOffset
	}

	// Update User
	_, err := api.DB.Queries.UpdateUser(api.DB.Ctx, newUserSettings)
	if err != nil {
		log.Error("[editSettings] UpdateUser DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Get User
	user, err := api.DB.Queries.GetUser(api.DB.Ctx, rUser.(string))
	if err != nil {
		log.Error("[editSettings] GetUser DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	// Get Devices
	devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string))
	if err != nil {
		log.Error("[editSettings] GetDevices DB Error:", err)
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
		return
	}

	templateVars["Data"] = gin.H{
		"Settings": gin.H{
			"TimeOffset": *user.TimeOffset,
		},
		"Devices":       devices,
		"SearchEnabled": api.Config.SearchEnabled,
	}

	c.HTML(http.StatusOK, "settings", templateVars)
}

func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStatsRow) error {
	// Do Transaction
	tx, err := api.DB.DB.Begin()
	if err != nil {
		log.Error("[getDocumentsWordCount] Transaction Begin DB Error:", err)
		return err
	}

	// Defer & Start Transaction
	defer tx.Rollback()
	qtx := api.DB.Queries.WithTx(tx)

	for _, item := range documents {
		if item.Words == nil && item.Filepath != nil {
			filePath := filepath.Join(api.Config.DataPath, "documents", *item.Filepath)
			wordCount, err := metadata.GetWordCount(filePath)
			if err != nil {
				log.Warn("[getDocumentsWordCount] Word Count Error - ", err)
			} else {
				if _, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
					ID:    item.ID,
					Words: &wordCount,
				}); err != nil {
					log.Error("[getDocumentsWordCount] UpsertDocument DB Error - ", err)
					return err
				}
			}
		}
	}

	// Commit Transaction
	if err := tx.Commit(); err != nil {
		log.Error("[getDocumentsWordCount] Transaction Commit DB Error:", err)
		return err
	}

	return nil
}

func bindQueryParams(c *gin.Context) queryParams {
	var qParams queryParams
	c.BindQuery(&qParams)

	if qParams.Limit == nil {
		var defaultValue int64 = 50
		qParams.Limit = &defaultValue
	} else if *qParams.Limit < 0 {
		var zeroValue int64 = 0
		qParams.Limit = &zeroValue
	}

	if qParams.Page == nil || *qParams.Page < 1 {
		var oneValue int64 = 0
		qParams.Page = &oneValue
	}

	return qParams
}
