package handlers

import (
	"database/sql"
	"fmt"
	"io"
	"mime"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"git.elijahkuntz.com/Elijah/drive/config"
	"git.elijahkuntz.com/Elijah/drive/database"
	"git.elijahkuntz.com/Elijah/drive/workers"
	"github.com/gofiber/fiber/v2"
	"github.com/cespare/xxhash/v2"
)

type FSHandler struct {
	DB     *database.DB
	Config *config.Config
}

type FileInfo struct {
	Name      string    `json:"name"`
	Path      string    `json:"path"`
	IsDir     bool      `json:"is_dir"`
	Size      int64     `json:"size"`
	MimeType  string    `json:"mime_type"`
	IsPinned  bool      `json:"is_pinned"`
	IsTrashed bool      `json:"is_trashed"`
	ModTime   time.Time `json:"mod_time"`
	Checksum  string    `json:"checksum,omitempty"`
}

// ListDirectory returns the contents of a directory.
// GET /api/files/*
func (h *FSHandler) ListDirectory(c *fiber.Ctx) error {
	resolvedPath := c.Locals("resolvedPath").(string)

	info, err := os.Stat(resolvedPath)
	if err != nil {
		if os.IsNotExist(err) {
			return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "path not found"})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to stat path"})
	}

	// If it's a file, return its info directly
	if !info.IsDir() {
		return h.getFileInfo(c, resolvedPath)
	}

	entries, err := os.ReadDir(resolvedPath)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to read directory"})
	}

	relativePath := c.Locals("relativePath").(string)
	files := make([]FileInfo, 0, len(entries))

	for _, entry := range entries {
		// Skip hidden system directories
		if entry.Name() == ".versions" || entry.Name() == ".trash" || entry.Name() == ".backups" {
			continue
		}

		entryInfo, err := entry.Info()
		if err != nil {
			continue
		}

		entryPath := filepath.Join(relativePath, entry.Name())
		mimeType := ""
		if !entry.IsDir() {
			mimeType = mime.TypeByExtension(filepath.Ext(entry.Name()))
		}

		fi := FileInfo{
			Name:    entry.Name(),
			Path:    filepath.ToSlash(entryPath),
			IsDir:   entry.IsDir(),
			Size:    entryInfo.Size(),
			MimeType: mimeType,
			ModTime: entryInfo.ModTime(),
		}

		// Enrich with DB metadata (pinned status, etc.)
		var isPinned, isTrashed int
		err = h.DB.QueryRow(
			"SELECT is_pinned, is_trashed FROM files WHERE path = ?",
			filepath.ToSlash(entryPath),
		).Scan(&isPinned, &isTrashed)
		if err == nil {
			fi.IsPinned = isPinned == 1
			fi.IsTrashed = isTrashed == 1
		}

		// Don't show trashed items in normal listings
		if fi.IsTrashed {
			continue
		}

		files = append(files, fi)
	}

	// Sort: directories first, then alphabetically
	sort.Slice(files, func(i, j int) bool {
		if files[i].IsDir != files[j].IsDir {
			return files[i].IsDir
		}
		return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
	})

	return c.JSON(fiber.Map{
		"path":  filepath.ToSlash(relativePath),
		"files": files,
		"count": len(files),
	})
}

func (h *FSHandler) getFileInfo(c *fiber.Ctx, absPath string) error {
	info, err := os.Stat(absPath)
	if err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file not found"})
	}

	relativePath := c.Locals("relativePath").(string)

	fi := FileInfo{
		Name:     info.Name(),
		Path:     filepath.ToSlash(relativePath),
		IsDir:    info.IsDir(),
		Size:     info.Size(),
		MimeType: mime.TypeByExtension(filepath.Ext(info.Name())),
		ModTime:  info.ModTime(),
	}

	var isPinned int
	var checksum string
	err = h.DB.QueryRow(
		"SELECT is_pinned, checksum FROM files WHERE path = ?",
		filepath.ToSlash(relativePath),
	).Scan(&isPinned, &checksum)
	if err == nil {
		fi.IsPinned = isPinned == 1
		fi.Checksum = checksum
	}

	return c.JSON(fi)
}

