package api

import (
	"archive/zip"
	"bufio"
	"crypto/md5"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strings"
	"time"

	argon2 "github.com/alexedwards/argon2id"
	"github.com/gabriel-vasile/mimetype"
	"github.com/gin-gonic/gin"
	"github.com/itchyny/gojq"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"
	"reichard.io/antholume/database"
	"reichard.io/antholume/metadata"
	"reichard.io/antholume/utils"
)

type adminAction string

const (
	adminBackup        adminAction = "BACKUP"
	adminRestore       adminAction = "RESTORE"
	adminMetadataMatch adminAction = "METADATA_MATCH"
	adminCacheTables   adminAction = "CACHE_TABLES"
)

type requestAdminAction struct {
	Action adminAction `form:"action"`

	// Backup Action
	BackupTypes []backupType `form:"backup_types"`

	// Restore Action
	RestoreFile *multipart.FileHeader `form:"restore_file"`
}

type importType string

const (
	importDirect importType = "DIRECT"
	importCopy   importType = "COPY"
)

type requestAdminImport struct {
	Directory string     `form:"directory"`
	Select    string     `form:"select"`
	Type      importType `form:"type"`
}

type operationType string

const (
	opUpdate operationType = "UPDATE"
	opCreate operationType = "CREATE"
	opDelete operationType = "DELETE"
)

type requestAdminUpdateUser struct {
	User      string        `form:"user"`
	Password  *string       `form:"password"`
	IsAdmin   *bool         `form:"is_admin"`
	Operation operationType `form:"operation"`
}

type requestAdminLogs struct {
	Filter string `form:"filter"`
}

type importStatus string

const (
	importFailed  importStatus = "FAILED"
	importSuccess importStatus = "SUCCESS"
	importExists  importStatus = "EXISTS"
)

type importResult struct {
	ID     string
	Name   string
	Path   string
	Status importStatus
	Error  error
}

func (api *API) appPerformAdminAction(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin", c)

	var rAdminAction requestAdminAction
	if err := c.ShouldBind(&rAdminAction); err != nil {
		log.Error("Invalid Form Bind: ", err)
		appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
		return
	}

	switch rAdminAction.Action {
	case adminMetadataMatch:
		// TODO
		// 1. Documents xref most recent metadata table?
		// 2. Select all / deselect?
	case adminCacheTables:
		go func() {
			err := api.db.CacheTempTables()
			if err != nil {
				log.Error("Unable to cache temp tables: ", err)
			}
		}()
	case adminRestore:
		api.processRestoreFile(rAdminAction, c)
		return
	case adminBackup:
		// Vacuum
		_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
		if err != nil {
			log.Error("Unable to vacuum DB: ", err)
			appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
			return
		}

		// Set Headers
		c.Header("Content-type", "application/octet-stream")
		c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405")))

		// Stream Backup ZIP Archive
		c.Stream(func(w io.Writer) bool {
			var directories []string
			for _, item := range rAdminAction.BackupTypes {
				if item == backupCovers {
					directories = append(directories, "covers")
				} else if item == backupDocuments {
					directories = append(directories, "documents")
				}
			}

			err := api.createBackup(w, directories)
			if err != nil {
				log.Error("Backup Error: ", err)
			}
			return false
		})

		return
	}

	c.HTML(http.StatusOK, "page/admin", templateVars)
}

func (api *API) appGetAdmin(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin", c)
	c.HTML(http.StatusOK, "page/admin", templateVars)
}

func (api *API) appGetAdminLogs(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin-logs", c)

	var rAdminLogs requestAdminLogs
	if err := c.ShouldBindQuery(&rAdminLogs); err != nil {
		log.Error("Invalid URI Bind")
		appErrorPage(c, http.StatusNotFound, "Invalid URI parameters")
		return
	}
	rAdminLogs.Filter = strings.TrimSpace(rAdminLogs.Filter)

	var jqFilter *gojq.Code
	var basicFilter string
	if strings.HasPrefix(rAdminLogs.Filter, "\"") && strings.HasSuffix(rAdminLogs.Filter, "\"") {
		basicFilter = rAdminLogs.Filter[1 : len(rAdminLogs.Filter)-1]
	} else if rAdminLogs.Filter != "" {
		parsed, err := gojq.Parse(rAdminLogs.Filter)
		if err != nil {
			log.Error("Unable to parse JQ filter")
			appErrorPage(c, http.StatusNotFound, "Unable to parse JQ filter")
			return
		}

		jqFilter, err = gojq.Compile(parsed)
		if err != nil {
			log.Error("Unable to compile JQ filter")
			appErrorPage(c, http.StatusNotFound, "Unable to compile JQ filter")
			return
		}
	}

	// Open Log File
	logPath := filepath.Join(api.cfg.ConfigPath, "logs/antholume.log")
	logFile, err := os.Open(logPath)
	if err != nil {
		appErrorPage(c, http.StatusBadRequest, "Missing AnthoLume log file")
		return
	}
	defer logFile.Close()

	// Log Lines
	var logLines []string
	scanner := bufio.NewScanner(logFile)
	for scanner.Scan() {
		rawLog := scanner.Text()

		// Attempt JSON Pretty
		var jsonMap map[string]any
		err := json.Unmarshal([]byte(rawLog), &jsonMap)
		if err != nil {
			logLines = append(logLines, scanner.Text())
			continue
		}

		// Parse JSON
		rawData, err := json.MarshalIndent(jsonMap, "", "  ")
		if err != nil {
			logLines = append(logLines, scanner.Text())
			continue
		}

		// Basic Filter
		if basicFilter != "" && strings.Contains(string(rawData), basicFilter) {
			logLines = append(logLines, string(rawData))
			continue
		}

		// No JQ Filter
		if jqFilter == nil {
			continue
		}

		// Error or nil
		result, _ := jqFilter.Run(jsonMap).Next()
		if _, ok := result.(error); ok {
			logLines = append(logLines, string(rawData))
			continue
		} else if result == nil {
			continue
		}

		// Attempt filtered json
		filteredData, err := json.MarshalIndent(result, "", "  ")
		if err == nil {
			rawData = filteredData
		}

		logLines = append(logLines, string(rawData))
	}

	templateVars["Data"] = logLines
	templateVars["Filter"] = rAdminLogs.Filter

	c.HTML(http.StatusOK, "page/admin-logs", templateVars)
}

func (api *API) appGetAdminUsers(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin-users", c)

	users, err := api.db.Queries.GetUsers(api.db.Ctx)
	if err != nil {
		log.Error("GetUsers DB Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
		return
	}

	templateVars["Data"] = users

	c.HTML(http.StatusOK, "page/admin-users", templateVars)
}

func (api *API) appUpdateAdminUsers(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin-users", c)

	var rUpdate requestAdminUpdateUser
	if err := c.ShouldBind(&rUpdate); err != nil {
		log.Error("Invalid URI Bind")
		appErrorPage(c, http.StatusNotFound, "Invalid user parameters")
		return
	}

	// Ensure Username
	if rUpdate.User == "" {
		appErrorPage(c, http.StatusInternalServerError, "User cannot be empty")
		return
	}

	var err error
	switch rUpdate.Operation {
	case opCreate:
		err = api.createUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
	case opUpdate:
		err = api.updateUser(rUpdate.User, rUpdate.Password, rUpdate.IsAdmin)
	case opDelete:
		err = api.deleteUser(rUpdate.User)
	default:
		appErrorPage(c, http.StatusNotFound, "Unknown user operation")
		return
	}

	if err != nil {
		appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unable to create or update user: %v", err))
		return
	}

	users, err := api.db.Queries.GetUsers(api.db.Ctx)
	if err != nil {
		log.Error("GetUsers DB Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
		return
	}

	templateVars["Data"] = users

	c.HTML(http.StatusOK, "page/admin-users", templateVars)
}

func (api *API) appGetAdminImport(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin-import", c)

	var rImportFolder requestAdminImport
	if err := c.ShouldBindQuery(&rImportFolder); err != nil {
		log.Error("Invalid URI Bind")
		appErrorPage(c, http.StatusNotFound, "Invalid directory")
		return
	}

	if rImportFolder.Select != "" {
		templateVars["SelectedDirectory"] = rImportFolder.Select
		c.HTML(http.StatusOK, "page/admin-import", templateVars)
		return
	}

	// Default Path
	if rImportFolder.Directory == "" {
		dPath, err := filepath.Abs(api.cfg.DataPath)
		if err != nil {
			log.Error("Absolute filepath error: ", rImportFolder.Directory)
			appErrorPage(c, http.StatusNotFound, "Unable to get data directory absolute path")
			return
		}

		rImportFolder.Directory = dPath
	}

	entries, err := os.ReadDir(rImportFolder.Directory)
	if err != nil {
		log.Error("Invalid directory: ", rImportFolder.Directory)
		appErrorPage(c, http.StatusNotFound, "Invalid directory")
		return
	}

	allDirectories := []string{}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}

		allDirectories = append(allDirectories, e.Name())
	}

	templateVars["CurrentPath"] = filepath.Clean(rImportFolder.Directory)
	templateVars["Data"] = allDirectories

	c.HTML(http.StatusOK, "page/admin-import", templateVars)
}