// CreateFolder creates a new directory.
// POST /api/files/mkdir
func (h *FSHandler) CreateFolder(c *fiber.Ctx) error {
	var body struct {
		Path string `json:"path"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	if body.Path == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "path is required"})
	}

	// Validate path within jail
	fullPath, err := h.resolveSafe(body.Path)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	if err := os.MkdirAll(fullPath, 0755); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create folder"})
	}

	// Record in DB
	cleanPath := filepath.ToSlash(body.Path)
	h.DB.Exec(
		"INSERT OR IGNORE INTO files (path, name, is_dir) VALUES (?, ?, 1)",
		cleanPath, filepath.Base(cleanPath),
	)

	h.DB.AddAuditLog("folder_created", cleanPath, c.IP())

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"message": "folder created",
		"path":    cleanPath,
	})
}

// Rename renames a file or folder.
// POST /api/files/rename
func (h *FSHandler) Rename(c *fiber.Ctx) error {
	var body struct {
		OldPath string `json:"old_path"`
		NewName string `json:"new_name"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	oldFull, err := h.resolveSafe(body.OldPath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	newPath := filepath.Join(filepath.Dir(body.OldPath), body.NewName)
	newFull, err := h.resolveSafe(newPath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	if err := os.Rename(oldFull, newFull); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to rename"})
	}

	// Update DB
	h.DB.Exec(
		"UPDATE files SET path = ?, name = ?, updated_at = CURRENT_TIMESTAMP WHERE path = ?",
		filepath.ToSlash(newPath), body.NewName, filepath.ToSlash(body.OldPath),
	)

	h.DB.AddAuditLog("renamed", fmt.Sprintf("%s -> %s", body.OldPath, newPath), c.IP())

	return c.JSON(fiber.Map{"message": "renamed successfully", "new_path": filepath.ToSlash(newPath)})
}

// Move moves a file or folder to a new location.
// POST /api/files/move
func (h *FSHandler) Move(c *fiber.Ctx) error {
	var body struct {
		SourcePath string `json:"source_path"`
		DestPath   string `json:"dest_path"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	srcFull, err := h.resolveSafe(body.SourcePath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	dstFull, err := h.resolveSafe(body.DestPath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	// If destination is a directory, move into it
	if info, err := os.Stat(dstFull); err == nil && info.IsDir() {
		dstFull = filepath.Join(dstFull, filepath.Base(srcFull))
		body.DestPath = filepath.Join(body.DestPath, filepath.Base(body.SourcePath))
	}

	if err := os.Rename(srcFull, dstFull); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move"})
	}

	h.DB.Exec(
		"UPDATE files SET path = ?, updated_at = CURRENT_TIMESTAMP WHERE path = ?",
		filepath.ToSlash(body.DestPath), filepath.ToSlash(body.SourcePath),
	)

	h.DB.AddAuditLog("moved", fmt.Sprintf("%s -> %s", body.SourcePath, body.DestPath), c.IP())

	return c.JSON(fiber.Map{"message": "moved successfully"})
}

// Copy copies a file or folder.
// POST /api/files/copy
func (h *FSHandler) Copy(c *fiber.Ctx) error {
	var body struct {
		SourcePath string `json:"source_path"`
		DestPath   string `json:"dest_path"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	srcFull, err := h.resolveSafe(body.SourcePath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	dstFull, err := h.resolveSafe(body.DestPath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	info, err := os.Stat(srcFull)
	if err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "source not found"})
	}

	if info.IsDir() {
		if err := copyDir(srcFull, dstFull); err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy directory"})
		}
	} else {
		if err := copyFile(srcFull, dstFull); err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to copy file"})
		}
	}

	h.DB.AddAuditLog("copied", fmt.Sprintf("%s -> %s", body.SourcePath, body.DestPath), c.IP())

	return c.JSON(fiber.Map{"message": "copied successfully"})
}

// Delete soft-deletes a file (moves to trash).
// DELETE /api/files/*
func (h *FSHandler) Delete(c *fiber.Ctx) error {
	resolvedPath := c.Locals("resolvedPath").(string)
	relativePath := c.Locals("relativePath").(string)

	// Check if file exists
	if _, err := os.Stat(resolvedPath); os.IsNotExist(err) {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file not found"})
	}

	// Soft-delete: move to .trash directory
	trashPath := filepath.Join(h.Config.TrashDir, relativePath)
	if err := os.MkdirAll(filepath.Dir(trashPath), 0755); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to prepare trash"})
	}

	if err := os.Rename(resolvedPath, trashPath); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to move to trash"})
	}

	// Update DB
	cleanPath := filepath.ToSlash(relativePath)
	h.DB.Exec(
		`UPDATE files SET is_trashed = 1, trashed_at = CURRENT_TIMESTAMP, original_path = ? WHERE path = ?`,
		cleanPath, cleanPath,
	)

	// Also insert if not tracked
	h.DB.Exec(
		`INSERT OR IGNORE INTO files (path, name, is_trashed, trashed_at, original_path) VALUES (?, ?, 1, CURRENT_TIMESTAMP, ?)`,
		cleanPath, filepath.Base(cleanPath), cleanPath,
	)

	h.DB.AddAuditLog("deleted", cleanPath, c.IP())

	return c.JSON(fiber.Map{"message": "moved to trash"})
}