func (api *API) appPerformAdminImport(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("admin-import", c)

	var rAdminImport requestAdminImport
	if err := c.ShouldBind(&rAdminImport); err != nil {
		log.Error("Invalid URI Bind")
		appErrorPage(c, http.StatusNotFound, "Invalid directory")
		return
	}

	// Get import directory
	importDirectory := filepath.Clean(rAdminImport.Directory)

	// Get data directory
	absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents"))

	// Validate different path
	if absoluteDataPath == importDirectory {
		appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
		return
	}

	// Do Transaction
	tx, err := api.db.DB.Begin()
	if err != nil {
		log.Error("Transaction Begin DB Error:", err)
		apiErrorPage(c, http.StatusBadRequest, "Unknown error")
		return
	}

	// Defer & Start Transaction
	defer func() {
		if err := tx.Rollback(); err != nil {
			log.Error("DB Rollback Error:", err)
		}
	}()
	qtx := api.db.Queries.WithTx(tx)

	// Track imports
	importResults := make([]importResult, 0)

	// Walk Directory & Import
	err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if f.IsDir() {
			return nil
		}

		// Get relative path
		basePath := importDirectory
		relFilePath, err := filepath.Rel(importDirectory, importPath)
		if err != nil {
			log.Warnf("path error: %v", err)
			return nil
		}

		// Track imports
		iResult := importResult{
			Path:   relFilePath,
			Status: importFailed,
		}
		defer func() {
			importResults = append(importResults, iResult)
		}()

		// Get metadata
		fileMeta, err := metadata.GetMetadata(importPath)
		if err != nil {
			log.Errorf("metadata error: %v", err)
			iResult.Error = err
			return nil
		}
		iResult.ID = *fileMeta.PartialMD5
		iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)

		// Check already exists
		_, err = qtx.GetDocument(api.db.Ctx, *fileMeta.PartialMD5)
		if err == nil {
			log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
			iResult.Status = importExists
			return nil
		}

		// Import Copy
		if rAdminImport.Type == importCopy {
			// Derive & Sanitize File Name
			relFilePath = deriveBaseFileName(fileMeta)
			safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)

			// Open Source File
			srcFile, err := os.Open(importPath)
			if err != nil {
				log.Errorf("unable to open current file: %v", err)
				iResult.Error = err
				return nil
			}
			defer srcFile.Close()

			// Open Destination File
			destFile, err := os.Create(safePath)
			if err != nil {
				log.Errorf("unable to open destination file: %v", err)
				iResult.Error = err
				return nil
			}
			defer destFile.Close()

			// Copy File
			if _, err = io.Copy(destFile, srcFile); err != nil {
				log.Errorf("unable to save file: %v", err)
				iResult.Error = err
				return nil
			}

			// Update Base & Path
			basePath = filepath.Join(api.cfg.DataPath, "documents")
			iResult.Path = relFilePath
		}

		// Upsert document
		if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
			ID:          *fileMeta.PartialMD5,
			Title:       fileMeta.Title,
			Author:      fileMeta.Author,
			Description: fileMeta.Description,
			Md5:         fileMeta.MD5,
			Words:       fileMeta.WordCount,
			Filepath:    &relFilePath,
			Basepath:    &basePath,
		}); err != nil {
			log.Errorf("UpsertDocument DB Error: %v", err)
			iResult.Error = err
			return nil
		}

		iResult.Status = importSuccess
		return nil
	})
	if err != nil {
		appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import Failed: %v", err))
		return
	}

	// Commit transaction
	if err := tx.Commit(); err != nil {
		log.Error("Transaction Commit DB Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import DB Error: %v", err))
		return
	}

	// Sort import results
	sort.Slice(importResults, func(i int, j int) bool {
		return importStatusPriority(importResults[i].Status) <
			importStatusPriority(importResults[j].Status)
	})

	templateVars["Data"] = importResults
	c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
}

func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
	// Validate Type & Derive Extension on MIME
	uploadedFile, err := rAdminAction.RestoreFile.Open()
	if err != nil {
		log.Error("File Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to open file")
		return
	}

	fileMime, err := mimetype.DetectReader(uploadedFile)
	if err != nil {
		log.Error("MIME Error")
		appErrorPage(c, http.StatusInternalServerError, "Unable to detect filetype")
		return
	}
	fileExtension := fileMime.Extension()

	// Validate Extension
	if !slices.Contains([]string{".zip"}, fileExtension) {
		log.Error("Invalid FileType: ", fileExtension)
		appErrorPage(c, http.StatusBadRequest, "Invalid filetype")
		return
	}

	// Create Temp File
	tempFile, err := os.CreateTemp("", "restore")
	if err != nil {
		log.Warn("Temp File Create Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to create temp file")
		return
	}
	defer os.Remove(tempFile.Name())
	defer tempFile.Close()

	// Save Temp
	err = c.SaveUploadedFile(rAdminAction.RestoreFile, tempFile.Name())
	if err != nil {
		log.Error("File Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to save file")
		return
	}

	// ZIP Info
	fileInfo, err := tempFile.Stat()
	if err != nil {
		log.Error("File Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to read file")
		return
	}

	// Create ZIP Reader
	zipReader, err := zip.NewReader(tempFile, fileInfo.Size())
	if err != nil {
		log.Error("ZIP Error: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to read zip")
		return
	}

	// Validate ZIP Contents
	hasDBFile := false
	hasUnknownFile := false
	for _, file := range zipReader.File {
		fileName := strings.TrimPrefix(file.Name, "/")
		if fileName == "antholume.db" {
			hasDBFile = true
			break
		} else if !strings.HasPrefix(fileName, "covers/") && !strings.HasPrefix(fileName, "documents/") {
			hasUnknownFile = true
			break
		}
	}

	// Invalid ZIP
	if !hasDBFile {
		log.Error("Invalid ZIP File - Missing DB")
		appErrorPage(c, http.StatusInternalServerError, "Invalid Restore ZIP - Missing DB")
		return
	} else if hasUnknownFile {
		log.Error("Invalid ZIP File - Invalid File(s)")
		appErrorPage(c, http.StatusInternalServerError, "Invalid Restore ZIP - Invalid File(s)")
		return
	}

	// Create Backup File
	backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
	backupFile, err := os.Create(backupFilePath)
	if err != nil {
		log.Error("Unable to create backup file: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to create backup file")
		return
	}
	defer backupFile.Close()

	// Save Backup File
	w := bufio.NewWriter(backupFile)
	err = api.createBackup(w, []string{"covers", "documents"})
	if err != nil {
		log.Error("Unable to save backup file: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to save backup file")
		return
	}

	// Remove Data
	err = api.removeData()
	if err != nil {
		log.Error("Unable to delete data: ", err)
		appErrorPage(c, http.StatusInternalServerError, "Unable to delete data")
		return
	}

	// Restore Data
	err = api.restoreData(zipReader)
	if err != nil {
		appErrorPage(c, http.StatusInternalServerError, "Unable to restore data")
		log.Panic("Unable to restore data: ", err)
	}

	// Reinit DB
	if err := api.db.Reload(); err != nil {
		appErrorPage(c, http.StatusInternalServerError, "Unable to reload DB")
		log.Panicf("Unable to reload DB: %v", err)
	}

	// Rotate Auth Hashes
	if err := api.rotateAllAuthHashes(); err != nil {
		appErrorPage(c, http.StatusInternalServerError, "Unable to rotate hashes")
		log.Panicf("Unable to rotate auth hashes: %v", err)
	}

	// Redirect to login page
	c.Redirect(http.StatusFound, "/login")
}