// ListTrash lists all trashed items.
// GET /api/trash
func (h *FSHandler) ListTrash(c *fiber.Ctx) error {
	rows, err := h.DB.Query(
		"SELECT path, name, is_dir, size, original_path, trashed_at FROM files WHERE is_trashed = 1 ORDER BY trashed_at DESC",
	)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}
	defer rows.Close()

	type TrashItem struct {
		Path         string    `json:"path"`
		Name         string    `json:"name"`
		IsDir        bool      `json:"is_dir"`
		Size         int64     `json:"size"`
		OriginalPath string    `json:"original_path"`
		TrashedAt    time.Time `json:"trashed_at"`
	}

	items := []TrashItem{}
	for rows.Next() {
		var item TrashItem
		var isDir int
		var trashedAt sql.NullTime
		if err := rows.Scan(&item.Path, &item.Name, &isDir, &item.Size, &item.OriginalPath, &trashedAt); err != nil {
			continue
		}
		item.IsDir = isDir == 1
		if trashedAt.Valid {
			item.TrashedAt = trashedAt.Time
		}
		items = append(items, item)
	}

	return c.JSON(fiber.Map{"items": items, "count": len(items)})
}

// RestoreFromTrash restores a file from the trash.
// POST /api/trash/restore
func (h *FSHandler) RestoreFromTrash(c *fiber.Ctx) error {
	var body struct {
		Path string `json:"path"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	// Get original path from DB
	var originalPath string
	err := h.DB.QueryRow(
		"SELECT original_path FROM files WHERE path = ? AND is_trashed = 1",
		body.Path,
	).Scan(&originalPath)
	if err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "item not found in trash"})
	}

	trashPath := filepath.Join(h.Config.TrashDir, filepath.FromSlash(body.Path))
	restorePath := filepath.Join(h.Config.StorageDir, filepath.FromSlash(originalPath))

	// Ensure parent directory exists
	if err := os.MkdirAll(filepath.Dir(restorePath), 0755); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to prepare restore location"})
	}

	if err := os.Rename(trashPath, restorePath); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to restore file"})
	}

	h.DB.Exec(
		"UPDATE files SET is_trashed = 0, trashed_at = NULL WHERE path = ?",
		body.Path,
	)

	h.DB.AddAuditLog("restored", fmt.Sprintf("Restored '%s' from trash", originalPath), c.IP())

	return c.JSON(fiber.Map{"message": "restored successfully", "path": originalPath})
}

// EmptyTrash permanently deletes all trashed items.
// DELETE /api/trash
func (h *FSHandler) EmptyTrash(c *fiber.Ctx) error {
	if err := os.RemoveAll(h.Config.TrashDir); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to empty trash"})
	}
	os.MkdirAll(h.Config.TrashDir, 0755)

	h.DB.Exec("DELETE FROM files WHERE is_trashed = 1")

	h.DB.AddAuditLog("trash_emptied", "All trashed items permanently deleted", c.IP())

	return c.JSON(fiber.Map{"message": "trash emptied"})
}

// TogglePin pins or unpins a file/folder.
// POST /api/files/pin
func (h *FSHandler) TogglePin(c *fiber.Ctx) error {
	var body struct {
		Path string `json:"path"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
	}

	cleanPath := filepath.ToSlash(body.Path)

	// Ensure the file record exists
	h.DB.Exec(
		"INSERT OR IGNORE INTO files (path, name) VALUES (?, ?)",
		cleanPath, filepath.Base(cleanPath),
	)

	// Toggle pin
	_, err := h.DB.Exec(
		"UPDATE files SET is_pinned = CASE WHEN is_pinned = 1 THEN 0 ELSE 1 END WHERE path = ?",
		cleanPath,
	)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to toggle pin"})
	}

	var isPinned int
	h.DB.QueryRow("SELECT is_pinned FROM files WHERE path = ?", cleanPath).Scan(&isPinned)

	return c.JSON(fiber.Map{"pinned": isPinned == 1, "path": cleanPath})
}

// ListPinned returns all pinned items.
// GET /api/files/pinned
func (h *FSHandler) ListPinned(c *fiber.Ctx) error {
	rows, err := h.DB.Query(
		"SELECT path, name, is_dir, size, mime_type FROM files WHERE is_pinned = 1 AND is_trashed = 0",
	)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
	}
	defer rows.Close()

	items := []FileInfo{}
	for rows.Next() {
		var fi FileInfo
		var isDir int
		if err := rows.Scan(&fi.Path, &fi.Name, &isDir, &fi.Size, &fi.MimeType); err != nil {
			continue
		}
		fi.IsDir = isDir == 1
		fi.IsPinned = true
		items = append(items, fi)
	}

	return c.JSON(fiber.Map{"items": items, "count": len(items)})
}

// Search performs an FTS5 full-text search on file names and paths.
// GET /api/search?q=query
func (h *FSHandler) Search(c *fiber.Ctx) error {
	query := c.Query("q", "")
	if query == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "query parameter 'q' is required"})
	}

	// Append wildcard for prefix matching
	ftsQuery := query + "*"

	rows, err := h.DB.Query(`
		SELECT f.path, f.name, f.is_dir, f.size, f.mime_type, f.is_pinned
		FROM files f
		JOIN files_fts fts ON f.id = fts.rowid
		WHERE files_fts MATCH ? AND f.is_trashed = 0
		ORDER BY rank
		LIMIT 50
	`, ftsQuery)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "search failed"})
	}
	defer rows.Close()

	results := []FileInfo{}
	for rows.Next() {
		var fi FileInfo
		var isDir, isPinned int
		if err := rows.Scan(&fi.Path, &fi.Name, &isDir, &fi.Size, &fi.MimeType, &isPinned); err != nil {
			continue
		}
		fi.IsDir = isDir == 1
		fi.IsPinned = isPinned == 1
		results = append(results, fi)
	}

	return c.JSON(fiber.Map{"results": results, "count": len(results)})
}

// Download serves a file with full HTTP Range Request support.
// GET /api/files/download/*
func (h *FSHandler) Download(c *fiber.Ctx) error {
	resolvedPath := c.Locals("resolvedPath").(string)

	info, err := os.Stat(resolvedPath)
	if err != nil || info.IsDir() {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "file not found"})
	}

	// Verify checksum on download if stored
	relativePath := c.Locals("relativePath").(string)
	var storedChecksum string
	h.DB.QueryRow("SELECT checksum FROM files WHERE path = ?", filepath.ToSlash(relativePath)).Scan(&storedChecksum)

	// Set appropriate headers for range requests
	c.Set("Accept-Ranges", "bytes")
	c.Set("Content-Type", mime.TypeByExtension(filepath.Ext(resolvedPath)))

	// Use Fiber's built-in SendFile which supports Range requests
	return c.SendFile(resolvedPath)
}

// Upload handles file upload and generates an XXHash checksum.
// POST /api/files/upload
func (h *FSHandler) Upload(c *fiber.Ctx) error {
	destPath := c.FormValue("path", "")
	if destPath == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "destination path is required"})
	}

	file, err := c.FormFile("file")
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "file is required"})
	}

	fullDest, err := h.resolveSafe(destPath)
	if err != nil {
		return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": err.Error()})
	}

	// If destination is a directory, save into it
	if info, err := os.Stat(fullDest); err == nil && info.IsDir() {
		fullDest = filepath.Join(fullDest, file.Filename)
		destPath = filepath.Join(destPath, file.Filename)
	}

	// Ensure parent directory exists
	if err := os.MkdirAll(filepath.Dir(fullDest), 0755); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create directory"})
	}

	// Version existing file if it exists
	CreateVersion(h.DB, h.Config, fullDest, filepath.ToSlash(destPath))

	// Save the file
	if err := c.SaveFile(file, fullDest); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save file"})
	}

	// Generate XXHash checksum
	checksum, err := generateChecksum(fullDest)
	if err != nil {
		// File saved but checksum failed — log but don't fail the upload
		fmt.Printf("Warning: checksum generation failed for %s: %v\n", destPath, err)
	}

	cleanPath := filepath.ToSlash(destPath)
	mimeType := mime.TypeByExtension(filepath.Ext(file.Filename))

	// Upsert file record
	h.DB.Exec(`
		INSERT INTO files (path, name, size, mime_type, checksum)
		VALUES (?, ?, ?, ?, ?)
		ON CONFLICT(path) DO UPDATE SET
			size = excluded.size,
			mime_type = excluded.mime_type,
			checksum = excluded.checksum,
			updated_at = CURRENT_TIMESTAMP
	`, cleanPath, file.Filename, file.Size, mimeType, checksum)

	h.DB.AddAuditLog("uploaded", fmt.Sprintf("%s (%d bytes)", cleanPath, file.Size), c.IP())

	// Queue thumbnail generation
	if workers.Instance != nil {
		workers.Instance.Enqueue(workers.ThumbnailJob{
			FilePath: fullDest,
			Checksum: checksum,
			MimeType: mimeType,
		})
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"message":  "uploaded successfully",
		"path":     cleanPath,
		"checksum": checksum,
		"size":     file.Size,
	})
}