func (api *API) restoreData(zipReader *zip.Reader) error {
	// Ensure Directories
	api.cfg.EnsureDirectories()

	// Restore Data
	for _, file := range zipReader.File {
		rc, err := file.Open()
		if err != nil {
			return err
		}
		defer rc.Close()

		destPath := filepath.Join(api.cfg.DataPath, file.Name)
		destFile, err := os.Create(destPath)
		if err != nil {
			log.Errorf("error creating destination file: %v", err)
			return err
		}
		defer destFile.Close()

		// Copy the contents from the zip file to the destination file.
		if _, err := io.Copy(destFile, rc); err != nil {
			log.Errorf("Error copying file contents: %v", err)
			return err
		}
	}

	return nil
}

func (api *API) removeData() error {
	allPaths := []string{
		"covers",
		"documents",
		"antholume.db",
		"antholume.db-wal",
		"antholume.db-shm",
	}

	for _, name := range allPaths {
		fullPath := filepath.Join(api.cfg.DataPath, name)
		err := os.RemoveAll(fullPath)
		if err != nil {
			log.Errorf("Unable to delete %s: %v", name, err)
			return err
		}

	}

	return nil
}

func (api *API) createBackup(w io.Writer, directories []string) error {
	// Vacuum DB
	_, err := api.db.DB.ExecContext(api.db.Ctx, "VACUUM;")
	if err != nil {
		return errors.Wrap(err, "Unable to vacuum database")
	}

	ar := zip.NewWriter(w)
	exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if f.IsDir() {
			return nil
		}

		// Open File on Disk
		file, err := os.Open(currentPath)
		if err != nil {
			return err
		}
		defer file.Close()

		// Derive Export Structure
		fileName := filepath.Base(currentPath)
		folderName := filepath.Base(filepath.Dir(currentPath))

		// Create File in Export
		newF, err := ar.Create(filepath.Join(folderName, fileName))
		if err != nil {
			return err
		}

		// Copy File in Export
		_, err = io.Copy(newF, file)
		if err != nil {
			return err
		}

		return nil
	}

	// Get DB Path
	fileName := fmt.Sprintf("%s.db", api.cfg.DBName)
	dbLocation := filepath.Join(api.cfg.ConfigPath, fileName)

	// Copy Database File
	dbFile, err := os.Open(dbLocation)
	if err != nil {
		return err
	}
	defer dbFile.Close()

	newDbFile, err := ar.Create(fileName)
	if err != nil {
		return err
	}

	_, err = io.Copy(newDbFile, dbFile)
	if err != nil {
		return err
	}

	// Backup Covers & Documents
	for _, dir := range directories {
		err = filepath.WalkDir(filepath.Join(api.cfg.DataPath, dir), exportWalker)
		if err != nil {
			return err
		}
	}

	ar.Close()
	return nil
}

func (api *API) isLastAdmin(userID string) (bool, error) {
	allUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
	if err != nil {
		return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err))
	}

	hasAdmin := false
	for _, user := range allUsers {
		if user.Admin && user.ID != userID {
			hasAdmin = true
			break
		}
	}

	return !hasAdmin, nil
}

func (api *API) createUser(user string, rawPassword *string, isAdmin *bool) error {
	// Validate Necessary Parameters
	if rawPassword == nil || *rawPassword == "" {
		return fmt.Errorf("password can't be empty")
	}

	// Base Params
	createParams := database.CreateUserParams{
		ID: user,
	}

	// Handle Admin (Explicit or False)
	if isAdmin != nil {
		createParams.Admin = *isAdmin
	} else {
		createParams.Admin = false
	}

	// Parse Password
	password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
	hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
	if err != nil {
		return fmt.Errorf("unable to create hashed password")
	}
	createParams.Pass = &hashedPassword

	// Generate Auth Hash
	rawAuthHash, err := utils.GenerateToken(64)
	if err != nil {
		return fmt.Errorf("unable to create token for user")
	}
	authHash := fmt.Sprintf("%x", rawAuthHash)
	createParams.AuthHash = &authHash

	// Create user in DB
	if rows, err := api.db.Queries.CreateUser(api.db.Ctx, createParams); err != nil {
		log.Error("CreateUser DB Error:", err)
		return fmt.Errorf("unable to create user")
	} else if rows == 0 {
		log.Warn("User Already Exists:", createParams.ID)
		return fmt.Errorf("user already exists")
	}

	return nil
}

func (api *API) updateUser(user string, rawPassword *string, isAdmin *bool) error {
	// Validate Necessary Parameters
	if rawPassword == nil && isAdmin == nil {
		return fmt.Errorf("nothing to update")
	}

	// Base Params
	updateParams := database.UpdateUserParams{
		UserID: user,
	}

	// Handle Admin (Update or Existing)
	if isAdmin != nil {
		updateParams.Admin = *isAdmin
	} else {
		user, err := api.db.Queries.GetUser(api.db.Ctx, user)
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
		}
		updateParams.Admin = user.Admin
	}

	// Check Admins - Disallow Demotion
	if isLast, err := api.isLastAdmin(user); err != nil {
		return err
	} else if isLast && !updateParams.Admin {
		return fmt.Errorf("unable to demote %s - last admin", user)
	}

	// Handle Password
	if rawPassword != nil {
		if *rawPassword == "" {
			return fmt.Errorf("password can't be empty")
		}

		// Parse Password
		password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword)))
		hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
		if err != nil {
			return fmt.Errorf("unable to create hashed password")
		}
		updateParams.Password = &hashedPassword

		// Generate Auth Hash
		rawAuthHash, err := utils.GenerateToken(64)
		if err != nil {
			return fmt.Errorf("unable to create token for user")
		}
		authHash := fmt.Sprintf("%x", rawAuthHash)
		updateParams.AuthHash = &authHash
	}

	// Update User
	_, err := api.db.Queries.UpdateUser(api.db.Ctx, updateParams)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
	}

	return nil
}

func (api *API) deleteUser(user string) error {
	// Check Admins
	if isLast, err := api.isLastAdmin(user); err != nil {
		return err
	} else if isLast {
		return fmt.Errorf("unable to delete %s - last admin", user)
	}

	// Create Backup File
	backupFilePath := filepath.Join(api.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405")))
	backupFile, err := os.Create(backupFilePath)
	if err != nil {
		return err
	}
	defer backupFile.Close()

	// Save Backup File (DB Only)
	w := bufio.NewWriter(backupFile)
	err = api.createBackup(w, []string{})
	if err != nil {
		return err
	}

	// Delete User
	_, err = api.db.Queries.DeleteUser(api.db.Ctx, user)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err))
	}

	return nil
}