// StorageDashboard returns storage usage broken down by file type.
// GET /api/storage/dashboard
func (h *FSHandler) StorageDashboard(c *fiber.Ctx) error {
	type CategoryUsage struct {
		Category string `json:"category"`
		Size     int64  `json:"size"`
		Count    int    `json:"count"`
	}

	// Walk the storage directory to compute real usage
	categories := map[string]*CategoryUsage{
		"images":    {Category: "images"},
		"videos":    {Category: "videos"},
		"audio":     {Category: "audio"},
		"documents": {Category: "documents"},
		"archives":  {Category: "archives"},
		"other":     {Category: "other"},
	}

	var totalSize int64

	filepath.Walk(h.Config.StorageDir, func(path string, info os.FileInfo, err error) error {
		if err != nil || info.IsDir() {
			return nil
		}
		// Skip system dirs
		rel, _ := filepath.Rel(h.Config.StorageDir, path)
		if strings.HasPrefix(rel, ".trash") || strings.HasPrefix(rel, ".versions") || strings.HasPrefix(rel, ".backups") {
			return nil
		}

		ext := strings.ToLower(filepath.Ext(info.Name()))
		cat := categorizeFile(ext)
		categories[cat].Size += info.Size()
		categories[cat].Count++
		totalSize += info.Size()
		return nil
	})

	result := make([]CategoryUsage, 0, len(categories))
	for _, cat := range categories {
		if cat.Count > 0 {
			result = append(result, *cat)
		}
	}

	return c.JSON(fiber.Map{
		"total_size":  totalSize,
		"categories":  result,
	})
}

// --- Helper functions ---

func (h *FSHandler) resolveSafe(relativePath string) (string, error) {
	cleaned := filepath.Clean(relativePath)
	if strings.Contains(cleaned, "..") {
		return "", fmt.Errorf("path traversal detected")
	}

	full := filepath.Join(h.Config.StorageDir, cleaned)
	abs, err := filepath.Abs(full)
	if err != nil {
		return "", fmt.Errorf("invalid path")
	}

	absRoot, _ := filepath.Abs(h.Config.StorageDir)
	if abs != absRoot && !strings.HasPrefix(abs, absRoot+string(filepath.Separator)) {
		return "", fmt.Errorf("access denied")
	}

	return abs, nil
}

func generateChecksum(filePath string) (string, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer f.Close()

	h := xxhash.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}

	return fmt.Sprintf("%016x", h.Sum64()), nil
}

func copyFile(src, dst string) error {
	if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
		return err
	}

	sourceFile, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sourceFile.Close()

	destFile, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer destFile.Close()

	_, err = io.Copy(destFile, sourceFile)
	return err
}

func copyDir(src, dst string) error {
	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		rel, _ := filepath.Rel(src, path)
		target := filepath.Join(dst, rel)

		if info.IsDir() {
			return os.MkdirAll(target, info.Mode())
		}

		return copyFile(path, target)
	})
}

func categorizeFile(ext string) string {
	switch ext {
	case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tiff":
		return "images"
	case ".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".m4v":
		return "videos"
	case ".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a":
		return "audio"
	case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".md", ".csv":
		return "documents"
	case ".zip", ".tar", ".gz", ".rar", ".7z", ".bz2":
		return "archives"
	default:
		return "other"
	}
}

// ServeThumbnail serves a generated thumbnail image.
// GET /api/files/thumbnail/:checksum
func (h *FSHandler) ServeThumbnail(c *fiber.Ctx) error {
	checksum := c.Params("checksum")
	if checksum == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "missing checksum"})
	}

	thumbPath := filepath.Join(h.Config.ThumbnailDir, checksum+".jpg")
	
	if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
		return c.SendStatus(fiber.StatusNotFound)
	}

	// Set caching headers for thumbnails (they don't change because checksums are immutable)
	c.Set("Cache-Control", "public, max-age=31536000, immutable")
	
	return c.SendFile(thumbPath)
}
