package http

import (
	"archive/zip"
	"bytes"
	"context"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"go.b0esche.cloud/backend/internal/audit"
	"go.b0esche.cloud/backend/internal/auth"
	"go.b0esche.cloud/backend/internal/config"
	"go.b0esche.cloud/backend/internal/database"
	"go.b0esche.cloud/backend/internal/errors"
	"go.b0esche.cloud/backend/internal/middleware"
	"go.b0esche.cloud/backend/internal/org"
	"go.b0esche.cloud/backend/internal/permission"
	"go.b0esche.cloud/backend/pkg/jwt"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"go.b0esche.cloud/backend/internal/storage"
)

// sanitizePath validates and sanitizes a file path to prevent path traversal attacks.
// Returns the cleaned path or an error if the path is invalid.
func sanitizePath(inputPath string) (string, error) {
	// Clean the path to resolve . and ..
	cleaned := path.Clean(inputPath)

	// Ensure the path doesn't try to escape the root
	if strings.Contains(cleaned, "..") {
		return "", fmt.Errorf("invalid path: path traversal detected")
	}

	// Ensure path starts with /
	if !strings.HasPrefix(cleaned, "/") {
		cleaned = "/" + cleaned
	}

	return cleaned, nil
}

// Avatar cache helpers
// avatarCachePath builds a path for the avatar cache file for the given user and version (if provided).
func avatarCachePath(cfg *config.Config, userID string, version string, ext string) string {
	dir := cfg.AvatarCacheDir
	if dir == "" {
		dir = "/var/cache/b0esche/avatars"
	}
	os.MkdirAll(dir, 0755)
	// Filename: <userID>.<version><ext>  (if version empty, use <userID><ext>)
	if version != "" {
		return filepath.Join(dir, fmt.Sprintf("%s.%s%s", userID, version, ext))
	}
	return filepath.Join(dir, fmt.Sprintf("%s%s", userID, ext))
}

// writeAvatarCache saves avatar bytes keyed by userID and version (best effort). If version is empty, writes a file without version suffix.
func writeAvatarCache(cfg *config.Config, userID string, version string, ext string, data []byte) error {
	dir := cfg.AvatarCacheDir
	if dir == "" {
		dir = "/var/cache/b0esche/avatars"
	}
	// Try to create the directory; if it fails, fall back to temp dir
	if err := os.MkdirAll(dir, 0755); err != nil {
		fmt.Printf("[WARN] failed to create avatar cache dir %s: %v; trying fallback to tmp dir\n", dir, err)
		fallback := filepath.Join(os.TempDir(), "b0esche_avatars")
		if err2 := os.MkdirAll(fallback, 0755); err2 != nil {
			return fmt.Errorf("failed to create avatar cache dir: %v; fallback failed: %v", err, err2)
		}
		dir = fallback
	}
	p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext)
	if err := os.WriteFile(p, data, 0644); err != nil {
		return err
	}
	fmt.Printf("[INFO] Wrote avatar cache for user=%s v=%s path=%s size=%d\n", userID, version, p, len(data))
	return nil
}

// readAvatarCache attempts to read a cached avatar for a user and optional version. If version is provided, it looks for an exact match; otherwise it returns the latest available cached avatar (if any).
func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte, string, error) {
	checkDirs := []string{cfg.AvatarCacheDir}
	if checkDirs[0] == "" {
		checkDirs[0] = "/var/cache/b0esche/avatars"
	}
	// Also check fallback tmp dir
	checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars"))

	fmt.Printf("[DEBUG] readAvatarCache checking dirs=%v for user=%s version=%s\n", checkDirs, userID, version)

	for _, dir := range checkDirs {
		entries, err := os.ReadDir(dir)
		if err != nil {
			fmt.Printf("[DEBUG] readAvatarCache cannot read dir %s: %v\n", dir, err)
			continue
		}
		if version != "" {
			// look for exact match: userID.version.*
			prefix := fmt.Sprintf("%s.%s", userID, version)
			for _, e := range entries {
				if strings.HasPrefix(e.Name(), prefix) {
					p := filepath.Join(dir, e.Name())
					fmt.Printf("[INFO] Found cached avatar for user=%s v=%s path=%s\n", userID, version, p)
					b, err := os.ReadFile(p)
					if err != nil {
						fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
						return nil, "", err
					}
					ext := filepath.Ext(e.Name())
					ct := mime.TypeByExtension(ext)
					if ct == "" {
						ct = "application/octet-stream"
					}
					return b, ct, nil
				}
			}
			// not found
			fmt.Printf("[DEBUG] no exact cached avatar match in %s for prefix=%s\n", dir, prefix)
			continue
		}
		// no version specified: return latest by modtime of files starting with userID.
		var latest os.FileInfo
		var latestName string
		for _, e := range entries {
			if strings.HasPrefix(e.Name(), userID+".") {
				fi, err := e.Info()
				if err != nil {
					continue
				}
				if latest == nil || fi.ModTime().After(latest.ModTime()) {
					latest = fi
					latestName = e.Name()
				}
			}
		}
		if latest != nil {
			p := filepath.Join(dir, latestName)
			fmt.Printf("[INFO] Found latest cached avatar for user=%s path=%s\n", userID, p)
			b, err := os.ReadFile(p)
			if err != nil {
				fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err)
				return nil, "", err
			}
			ext := filepath.Ext(latestName)
			ct := mime.TypeByExtension(ext)
			if ct == "" {
				ct = "application/octet-stream"
			}
			return b, ct, nil
		}
	}
	return nil, "", fmt.Errorf("cache miss")
}

// getUserWebDAVClient gets or creates a user's Nextcloud account and returns a WebDAV client for them
func getUserWebDAVClient(ctx context.Context, db *database.DB, userID uuid.UUID, nextcloudBaseURL, adminUser, adminPass string) (*storage.WebDAVClient, error) {
	var user struct {
		Username          string
		NextcloudUsername string
		NextcloudPassword string
	}

	err := db.QueryRowContext(ctx,
		"SELECT username, COALESCE(nextcloud_username, ''), COALESCE(nextcloud_password, '') FROM users WHERE id = $1",
		userID).Scan(&user.Username, &user.NextcloudUsername, &user.NextcloudPassword)
	if err != nil {
		return nil, fmt.Errorf("failed to get user: %w", err)
	}

	// If user doesn't have Nextcloud credentials, create them
	if user.NextcloudUsername == "" || user.NextcloudPassword == "" {
		// Use the actual username from the users table
		ncUsername := user.Username
		ncPassword, err := storage.GenerateSecurePassword(32)
		if err != nil {
			return nil, fmt.Errorf("failed to generate password: %w", err)
		}

		// Create Nextcloud user account
		err = storage.CreateNextcloudUser(nextcloudBaseURL, adminUser, adminPass, ncUsername, ncPassword)
		if err != nil {
			return nil, fmt.Errorf("failed to create Nextcloud user: %w", err)
		}

		// Update database with Nextcloud credentials
		_, err = db.ExecContext(ctx,
			"UPDATE users SET nextcloud_username = $1, nextcloud_password = $2 WHERE id = $3",
			ncUsername, ncPassword, userID)
		if err != nil {
			return nil, fmt.Errorf("failed to update user credentials: %w", err)
		}

		user.NextcloudUsername = ncUsername
		user.NextcloudPassword = ncPassword
		log.Printf("[AUTO-PROVISION] Created Nextcloud account for user %s: %s\n", userID, ncUsername)
	}

	// Create user-specific WebDAV client
	return storage.NewUserWebDAVClient(nextcloudBaseURL, user.NextcloudUsername, user.NextcloudPassword), nil
}

func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, authService *auth.Service, auditLogger *audit.Logger) http.Handler {
	r := chi.NewRouter()

	// Global middleware
	r.Use(middleware.RequestID)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(middleware.CORS(cfg.AllowedOrigins))
	r.Use(middleware.SecurityHeaders())
	r.Use(middleware.RateLimit)

	// Health check
	r.Get("/health", healthHandler)

	// Join org by invite token (public)
	r.Get("/join", func(w http.ResponseWriter, req *http.Request) {
		getOrgByInviteTokenHandler(w, req, db)
	})

	// WOPI routes (public, token validation done per endpoint)
	r.Route("/wopi", func(r chi.Router) {
		r.Route("/files/{fileId}", func(r chi.Router) {
			// CheckFileInfo: GET /wopi/files/{fileId}
			r.Get("/", func(w http.ResponseWriter, req *http.Request) {
				wopiCheckFileInfoHandler(w, req, db, jwtManager)
			})
			// GetFile: GET /wopi/files/{fileId}/contents
			r.Get("/contents", func(w http.ResponseWriter, req *http.Request) {
				wopiGetFileHandler(w, req, db, jwtManager, cfg)
			})
			// PutFile & Lock operations: POST /wopi/files/{fileId}/contents and POST /wopi/files/{fileId}
			r.Post("/contents", func(w http.ResponseWriter, req *http.Request) {
				wopiPutFileHandler(w, req, db, jwtManager, cfg)
			})
			// Lock operations: POST /wopi/files/{fileId}
			r.Post("/", func(w http.ResponseWriter, req *http.Request) {
				wopiLockHandler(w, req, db, jwtManager)
			})
		})
	})

	// Auth routes (no auth required)
	r.Route("/auth", func(r chi.Router) {
		r.Post("/refresh", func(w http.ResponseWriter, req *http.Request) {
			refreshHandler(w, req, jwtManager, db)
		})
		r.Post("/logout", func(w http.ResponseWriter, req *http.Request) {
			logoutHandler(w, req, jwtManager, db, auditLogger)
		})
		// Passkey routes
		r.Post("/signup", func(w http.ResponseWriter, req *http.Request) {
			signupHandler(w, req, db, auditLogger)
		})
		r.Post("/registration-challenge", func(w http.ResponseWriter, req *http.Request) {
			registrationChallengeHandler(w, req, db)
		})
		r.Post("/registration-verify", func(w http.ResponseWriter, req *http.Request) {
			registrationVerifyHandler(w, req, db, jwtManager, auditLogger)
		})
		r.Post("/authentication-challenge", func(w http.ResponseWriter, req *http.Request) {
			authenticationChallengeHandler(w, req, db)
		})
		r.Post("/authentication-verify", func(w http.ResponseWriter, req *http.Request) {
			authenticationVerifyHandler(w, req, db, jwtManager, auditLogger)
		})
		// Password login route
		r.Post("/password-login", func(w http.ResponseWriter, req *http.Request) {
			passwordLoginHandler(w, req, db, jwtManager, auditLogger)
		})
	})

	// Protected routes (with auth middleware)
	r.Route("/", func(r chi.Router) {
		r.Use(middleware.Auth(jwtManager, db))

		// User-scoped routes (personal workspace)
		r.Get("/user/files", func(w http.ResponseWriter, req *http.Request) {
			userFilesHandler(w, req, db)
		})
		// User file viewer
		r.Get("/user/files/{fileId}/view", func(w http.ResponseWriter, req *http.Request) {
			userViewerHandler(w, req, db, jwtManager, auditLogger)
		})
		// Download user file
		r.Get("/user/files/download", func(w http.ResponseWriter, req *http.Request) {
			downloadUserFileHandler(w, req, db, cfg)
		})
		// Create / delete in user workspace
		r.Post("/user/files", func(w http.ResponseWriter, req *http.Request) {
			createUserFileHandler(w, req, db, auditLogger, cfg)
		})
		r.Delete("/user/files", func(w http.ResponseWriter, req *http.Request) {
			deleteUserFileHandler(w, req, db, auditLogger, cfg)
		})
		// POST wrapper for delete
		r.Post("/user/files/delete", func(w http.ResponseWriter, req *http.Request) {
			deleteUserFilePostHandler(w, req, db, auditLogger, cfg)
		})
		// Move file/folder in user workspace
		r.Post("/user/files/move", func(w http.ResponseWriter, req *http.Request) {
			moveUserFileHandler(w, req, db, auditLogger, cfg)
		})
		// WOPI session for user files
		r.Post("/user/files/{fileId}/wopi-session", func(w http.ResponseWriter, req *http.Request) {
			wopiSessionHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
		})
		// User file editor
		r.Get("/user/files/{fileId}/edit", func(w http.ResponseWriter, req *http.Request) {
			userEditorHandler(w, req, db, auditLogger)
		})
		// Collabora form proxy for user files
		r.Get("/user/files/{fileId}/collabora-proxy", func(w http.ResponseWriter, req *http.Request) {
			collaboraProxyHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
		})
		// Share link management for user files
		r.Get("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			getUserFileShareLinkHandler(w, req, db)
		})
		r.Post("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			createUserFileShareLinkHandler(w, req, db)
		})
		r.Delete("/user/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			revokeUserFileShareLinkHandler(w, req, db)
		})

		// Share link management for personal files (alternative path for frontend compatibility)
		r.Get("/orgs/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			getUserFileShareLinkHandler(w, req, db)
		})
		r.Post("/orgs/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			createUserFileShareLinkHandler(w, req, db)
		})
		r.Delete("/orgs/files/{fileId}/share", func(w http.ResponseWriter, req *http.Request) {
			revokeUserFileShareLinkHandler(w, req, db)
		})

		// User profile routes
		r.Get("/user/profile", func(w http.ResponseWriter, req *http.Request) {
			getUserProfileHandler(w, req, db)
		})
		r.Put("/user/profile", func(w http.ResponseWriter, req *http.Request) {
			updateUserProfileHandler(w, req, db, auditLogger)
		})
		r.Options("/user/profile", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			w.WriteHeader(http.StatusOK)
		})
		r.Post("/user/change-password", func(w http.ResponseWriter, req *http.Request) {
			changePasswordHandler(w, req, db, auditLogger)
		})
		r.Options("/user/change-password", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			w.WriteHeader(http.StatusOK)
		})
		r.Post("/user/avatar", func(w http.ResponseWriter, req *http.Request) {
			uploadUserAvatarHandler(w, req, db, auditLogger, cfg)
		})
		r.Get("/user/avatar", func(w http.ResponseWriter, req *http.Request) {
			getUserAvatarHandler(w, req, db, jwtManager, cfg)
		})
		r.Options("/user/avatar", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			w.WriteHeader(http.StatusOK)
		})
		r.Delete("/user/account", func(w http.ResponseWriter, req *http.Request) {
			deleteUserAccountHandler(w, req, db, auditLogger, cfg)
		})
		r.Options("/user/account", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			w.WriteHeader(http.StatusOK)
		})

		// Org routes
		r.Get("/orgs", func(w http.ResponseWriter, req *http.Request) {
			listOrgsHandler(w, req, db)
		})
		r.Post("/orgs", func(w http.ResponseWriter, req *http.Request) {
			createOrgHandler(w, req, db, auditLogger)
		})

		// Org-scoped routes
		r.Route("/orgs/{orgId}", func(r chi.Router) {
			r.Use(middleware.Org(db, auditLogger))

			// File routes
			r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files", func(w http.ResponseWriter, req *http.Request) {
				listFilesHandler(w, req, db)
			})
			// Download org file
			r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/files/download", func(w http.ResponseWriter, req *http.Request) {
				downloadOrgFileHandler(w, req, db, cfg)
			})

			// Create file/folder in org workspace
			r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files", func(w http.ResponseWriter, req *http.Request) {
				createOrgFileHandler(w, req, db, auditLogger, cfg)
			})
			// Also accept POST delete for clients that cannot send DELETE with body
			r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files/delete", func(w http.ResponseWriter, req *http.Request) {
				deleteOrgFilePostHandler(w, req, db, auditLogger, cfg)
			})

			// Delete file/folder in org workspace (body: {"path":"/path"})
			r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Delete("/files", func(w http.ResponseWriter, req *http.Request) {
				deleteOrgFileHandler(w, req, db, auditLogger, cfg)
			})
			// Move file/folder in org workspace (body: {"sourcePath":"/old", "targetPath":"/new"})
			r.With(middleware.Permission(db, auditLogger, permission.FileWrite)).Post("/files/move", func(w http.ResponseWriter, req *http.Request) {
				moveOrgFileHandler(w, req, db, auditLogger, cfg)
			})
			r.Route("/files/{fileId}", func(r chi.Router) {
				r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/view", func(w http.ResponseWriter, req *http.Request) {
					viewerHandler(w, req, db, jwtManager, auditLogger)
				})
				r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Get("/edit", func(w http.ResponseWriter, req *http.Request) {
					editorHandler(w, req, db, auditLogger)
				})
				r.With(middleware.Permission(db, auditLogger, permission.DocumentEdit)).Post("/annotations", func(w http.ResponseWriter, req *http.Request) {
					annotationsHandler(w, req, db, auditLogger)
				})
				r.Get("/meta", func(w http.ResponseWriter, req *http.Request) {
					fileMetaHandler(w, req)
				})
				// Share link management
				r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Get("/share", func(w http.ResponseWriter, req *http.Request) {
					getFileShareLinkHandler(w, req, db)
				})
				r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Post("/share", func(w http.ResponseWriter, req *http.Request) {
					createFileShareLinkHandler(w, req, db)
				})
				r.With(middleware.Permission(db, auditLogger, permission.FileRead)).Delete("/share", func(w http.ResponseWriter, req *http.Request) {
					revokeFileShareLinkHandler(w, req, db)
				})
				// WOPI session for org files
				r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Post("/wopi-session", func(w http.ResponseWriter, req *http.Request) {
					wopiSessionHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
				})
				// Collabora form proxy for org files
				r.With(middleware.Permission(db, auditLogger, permission.DocumentView)).Get("/collabora-proxy", func(w http.ResponseWriter, req *http.Request) {
					collaboraProxyHandler(w, req, db, jwtManager, "https://of.b0esche.cloud")
				})
			})
			r.Get("/activity", func(w http.ResponseWriter, req *http.Request) {
				activityHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/members", func(w http.ResponseWriter, req *http.Request) {
				listMembersHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Patch("/members/{userId}", func(w http.ResponseWriter, req *http.Request) {
				updateMemberRoleHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Delete("/members/{userId}", func(w http.ResponseWriter, req *http.Request) {
				removeMemberHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/users/search", func(w http.ResponseWriter, req *http.Request) {
				searchUsersHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Post("/invitations", func(w http.ResponseWriter, req *http.Request) {
				createInvitationHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/invitations", func(w http.ResponseWriter, req *http.Request) {
				listInvitationsHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Delete("/invitations/{invitationId}", func(w http.ResponseWriter, req *http.Request) {
				cancelInvitationHandler(w, req, db, auditLogger)
			})
			r.Post("/join-requests", func(w http.ResponseWriter, req *http.Request) {
				createJoinRequestHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/join-requests", func(w http.ResponseWriter, req *http.Request) {
				listJoinRequestsHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Post("/join-requests/{requestId}/accept", func(w http.ResponseWriter, req *http.Request) {
				acceptJoinRequestHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Post("/join-requests/{requestId}/reject", func(w http.ResponseWriter, req *http.Request) {
				rejectJoinRequestHandler(w, req, db, auditLogger)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Get("/invite-link", func(w http.ResponseWriter, req *http.Request) {
				getInviteLinkHandler(w, req, db)
			})
			r.With(middleware.Permission(db, auditLogger, permission.OrgManage)).Post("/invite-link/regenerate", func(w http.ResponseWriter, req *http.Request) {
				regenerateInviteLinkHandler(w, req, db, auditLogger)
			})
			r.Get("/permissions", func(w http.ResponseWriter, req *http.Request) {
				getPermissionsHandler(w, req, db)
			})
		})
	}) // Close protected routes

	// Public routes (no auth required)
	r.Route("/public", func(r chi.Router) {
		r.Get("/share/{token}", func(w http.ResponseWriter, req *http.Request) {
			publicFileShareHandler(w, req, db, jwtManager)
		})
		r.Options("/share/{token}", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Range")
			w.WriteHeader(http.StatusOK)
		})
		r.Get("/share/{token}/download", func(w http.ResponseWriter, req *http.Request) {
			publicFileDownloadHandler(w, req, db, cfg, jwtManager)
		})
		r.Options("/share/{token}/download", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Range")
			w.WriteHeader(http.StatusOK)
		})
		r.Get("/share/{token}/view", func(w http.ResponseWriter, req *http.Request) {
			publicFileViewHandler(w, req, db, cfg, jwtManager)
		})
		r.Options("/share/{token}/view", func(w http.ResponseWriter, req *http.Request) {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Range")
			w.WriteHeader(http.StatusOK)
		})
		// Public WOPI routes for shared files
		r.Route("/wopi/share/{token}", func(r chi.Router) {
			r.Get("/", func(w http.ResponseWriter, req *http.Request) {
				publicWopiCheckFileInfoHandler(w, req, db, jwtManager)
			})
			r.Get("/contents", func(w http.ResponseWriter, req *http.Request) {
				publicWopiGetFileHandler(w, req, db, cfg, jwtManager)
			})
		})
	})

	return r
}

func getOrgByInviteTokenHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	token := r.URL.Query().Get("token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	var org database.Organization
	err := db.QueryRowContext(r.Context(), `
		SELECT id, owner_id, name, slug, created_at
		FROM organizations
		WHERE invite_link_token = $1
	`, token).Scan(&org.ID, &org.OwnerID, &org.Name, &org.Slug, &org.CreatedAt)
	if err != nil {
		errors.LogError(r, err, "Invalid invite token")
		errors.WriteError(w, errors.CodeNotFound, "Invalid token", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(org)
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

func refreshHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manager, db *database.DB) {
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, "Bearer ") {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	tokenString := strings.TrimPrefix(authHeader, "Bearer ")

	claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
	if err != nil {
		errors.LogError(r, err, "Invalid token")
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, _ := uuid.Parse(claims.UserID)
	orgs, err := db.GetUserOrganizations(r.Context(), userID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user organizations")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	orgIDs := make([]string, len(orgs))
	for i, o := range orgs {
		orgIDs[i] = o.ID.String()
	}

	newToken, err := jwtManager.Generate(claims.UserID, orgIDs, session.ID.String())
	if err != nil {
		errors.LogError(r, err, "Token generation failed")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(`{"token": "` + newToken + `"}`))
}

func logoutHandler(w http.ResponseWriter, r *http.Request, jwtManager *jwt.Manager, db *database.DB, auditLogger *audit.Logger) {
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, "Bearer ") {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	tokenString := strings.TrimPrefix(authHeader, "Bearer ")

	claims, session, err := jwtManager.ValidateWithSession(r.Context(), tokenString, db)
	if err != nil {
		// Token invalid or session already revoked/expired — still return success
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status": "ok"}`))
		return
	}

	userID, _ := uuid.Parse(claims.UserID)

	// Revoke session
	if err := db.RevokeSession(r.Context(), session.ID); err != nil {
		errors.LogError(r, err, "Failed to revoke session")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "logout",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func listOrgsHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	// User ID is already set by Auth middleware
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, _ := uuid.Parse(userIDStr)
	orgs, err := org.ResolveUserOrgs(r.Context(), db, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to resolve user orgs")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(orgs)
}

func createOrgHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	// User ID is already set by Auth middleware
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, _ := uuid.Parse(userIDStr)

	var req struct {
		Name string `json:"name"`
		Slug string `json:"slug,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	org, err := org.CreateOrg(r.Context(), db, userID, req.Name, req.Slug)
	if err != nil {
		errors.LogError(r, err, "Failed to create org")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		OrgID:   &org.ID,
		Action:  "create_org",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(org)
}

func listFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	// Org ID is provided by middleware.Org
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.LogError(r, err, "Invalid user id in context")
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	// Query params: path, q (search), page, pageSize
	path := r.URL.Query().Get("path")
	if path == "" {
		path = "/"
	}
	path, err = sanitizePath(path)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
		return
	}
	q := r.URL.Query().Get("q")
	page := 1
	pageSize := 100
	if p := r.URL.Query().Get("page"); p != "" {
		fmt.Sscanf(p, "%d", &page)
	}
	if ps := r.URL.Query().Get("pageSize"); ps != "" {
		fmt.Sscanf(ps, "%d", &pageSize)
	}

	files, err := db.GetOrgFiles(r.Context(), orgID, userID, path, q, page, pageSize)
	if err != nil {
		errors.LogError(r, err, "Failed to get org files")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Convert to a JSON-friendly shape expected by frontend
	out := make([]map[string]interface{}, 0, len(files))
	for _, f := range files {
		out = append(out, map[string]interface{}{
			"id":           f.ID.String(),
			"name":         f.Name,
			"path":         f.Path,
			"type":         f.Type,
			"size":         f.Size,
			"lastModified": f.LastModified.UTC().Format(time.RFC3339),
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(out)
}

func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	sessionObj, _ := middleware.GetSession(r.Context())
	fileId := chi.URLParam(r, "fileId")

	// Get file metadata to determine path and type
	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file metadata")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Check if it's a folder - cannot view folders
	if file.Type == "folder" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Cannot view folders", http.StatusBadRequest)
		return
	}

	// Log activity
	db.LogActivity(r.Context(), userID, orgID, &fileId, "view_file", map[string]interface{}{})

	// Build download URL with proper URL encoding using the request's scheme and host
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := r.Host
	if host == "" {
		host = "go.b0esche.cloud"
	}
	// Generate a long-lived token specifically for this viewer session (24 hours)
	orgs, err := db.GetUserOrganizations(r.Context(), userID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user organizations")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	orgIDs := make([]string, len(orgs))
	for i, o := range orgs {
		orgIDs[i] = o.ID.String()
	}
	viewerToken, err := jwtManager.GenerateWithDuration(userID.String(), orgIDs, sessionObj.ID.String(), 24*time.Hour)
	if err != nil {
		errors.LogError(r, err, "Failed to generate viewer token")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	downloadPath := fmt.Sprintf("%s://%s/orgs/%s/files/download?path=%s&token=%s", scheme, host, orgID.String(), url.QueryEscape(file.Path), url.QueryEscape(viewerToken))

	// Determine file type based on extension
	isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
	mimeType := getMimeType(file.Name)

	viewerSession := struct {
		ViewUrl      string `json:"viewUrl"`
		Token        string `json:"token"`
		Capabilities struct {
			CanEdit     bool   `json:"canEdit"`
			CanAnnotate bool   `json:"canAnnotate"`
			IsPdf       bool   `json:"isPdf"`
			MimeType    string `json:"mimeType"`
		} `json:"capabilities"`
		FileInfo struct {
			Name           string `json:"name"`
			Size           int64  `json:"size"`
			LastModified   string `json:"lastModified"`
			ModifiedByName string `json:"modifiedByName"`
		} `json:"fileInfo"`
		ExpiresAt string `json:"expiresAt"`
	}{
		ViewUrl: downloadPath,
		Token:   viewerToken, // Long-lived JWT token for authenticating file download
		Capabilities: struct {
			CanEdit     bool   `json:"canEdit"`
			CanAnnotate bool   `json:"canAnnotate"`
			IsPdf       bool   `json:"isPdf"`
			MimeType    string `json:"mimeType"`
		}{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf, MimeType: mimeType},
		FileInfo: struct {
			Name           string `json:"name"`
			Size           int64  `json:"size"`
			LastModified   string `json:"lastModified"`
			ModifiedByName string `json:"modifiedByName"`
		}{
			Name:           file.Name,
			Size:           file.Size,
			LastModified:   file.LastModified.UTC().Format(time.RFC3339),
			ModifiedByName: file.ModifiedByName,
		},
		ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
	}

	fmt.Printf("[VIEWER-SESSION] orgId=%s, fileId=%s, token_included=yes, isPdf=%v, mimeType=%s\n", orgID.String(), fileId, isPdf, mimeType)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(viewerSession)
}

// userViewerHandler serves a viewer session for personal workspace files
func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	sessionObj, _ := middleware.GetSession(r.Context())
	fileId := chi.URLParam(r, "fileId")

	// Get file metadata to determine path and type
	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file metadata")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Check if it's a folder - cannot view folders
	if file.Type == "folder" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Cannot view folders", http.StatusBadRequest)
		return
	}

	// Optionally log activity without org id
	db.LogActivity(r.Context(), userID, uuid.Nil, &fileId, "view_user_file", map[string]interface{}{})

	// Build download URL with proper URL encoding using the request's scheme and host
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := r.Host
	if host == "" {
		host = "go.b0esche.cloud"
	}
	// Generate a long-lived token specifically for this viewer session (24 hours)
	orgs, err := db.GetUserOrganizations(r.Context(), userID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user organizations")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	orgIDs := make([]string, len(orgs))
	for i, o := range orgs {
		orgIDs[i] = o.ID.String()
	}
	viewerToken, err := jwtManager.GenerateWithDuration(userID.String(), orgIDs, sessionObj.ID.String(), 24*time.Hour)
	if err != nil {
		errors.LogError(r, err, "Failed to generate viewer token")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	downloadPath := fmt.Sprintf("%s://%s/user/files/download?path=%s&token=%s", scheme, host, url.QueryEscape(file.Path), url.QueryEscape(viewerToken))

	// Determine file type based on extension
	isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
	mimeType := getMimeType(file.Name)

	viewerSession := struct {
		ViewUrl      string `json:"viewUrl"`
		Token        string `json:"token"`
		Capabilities struct {
			CanEdit     bool   `json:"canEdit"`
			CanAnnotate bool   `json:"canAnnotate"`
			IsPdf       bool   `json:"isPdf"`
			MimeType    string `json:"mimeType"`
		} `json:"capabilities"`
		FileInfo struct {
			Name           string `json:"name"`
			Size           int64  `json:"size"`
			LastModified   string `json:"lastModified"`
			ModifiedByName string `json:"modifiedByName"`
		} `json:"fileInfo"`
		ExpiresAt string `json:"expiresAt"`
	}{
		ViewUrl: downloadPath,
		Token:   viewerToken, // Long-lived JWT token for authenticating file download
		Capabilities: struct {
			CanEdit     bool   `json:"canEdit"`
			CanAnnotate bool   `json:"canAnnotate"`
			IsPdf       bool   `json:"isPdf"`
			MimeType    string `json:"mimeType"`
		}{
			CanEdit:     false,
			CanAnnotate: isPdf,
			IsPdf:       isPdf,
			MimeType:    mimeType,
		},
		FileInfo: struct {
			Name           string `json:"name"`
			Size           int64  `json:"size"`
			LastModified   string `json:"lastModified"`
			ModifiedByName string `json:"modifiedByName"`
		}{
			Name:           file.Name,
			Size:           file.Size,
			LastModified:   file.LastModified.UTC().Format(time.RFC3339),
			ModifiedByName: file.ModifiedByName,
		},
		ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
	}

	fmt.Printf("[VIEWER-SESSION] userId=%s, fileId=%s, token_included=yes, isPdf=%v, mimeType=%s\n", userID.String(), fileId, isPdf, mimeType)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(viewerSession)
}

func editorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	fileId := chi.URLParam(r, "fileId")

	fmt.Printf("[EDITOR] Starting editor session for file=%s user=%s org=%s\n", fileId, userIDStr, orgID.String())

	// Log activity
	db.LogActivity(r.Context(), userID, orgID, &fileId, "edit_file", map[string]interface{}{})

	// Generate WOPI access token (1 hour duration)
	token, _ := middleware.GetToken(r.Context())

	// Build WOPISrc URL
	wopiSrc := fmt.Sprintf("https://go.b0esche.cloud/wopi/files/%s?access_token=%s", fileId, token)

	// Build Collabora editor URL
	collaboraUrl := fmt.Sprintf("https://of.b0esche.cloud/lool/dist/mobile/cool.html?WOPISrc=%s", url.QueryEscape(wopiSrc))

	// Check if user can edit (for now, all org members can edit)
	readOnly := false

	session := struct {
		EditUrl   string `json:"editUrl"`
		Token     string `json:"token"`
		ReadOnly  bool   `json:"readOnly"`
		ExpiresAt string `json:"expiresAt"`
	}{
		EditUrl:   collaboraUrl,
		Token:     token, // JWT token for authenticating file access
		ReadOnly:  readOnly,
		ExpiresAt: time.Now().Add(15 * time.Minute).UTC().Format(time.RFC3339),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(session)
}

// userEditorHandler handles GET /user/files/{fileId}/edit
func userEditorHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	fileId := chi.URLParam(r, "fileId")

	fmt.Printf("[EDITOR] Starting user editor session for file=%s user=%s\n", fileId, userIDStr)

	// Get file metadata to determine path and type
	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file metadata")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Verify user owns this file
	if file.UserID == nil || *file.UserID != userID {
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Log activity
	db.LogActivity(r.Context(), userID, uuid.Nil, &fileId, "edit_file", map[string]interface{}{})

	// Generate WOPI access token (1 hour duration)
	token, _ := middleware.GetToken(r.Context())

	// Build WOPISrc URL
	wopiSrc := fmt.Sprintf("https://go.b0esche.cloud/wopi/files/%s?access_token=%s", fileId, token)

	// Build Collabora editor URL
	collaboraUrl := fmt.Sprintf("https://of.b0esche.cloud/lool/dist/mobile/cool.html?WOPISrc=%s", url.QueryEscape(wopiSrc))

	fmt.Printf("[EDITOR] Built user URLs: wopiSrc=%s collaboraUrl=%s\n", wopiSrc, collaboraUrl)

	// Check if user can edit (for now, all users can edit their own files)
	readOnly := false

	session := struct {
		EditUrl   string `json:"editUrl"`
		Token     string `json:"token"`
		ReadOnly  bool   `json:"readOnly"`
		ExpiresAt string `json:"expiresAt"`
	}{
		EditUrl:   collaboraUrl,
		Token:     token, // JWT token for authenticating file access
		ReadOnly:  readOnly,
		ExpiresAt: time.Now().Add(15 * time.Minute).UTC().Format(time.RFC3339),
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(session)
}

func annotationsHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	fileId := chi.URLParam(r, "fileId")

	// Parse payload
	var payload struct {
		Annotations   []interface{} `json:"annotations"`
		BaseVersionId string        `json:"baseVersionId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	// Log activity
	auditLogger.Log(r.Context(), audit.Entry{
		UserID:   &userID,
		OrgID:    &orgID,
		Resource: &fileId,
		Action:   "annotate_pdf",
		Success:  true,
		Metadata: map[string]interface{}{"count": len(payload.Annotations)},
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func activityHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	activities, err := db.GetOrgActivities(r.Context(), orgID, 50)
	if err != nil {
		errors.LogError(r, err, "Failed to get org activities")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(activities)
}

type memberResponse struct {
	UserID    string    `json:"userId"`
	OrgID     string    `json:"orgId"`
	Role      string    `json:"role"`
	CreatedAt time.Time `json:"createdAt"`
	User      userInfo  `json:"user"`
}

type userInfo struct {
	ID          string  `json:"id"`
	Username    string  `json:"username"`
	DisplayName *string `json:"displayName"`
	Email       string  `json:"email"`
}

func listMembersHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	members, err := db.GetOrgMembersWithUsers(r.Context(), orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to get org members")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Convert to proper response format
	var response []memberResponse
	for _, m := range members {
		response = append(response, memberResponse{
			UserID:    m.Membership.UserID.String(),
			OrgID:     m.Membership.OrgID.String(),
			Role:      m.Membership.Role,
			CreatedAt: m.Membership.CreatedAt,
			User: userInfo{
				ID:       m.User.ID.String(),
				Username: m.User.Username,
				DisplayName: func() *string {
					if m.User.DisplayName == "" {
						return nil
					}
					return &m.User.DisplayName
				}(),
				Email: m.User.Email,
			},
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func updateMemberRoleHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr := chi.URLParam(r, "userId")
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	var req struct {
		Role string `json:"role"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if err := db.UpdateMemberRole(r.Context(), orgID, userID, req.Role); err != nil {
		errors.LogError(r, err, "Failed to update member role")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func removeMemberHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr := chi.URLParam(r, "userId")
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Check if trying to remove the owner
	membership, err := db.GetUserMembership(r.Context(), userID, orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to get membership")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	if membership.Role == "owner" {
		errors.WriteError(w, errors.CodePermissionDenied, "Cannot remove organization owner", http.StatusForbidden)
		return
	}

	if err := db.RemoveMember(r.Context(), orgID, userID); err != nil {
		errors.LogError(r, err, "Failed to remove member")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	resource := userID.String()
	auditLogger.Log(r.Context(), audit.Entry{
		OrgID:    &orgID,
		Action:   "remove_member",
		Resource: &resource,
		Success:  true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func searchUsersHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	query := r.URL.Query().Get("q")
	if query == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Query parameter 'q' is required", http.StatusBadRequest)
		return
	}

	users, err := db.SearchUsersByUsername(r.Context(), query, 10)
	if err != nil {
		errors.LogError(r, err, "Failed to search users")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(users)
}

func createInvitationHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	invitedBy, _ := uuid.Parse(userIDStr)

	var req struct {
		Username string `json:"username"`
		Role     string `json:"role"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if req.Role != "admin" && req.Role != "member" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Role must be 'admin' or 'member'", http.StatusBadRequest)
		return
	}

	invitation, err := db.CreateInvitation(r.Context(), orgID, invitedBy, req.Username, req.Role)
	if err != nil {
		if strings.Contains(err.Error(), "duplicate key value") {
			errors.WriteError(w, errors.CodeAlreadyExists, "User is already invited or a member", http.StatusConflict)
			return
		}
		errors.LogError(r, err, "Failed to create invitation")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:   &invitedBy,
		OrgID:    &orgID,
		Action:   "create_invitation",
		Resource: &req.Username,
		Success:  true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(invitation)
}

func listInvitationsHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	invitations, err := db.GetOrgInvitations(r.Context(), orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to get invitations")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(invitations)
}

func cancelInvitationHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	invitationIDStr := chi.URLParam(r, "invitationId")
	invitationID, err := uuid.Parse(invitationIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid invitation ID", http.StatusBadRequest)
		return
	}

	if err := db.CancelInvitation(r.Context(), invitationID); err != nil {
		errors.LogError(r, err, "Failed to cancel invitation")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	resource := invitationID.String()
	auditLogger.Log(r.Context(), audit.Entry{
		OrgID:    &orgID,
		Action:   "cancel_invitation",
		Resource: &resource,
		Success:  true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func createJoinRequestHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	var req struct {
		OrgID       string  `json:"orgId"`
		InviteToken *string `json:"inviteToken,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	orgID, err := uuid.Parse(req.OrgID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid org ID", http.StatusBadRequest)
		return
	}

	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)

	// If invite token provided, validate it
	if req.InviteToken != nil {
		var token *string
		err := db.QueryRowContext(r.Context(), `
			SELECT invite_link_token FROM organizations WHERE id = $1
		`, orgID).Scan(&token)
		if err != nil || token == nil || *token != *req.InviteToken {
			errors.WriteError(w, errors.CodeInvalidArgument, "Invalid invite token", http.StatusBadRequest)
			return
		}
	}

	joinRequest, err := db.CreateJoinRequest(r.Context(), orgID, userID, req.InviteToken)
	if err != nil {
		errors.LogError(r, err, "Failed to create join request")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		OrgID:   &orgID,
		Action:  "create_join_request",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(joinRequest)
}

type joinRequestResponse struct {
	ID          string    `json:"id"`
	OrgID       string    `json:"orgId"`
	UserID      string    `json:"userId"`
	InviteToken *string   `json:"inviteToken"`
	RequestedAt time.Time `json:"requestedAt"`
	Status      string    `json:"status"`
	User        userInfo  `json:"user"`
}

func listJoinRequestsHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	requests, err := db.GetOrgJoinRequests(r.Context(), orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to get join requests")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Convert to proper response format
	var response []joinRequestResponse
	for _, req := range requests {
		response = append(response, joinRequestResponse{
			ID:          req.JoinRequest.ID.String(),
			OrgID:       req.JoinRequest.OrgID.String(),
			UserID:      req.JoinRequest.UserID.String(),
			InviteToken: req.JoinRequest.InviteToken,
			RequestedAt: req.JoinRequest.RequestedAt,
			Status:      req.JoinRequest.Status,
			User: userInfo{
				ID:       req.User.ID.String(),
				Username: req.User.Username,
				DisplayName: func() *string {
					if req.User.DisplayName == "" {
						return nil
					}
					return &req.User.DisplayName
				}(),
				Email: req.User.Email,
			},
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func acceptJoinRequestHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	requestIDStr := chi.URLParam(r, "requestId")
	requestID, err := uuid.Parse(requestIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid request ID", http.StatusBadRequest)
		return
	}

	var req struct {
		Role string `json:"role"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if req.Role != "admin" && req.Role != "member" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Role must be 'admin' or 'member'", http.StatusBadRequest)
		return
	}

	if err := db.AcceptJoinRequest(r.Context(), requestID, req.Role); err != nil {
		errors.LogError(r, err, "Failed to accept join request")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	resource := requestID.String()
	auditLogger.Log(r.Context(), audit.Entry{
		OrgID:    &orgID,
		Action:   "accept_join_request",
		Resource: &resource,
		Success:  true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func rejectJoinRequestHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	requestIDStr := chi.URLParam(r, "requestId")
	requestID, err := uuid.Parse(requestIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid request ID", http.StatusBadRequest)
		return
	}

	if err := db.RejectJoinRequest(r.Context(), requestID); err != nil {
		errors.LogError(r, err, "Failed to reject join request")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	resource := requestID.String()
	auditLogger.Log(r.Context(), audit.Entry{
		OrgID:    &orgID,
		Action:   "reject_join_request",
		Resource: &resource,
		Success:  true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status": "ok"}`))
}

func getInviteLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	token, err := db.GetInviteLink(r.Context(), orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to get invite link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	response := struct {
		InviteLink *string `json:"inviteLink"`
	}{
		InviteLink: token,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func regenerateInviteLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)

	newToken, err := db.RegenerateInviteLink(r.Context(), orgID)
	if err != nil {
		errors.LogError(r, err, "Failed to regenerate invite link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		OrgID:   &orgID,
		Action:  "regenerate_invite_link",
		Success: true,
	})

	response := struct {
		InviteLink string `json:"inviteLink"`
	}{
		InviteLink: *newToken,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func getPermissionsHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)

	// Check each permission
	canRead, _ := permission.HasPermission(r.Context(), db, userID, orgID, permission.FileRead)
	canWrite, _ := permission.HasPermission(r.Context(), db, userID, orgID, permission.FileWrite)
	canEdit, _ := permission.HasPermission(r.Context(), db, userID, orgID, permission.DocumentEdit)
	canAdmin, _ := permission.HasPermission(r.Context(), db, userID, orgID, permission.OrgManage)

	response := struct {
		CanRead     bool `json:"canRead"`
		CanWrite    bool `json:"canWrite"`
		CanShare    bool `json:"canShare"`
		CanAdmin    bool `json:"canAdmin"`
		CanAnnotate bool `json:"canAnnotate"`
		CanEdit     bool `json:"canEdit"`
	}{
		CanRead:     canRead,
		CanWrite:    canWrite,
		CanShare:    canRead, // Share is tied to read for now
		CanAdmin:    canAdmin,
		CanAnnotate: canEdit, // Annotate is tied to edit
		CanEdit:     canEdit,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func fileMetaHandler(w http.ResponseWriter, r *http.Request) {
	meta := struct {
		LastModified   string `json:"lastModified"`
		LastModifiedBy string `json:"lastModifiedBy"`
		VersionCount   int    `json:"versionCount"`
	}{
		LastModified:   "2023-01-01T00:00:00Z",
		LastModifiedBy: "user@example.com",
		VersionCount:   1,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(meta)
}

// Passkey handlers

func signupHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	var req struct {
		Username    string `json:"username"`
		Email       string `json:"email"`
		DisplayName string `json:"displayName"`
		Password    string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if req.Username == "" || req.Email == "" || req.Password == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Username, email, and password are required", http.StatusBadRequest)
		return
	}

	// Hash password
	passkeyService := auth.NewService(db)
	passwordHash, err := passkeyService.HashPassword(req.Password)
	if err != nil {
		errors.LogError(r, err, "Failed to hash password")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Create user with hashed password
	user, err := db.CreateUser(r.Context(), req.Username, req.Email, req.DisplayName, &passwordHash)
	if err != nil {
		errors.LogError(r, err, "Failed to create user")
		if strings.Contains(err.Error(), "duplicate key") {
			errors.WriteError(w, errors.CodeConflict, "Username or email already exists", http.StatusConflict)
		} else {
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		}
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"userId": user.ID,
		"user":   user,
	})
}

func registrationChallengeHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	var req struct {
		UserID string `json:"userId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	userID, err := uuid.Parse(req.UserID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	passkeyService := auth.NewService(db)
	challenge, err := passkeyService.StartRegistrationChallenge(r.Context(), userID)
	if err != nil {
		errors.LogError(r, err, "Failed to generate challenge")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"challenge": challenge,
		"rp": map[string]string{
			"name": auth.RPName,
			"id":   auth.RPID,
		},
		"user": map[string]string{
			"id":   userID.String(),
			"name": userID.String(),
		},
		"pubKeyCredParams": []map[string]interface{}{
			{"alg": -7, "type": "public-key"},
			{"alg": -257, "type": "public-key"},
		},
		"timeout":     60000,
		"attestation": "direct",
		"authenticatorSelection": map[string]interface{}{
			"authenticatorAttachment": "platform",
			"requireResidentKey":      false,
			"userVerification":        "preferred",
		},
	})
}

func registrationVerifyHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
	var req struct {
		UserID            string `json:"userId"`
		Challenge         string `json:"challenge"`
		CredentialID      string `json:"credentialId"`
		PublicKey         string `json:"publicKey"`
		ClientDataJSON    string `json:"clientDataJSON"`
		AttestationObject string `json:"attestationObject"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	userID, err := uuid.Parse(req.UserID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	passkeyService := auth.NewService(db)
	_, err = passkeyService.VerifyRegistrationResponse(
		r.Context(),
		userID,
		req.Challenge,
		req.CredentialID,
		req.PublicKey,
		req.ClientDataJSON,
		req.AttestationObject,
	)
	if err != nil {
		errors.LogError(r, err, "Failed to verify registration")
		errors.WriteError(w, errors.CodeUnauthenticated, "Registration failed: "+err.Error(), http.StatusBadRequest)
		return
	}

	// Create session
	session, err := db.CreateSession(r.Context(), userID, time.Now().Add(15*time.Minute))
	if err != nil {
		errors.LogError(r, err, "Failed to create session")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get user
	user, err := db.GetUserByID(r.Context(), userID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Generate JWT
	orgIDs := []string{}
	token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
	if err != nil {
		errors.LogError(r, err, "Token generation failed")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "registration",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"token": token,
		"user":  user,
	})
}

func authenticationChallengeHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	var req struct {
		Username string `json:"username"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if req.Username == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Username is required", http.StatusBadRequest)
		return
	}

	passkeyService := auth.NewService(db)
	challenge, credentialIDs, err := passkeyService.StartAuthenticationChallenge(r.Context(), req.Username)
	if err != nil {
		errors.LogError(r, err, "Failed to generate challenge")
		errors.WriteError(w, errors.CodeNotFound, "User not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"challenge":        challenge,
		"timeout":          60000,
		"userVerification": "preferred",
		"allowCredentials": credentialIDs,
	})
}

func authenticationVerifyHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
	var req struct {
		Username          string `json:"username"`
		Challenge         string `json:"challenge"`
		CredentialID      string `json:"credentialId"`
		AuthenticatorData string `json:"authenticatorData"`
		ClientDataJSON    string `json:"clientDataJSON"`
		Signature         string `json:"signature"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	passkeyService := auth.NewService(db)
	user, err := passkeyService.VerifyAuthenticationResponse(
		r.Context(),
		req.Username,
		req.Challenge,
		req.CredentialID,
		req.AuthenticatorData,
		req.ClientDataJSON,
		req.Signature,
	)
	if err != nil {
		errors.LogError(r, err, "Failed to verify authentication")
		errors.WriteError(w, errors.CodeUnauthenticated, "Authentication failed: "+err.Error(), http.StatusBadRequest)
		return
	}

	// Create session
	session, err := db.CreateSession(r.Context(), user.ID, time.Now().Add(15*time.Minute))
	if err != nil {
		errors.LogError(r, err, "Failed to create session")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get user orgs
	orgs, err := db.GetUserOrganizations(r.Context(), user.ID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user orgs")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	orgIDs := make([]string, len(orgs))
	for i, o := range orgs {
		orgIDs[i] = o.ID.String()
	}

	// Generate JWT
	token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
	if err != nil {
		errors.LogError(r, err, "Token generation failed")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &user.ID,
		Action:  "login",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"token": token,
		"user":  user,
	})
}

func passwordLoginHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, auditLogger *audit.Logger) {
	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	if req.Username == "" || req.Password == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Username and password are required", http.StatusBadRequest)
		return
	}

	// Verify password
	passkeyService := auth.NewService(db)
	user, err := passkeyService.VerifyPasswordLogin(r.Context(), req.Username, req.Password)
	if err != nil {
		auditLogger.Log(r.Context(), audit.Entry{
			Action:   "login",
			Success:  false,
			Metadata: map[string]interface{}{"error": err.Error()},
		})
		errors.LogError(r, err, "Password login failed")
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// Create session
	session, err := db.CreateSession(r.Context(), user.ID, time.Now().Add(15*time.Minute))
	if err != nil {
		errors.LogError(r, err, "Failed to create session")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get user orgs
	orgs, err := db.GetUserOrganizations(r.Context(), user.ID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user orgs")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	orgIDs := make([]string, len(orgs))
	for i, o := range orgs {
		orgIDs[i] = o.ID.String()
	}

	// Generate JWT
	token, err := jwtManager.Generate(user.ID.String(), orgIDs, session.ID.String())
	if err != nil {
		errors.LogError(r, err, "Token generation failed")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &user.ID,
		Action:  "login",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"token": token,
		"user":  user,
	})
}

// userFilesHandler returns files for the authenticated user's personal workspace.
func userFilesHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.LogError(r, err, "Invalid user id in context")
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	path := r.URL.Query().Get("path")
	if path == "" {
		path = "/"
	}
	q := r.URL.Query().Get("q")
	page := 1
	pageSize := 100
	if p := r.URL.Query().Get("page"); p != "" {
		fmt.Sscanf(p, "%d", &page)
	}
	if ps := r.URL.Query().Get("pageSize"); ps != "" {
		fmt.Sscanf(ps, "%d", &pageSize)
	}

	files, err := db.GetUserFiles(r.Context(), userID, path, q, page, pageSize)
	if err != nil {
		errors.LogError(r, err, "Failed to get user files")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	out := make([]map[string]interface{}, 0, len(files))
	for _, f := range files {
		out = append(out, map[string]interface{}{
			"id":           f.ID.String(),
			"name":         f.Name,
			"path":         f.Path,
			"type":         f.Type,
			"size":         f.Size,
			"lastModified": f.LastModified.UTC().Format(time.RFC3339),
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(out)
}

// createOrgFileHandler creates a file or folder record for an org workspace.
func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	var f *database.File
	var err error

	// Support multipart uploads (field "file") or JSON metadata for folders
	contentType := r.Header.Get("Content-Type")
	if strings.HasPrefix(contentType, "multipart/form-data") {
		// Handle file upload
		if err = r.ParseMultipartForm(32 << 20); err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Bad multipart request", http.StatusBadRequest)
			return
		}
		parentPath := r.FormValue("path")
		if parentPath == "" {
			parentPath = "/"
		}
		parentPath, err = sanitizePath(parentPath)
		if err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
			return
		}
		var file multipart.File
		var header *multipart.FileHeader
		file, header, err = r.FormFile("file")
		if err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Missing file", http.StatusBadRequest)
			return
		}
		defer file.Close()

		// Read file into memory
		data, err := io.ReadAll(file)
		if err != nil {
			errors.LogError(r, err, "Failed to read uploaded file")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
		if !strings.HasPrefix(storedPath, "/") {
			storedPath = "/" + storedPath
		}
		written := int64(len(data))

		// Get or create user's WebDAV client
		storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
		if err != nil {
			errors.LogError(r, err, "Failed to get user WebDAV client")
			errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
			return
		}

		// Upload to user's Nextcloud space under /orgs/<orgID>/
		rel := strings.TrimPrefix(storedPath, "/")
		remotePath := path.Join("/orgs", orgID.String(), rel)
		if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
			errors.LogError(r, err, "WebDAV upload failed")
			errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
			return
		}
		f, err = db.CreateFile(r.Context(), &orgID, &userID, header.Filename, storedPath, "file", written)
		if err != nil {
			errors.LogError(r, err, "Failed to create org file")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		auditLogger.Log(r.Context(), audit.Entry{
			UserID:  &userID,
			OrgID:   &orgID,
			Action:  "upload_file",
			Success: true,
		})

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
		return
	}

	var req struct {
		Name string `json:"name"`
		Path string `json:"path"`
		Type string `json:"type"` // file|folder
		Size int64  `json:"size"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	f, err = db.CreateFile(r.Context(), &orgID, &userID, req.Name, req.Path, req.Type, req.Size)
	if err != nil {
		errors.LogError(r, err, "Failed to create org file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		OrgID:   &orgID,
		Action:  "create_file",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
}

// deleteOrgFileHandler deletes a file/folder in org workspace by path
func deleteOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)

	var req struct {
		Path string `json:"path"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	var err error
	req.Path, err = sanitizePath(req.Path)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
		return
	}

	// Get or create user's WebDAV client and delete from Nextcloud
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database deletion)")
	} else {
		rel := strings.TrimPrefix(req.Path, "/")
		remotePath := path.Join("/orgs", orgID.String(), rel)
		if err := storageClient.Delete(r.Context(), remotePath); err != nil {
			errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
		}
	}

	// Delete from database
	if err := db.DeleteFileByPath(r.Context(), &orgID, nil, req.Path); err != nil {
		errors.LogError(r, err, "Failed to delete org file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		OrgID:   &orgID,
		Action:  "delete_file",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"ok"}`))
}

// Also accept POST /orgs/{orgId}/files/delete for clients that cannot send DELETE with body
func deleteOrgFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	deleteOrgFileHandler(w, r, db, auditLogger, cfg)
}

// moveOrgFileHandler moves/renames a file in org workspace
func moveOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)

	var req struct {
		SourcePath string `json:"sourcePath"`
		TargetPath string `json:"targetPath"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	var err error
	req.SourcePath, err = sanitizePath(req.SourcePath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid source path", http.StatusBadRequest)
		return
	}
	req.TargetPath, err = sanitizePath(req.TargetPath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid target path", http.StatusBadRequest)
		return
	}

	// Get source file details before moving
	sourceFiles, err := db.GetOrgFiles(r.Context(), orgID, userID, "/", "", 0, 1000)
	if err != nil {
		errors.LogError(r, err, "Failed to get org files")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	var sourceFile *database.File
	for i := range sourceFiles {
		if sourceFiles[i].Path == req.SourcePath {
			sourceFile = &sourceFiles[i]
			break
		}
	}

	if sourceFile == nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Source file not found", http.StatusNotFound)
		return
	}

	// Prevent moving a folder into itself or its own subfolders
	if sourceFile.Type == "folder" {
		// Check if trying to move into itself
		if req.SourcePath == req.TargetPath {
			errors.WriteError(w, errors.CodeInvalidArgument, "Cannot move a folder into itself", http.StatusBadRequest)
			return
		}
		// Check if target is a subfolder of source
		if strings.HasPrefix(req.TargetPath, req.SourcePath+"/") {
			errors.WriteError(w, errors.CodeInvalidArgument, "Cannot move a folder into its own subfolder", http.StatusBadRequest)
			return
		}
	}

	// Determine new file name - check if target is a folder
	var newPath string
	var targetFile *database.File
	for i := range sourceFiles {
		if sourceFiles[i].Path == req.TargetPath {
			targetFile = &sourceFiles[i]
			break
		}
	}

	if targetFile != nil && targetFile.Type == "folder" {
		// Target is a folder, move file into it
		newPath = path.Join(req.TargetPath, sourceFile.Name)
	} else if targetFile == nil && strings.HasSuffix(req.TargetPath, "/") {
		// Target path doesn't exist but ends with /, treat as folder
		newPath = path.Join(req.TargetPath, sourceFile.Name)
	} else {
		// Moving/renaming to a specific path
		newPath = req.TargetPath
	}

	// Get or create user's WebDAV client and move in Nextcloud
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database operation)")
	} else {
		sourceRel := strings.TrimPrefix(req.SourcePath, "/")
		sourcePath := path.Join("/orgs", orgID.String(), sourceRel)
		targetRel := strings.TrimPrefix(newPath, "/")
		targetPath := path.Join("/orgs", orgID.String(), targetRel)
		if err := storageClient.Move(r.Context(), sourcePath, targetPath); err != nil {
			errors.LogError(r, err, "Failed to move in Nextcloud (continuing with database operation)")
		}
	}

	// Delete old file record
	if err := db.DeleteFileByPath(r.Context(), &orgID, nil, req.SourcePath); err != nil {
		errors.LogError(r, err, "Failed to delete old file record")
	}

	// Create new file record at the new location
	if _, err := db.CreateFile(r.Context(), &orgID, nil, sourceFile.Name, newPath, sourceFile.Type, sourceFile.Size); err != nil {
		errors.LogError(r, err, "Failed to create new file record at destination")
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		OrgID:   &orgID,
		Action:  "move_file",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"ok"}`))
}

// createUserFileHandler creates a file or folder record for the authenticated user's personal workspace.
func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)
	var f *database.File
	var err error
	// Support multipart uploads for file content or JSON for folders
	contentType := r.Header.Get("Content-Type")
	if strings.HasPrefix(contentType, "multipart/form-data") {
		if err = r.ParseMultipartForm(32 << 20); err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Bad multipart request", http.StatusBadRequest)
			return
		}
		parentPath := r.FormValue("path")
		if parentPath == "" {
			parentPath = "/"
		}
		parentPath, err = sanitizePath(parentPath)
		if err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
			return
		}
		var file multipart.File
		var header *multipart.FileHeader
		file, header, err = r.FormFile("file")
		if err != nil {
			errors.WriteError(w, errors.CodeInvalidArgument, "Missing file", http.StatusBadRequest)
			return
		}
		defer file.Close()
		// Read file into memory to allow WebDAV upload
		data, err := io.ReadAll(file)
		if err != nil {
			errors.LogError(r, err, "Failed to read uploaded file")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}
		storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
		if !strings.HasPrefix(storedPath, "/") {
			storedPath = "/" + storedPath
		}
		written := int64(len(data))
		fmt.Printf("[DEBUG] Upload: user=%s, file=%s, size=%d, path=%s\n", userID.String(), header.Filename, len(data), storedPath)

		// Get or create user's WebDAV client
		storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
		if err != nil {
			errors.LogError(r, err, "Failed to get user WebDAV client")
			errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
			return
		}

		// Upload to user's personal Nextcloud space (just the path, no username prefix)
		remotePath := strings.TrimPrefix(storedPath, "/")
		fmt.Printf("[DEBUG] Uploading to user WebDAV: /%s\n", remotePath)
		if err = storageClient.Upload(r.Context(), "/"+remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
			errors.LogError(r, err, "WebDAV upload failed")
			errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
			return
		}

		f, err = db.CreateFile(r.Context(), nil, &userID, header.Filename, storedPath, "file", written)
		if err != nil {
			errors.LogError(r, err, "Failed to create user file")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		auditLogger.Log(r.Context(), audit.Entry{
			UserID:  &userID,
			Action:  "upload_user_file",
			Success: true,
		})

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
		return
	}

	var req struct {
		Name string `json:"name"`
		Path string `json:"path"`
		Type string `json:"type"` // file|folder
		Size int64  `json:"size"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	f, err = db.CreateFile(r.Context(), nil, &userID, req.Name, req.Path, req.Type, req.Size)
	if err != nil {
		errors.LogError(r, err, "Failed to create user file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "create_user_file",
		Success: true,
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
}

// Also accept POST /user/files/delete
func deleteUserFilePostHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	deleteUserFileHandler(w, r, db, auditLogger, cfg)
}

// moveUserFileHandler moves/renames a file in user's personal workspace
func moveUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)

	var req struct {
		SourcePath string `json:"sourcePath"`
		TargetPath string `json:"targetPath"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	var err error
	req.SourcePath, err = sanitizePath(req.SourcePath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid source path", http.StatusBadRequest)
		return
	}
	req.TargetPath, err = sanitizePath(req.TargetPath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid target path", http.StatusBadRequest)
		return
	}

	// Get source file details before moving
	sourceFile, err := db.GetUserFileByPath(r.Context(), userID, req.SourcePath)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeInvalidArgument, "Source file not found", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get source file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Prevent moving a folder into itself or its own subfolders
	if sourceFile.Type == "folder" {
		// Check if trying to move into itself
		if req.SourcePath == req.TargetPath {
			errors.WriteError(w, errors.CodeInvalidArgument, "Cannot move a folder into itself", http.StatusBadRequest)
			return
		}
		// Check if target is a subfolder of source
		if strings.HasPrefix(req.TargetPath, req.SourcePath+"/") {
			errors.WriteError(w, errors.CodeInvalidArgument, "Cannot move a folder into its own subfolder", http.StatusBadRequest)
			return
		}
	}

	// Determine new file name - check if target is a folder
	var newPath string
	targetFile, err := db.GetUserFileByPath(r.Context(), userID, req.TargetPath)
	if err != nil && err != sql.ErrNoRows {
		errors.LogError(r, err, "Failed to get target file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	// If err == sql.ErrNoRows, targetFile is nil

	if targetFile != nil && targetFile.Type == "folder" {
		// Target is a folder, move file into it
		newPath = path.Join(req.TargetPath, sourceFile.Name)
	} else if targetFile == nil && strings.HasSuffix(req.TargetPath, "/") {
		// Target path doesn't exist but ends with /, treat as folder
		newPath = path.Join(req.TargetPath, sourceFile.Name)
	} else {
		// Moving/renaming to a specific path
		newPath = req.TargetPath
	}

	// Get or create user's WebDAV client and move in Nextcloud
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database operation)")
	} else {
		// User files are stored directly in the user's WebDAV root (no /users/{id} prefix)
		sourcePath := "/" + strings.TrimPrefix(req.SourcePath, "/")
		targetPath := "/" + strings.TrimPrefix(newPath, "/")
		if err := storageClient.Move(r.Context(), sourcePath, targetPath); err != nil {
			errors.LogError(r, err, "Failed to move in Nextcloud (continuing with database operation)")
		}
	}

	// Delete old file record
	if err := db.DeleteFileByPath(r.Context(), nil, &userID, req.SourcePath); err != nil {
		errors.LogError(r, err, "Failed to delete old file record")
	}

	// Create new file record at the new location
	if _, err := db.CreateFile(r.Context(), nil, &userID, sourceFile.Name, newPath, sourceFile.Type, sourceFile.Size); err != nil {
		errors.LogError(r, err, "Failed to create new file record at destination")
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "move_file",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"ok"}`))
}

// deleteUserFileHandler deletes a file/folder in user's personal workspace by path
func deleteUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)

	var req struct {
		Path string `json:"path"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Bad request", http.StatusBadRequest)
		return
	}

	var err error
	req.Path, err = sanitizePath(req.Path)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
		return
	}

	// Get or create user's WebDAV client and delete from Nextcloud
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client (continuing with database deletion)")
	} else {
		remotePath := strings.TrimPrefix(req.Path, "/")
		if err := storageClient.Delete(r.Context(), "/"+remotePath); err != nil {
			errors.LogError(r, err, "Failed to delete from Nextcloud (continuing anyway)")
		}
	}

	// Delete from database
	if err := db.DeleteFileByPath(r.Context(), nil, &userID, req.Path); err != nil {
		errors.LogError(r, err, "Failed to delete user file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "delete_user_file",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"status":"ok"}`))
}

// downloadOrgFileHandler downloads a file from org workspace
func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) {
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)

	filePath := r.URL.Query().Get("path")
	if filePath == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing path parameter", http.StatusBadRequest)
		return
	}

	// Sanitize path to prevent path traversal
	filePath, err := sanitizePath(filePath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
		return
	}

	// Get or create user's WebDAV client
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client")
		errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
		return
	}

	// Check if it's a folder
	file, err := db.GetOrgFileByPath(r.Context(), orgID, userID, filePath)
	if err != nil && err.Error() != "sql: no rows in result set" {
		errors.LogError(r, err, "Failed to get file info")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	if file != nil && file.Type == "folder" {
		// Download folder as ZIP
		err = downloadOrgFolderAsZip(w, r, db, cfg, orgID, userID, filePath, storageClient)
		if err != nil {
			errors.LogError(r, err, "Failed to download folder")
			errors.WriteError(w, errors.CodeInternal, "Failed to download folder", http.StatusInternalServerError)
		}
		return
	}

	// Download from user's Nextcloud space under /orgs/<orgID>/
	rel := strings.TrimPrefix(filePath, "/")
	remotePath := path.Join("/orgs", orgID.String(), rel)
	resp, err := storageClient.Download(r.Context(), remotePath, r.Header.Get("Range"))
	if err != nil {
		errors.LogError(r, err, "Failed to download from Nextcloud")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	defer resp.Body.Close()

	// Set appropriate headers for inline viewing
	fileName := path.Base(filePath)
	// Determine content type based on file extension - use our getMimeType function
	// which supports all common video/audio/image formats
	contentType := getMimeType(fileName)

	w.Header().Set("Access-Control-Allow-Origin", "https://www.b0esche.cloud")
	w.Header().Set("Access-Control-Allow-Credentials", "true")
	w.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
	w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Accept-Ranges", "bytes")
	if cr := resp.Header.Get("Content-Range"); cr != "" {
		w.Header().Set("Content-Range", cr)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		w.Header().Set("Content-Length", cl)
	}

	if resp.StatusCode == http.StatusPartialContent {
		w.WriteHeader(http.StatusPartialContent)
	}

	// Stream the file
	io.Copy(w, resp.Body)

}

// downloadOrgFolderAsZip downloads a folder as ZIP archive
func downloadOrgFolderAsZip(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, orgID, userID uuid.UUID, folderPath string, storageClient *storage.WebDAVClient) error {
	// Get all files under the folder
	files, err := db.GetAllOrgFilesUnderPath(r.Context(), orgID, userID, folderPath)
	if err != nil {
		return err
	}

	// Filter only files, not folders
	var fileList []database.File
	for _, f := range files {
		if f.Type == "file" {
			fileList = append(fileList, f)
		}
	}

	// Set headers for ZIP download
	folderName := path.Base(folderPath)
	if folderName == "" || folderName == "/" {
		folderName = "org_files"
	}
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", folderName))

	// Create ZIP writer
	zipWriter := zip.NewWriter(w)
	defer zipWriter.Close()

	// Ensure folderPath ends with / for proper relative path calculation
	if !strings.HasSuffix(folderPath, "/") {
		folderPath += "/"
	}

	// Add each file to ZIP
	for _, file := range fileList {
		// Calculate relative path in ZIP
		relPath := strings.TrimPrefix(file.Path, folderPath)

		// Download file from WebDAV
		remoteRel := strings.TrimPrefix(file.Path, "/")
		remotePath := path.Join("/orgs", orgID.String(), remoteRel)
		resp, err := storageClient.Download(r.Context(), remotePath, "")
		if err != nil {
			continue // Skip files that can't be downloaded
		}
		defer resp.Body.Close()

		// Create ZIP entry
		zipFile, err := zipWriter.Create(relPath)
		if err != nil {
			continue
		}

		// Copy file content to ZIP
		io.Copy(zipFile, resp.Body)
	}

	return nil
}

// downloadUserFileHandler downloads a file from user's personal workspace
func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) {
	// Try to get userID from context (Bearer token), fallback to query parameter
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		// Token might be in query parameter for PDF viewer compatibility
		// This is acceptable since the token is still validated
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userID, _ := uuid.Parse(userIDStr)

	filePath := r.URL.Query().Get("path")
	if filePath == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing path parameter", http.StatusBadRequest)
		return
	}

	// Sanitize path to prevent path traversal
	filePath, err := sanitizePath(filePath)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
		return
	}

	// Get or create user's WebDAV client
	storageClient, err := getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get user WebDAV client")
		errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
		return
	}

	// Check if it's a folder
	file, err := db.GetUserFileByPath(r.Context(), userID, filePath)
	if err != nil && err.Error() != "sql: no rows in result set" {
		errors.LogError(r, err, "Failed to get file info")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	if file != nil && file.Type == "folder" {
		// Download folder as ZIP
		err = downloadUserFolderAsZip(w, r, db, cfg, userID, filePath, storageClient)
		if err != nil {
			errors.LogError(r, err, "Failed to download folder")
			errors.WriteError(w, errors.CodeInternal, "Failed to download folder", http.StatusInternalServerError)
		}
		return
	}

	// Download from user's personal Nextcloud space
	remotePath := strings.TrimPrefix(filePath, "/")

	resp, err := storageClient.Download(r.Context(), "/"+remotePath, r.Header.Get("Range"))
	if err != nil {
		errors.LogError(r, err, "Failed to download from Nextcloud")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	defer resp.Body.Close()

	// Set appropriate headers for inline viewing
	fileName := path.Base(filePath)
	// Determine content type based on file extension - use our getMimeType function
	// which supports all common video/audio/image formats
	contentType := getMimeType(fileName)

	w.Header().Set("Access-Control-Allow-Origin", "https://www.b0esche.cloud")
	w.Header().Set("Access-Control-Allow-Credentials", "true")
	w.Header().Set("Access-Control-Allow-Headers", "Authorization, Range")
	w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Accept-Ranges", "bytes")
	if cr := resp.Header.Get("Content-Range"); cr != "" {
		w.Header().Set("Content-Range", cr)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		w.Header().Set("Content-Length", cl)
	}

	if resp.StatusCode == http.StatusPartialContent {
		w.WriteHeader(http.StatusPartialContent)
	}

	// Stream the file
	io.Copy(w, resp.Body)

}

// downloadUserFolderAsZip downloads a folder as ZIP archive
func downloadUserFolderAsZip(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, userID uuid.UUID, folderPath string, storageClient *storage.WebDAVClient) error {
	// Get all files under the folder
	files, err := db.GetAllUserFilesUnderPath(r.Context(), userID, folderPath)
	if err != nil {
		return err
	}

	// Filter only files, not folders
	var fileList []database.File
	for _, f := range files {
		if f.Type == "file" {
			fileList = append(fileList, f)
		}
	}

	// Set headers for ZIP download
	folderName := path.Base(folderPath)
	if folderName == "" || folderName == "/" {
		folderName = "user_files"
	}
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", folderName))

	// Create ZIP writer
	zipWriter := zip.NewWriter(w)
	defer zipWriter.Close()

	// Ensure folderPath ends with / for proper relative path calculation
	if !strings.HasSuffix(folderPath, "/") {
		folderPath += "/"
	}

	// Add each file to ZIP
	for _, file := range fileList {
		// Calculate relative path in ZIP
		relPath := strings.TrimPrefix(file.Path, folderPath)

		// Download file from WebDAV
		remotePath := strings.TrimPrefix(file.Path, "/")
		resp, err := storageClient.Download(r.Context(), "/"+remotePath, "")
		if err != nil {
			continue // Skip files that can't be downloaded
		}
		defer resp.Body.Close()

		// Create ZIP entry
		zipFile, err := zipWriter.Create(relPath)
		if err != nil {
			continue
		}

		// Copy file content to ZIP
		io.Copy(zipFile, resp.Body)
	}

	return nil
}

// getMimeType returns the MIME type based on file extension
func getMimeType(filename string) string {
	lower := strings.ToLower(filename)
	switch {
	// Documents
	case strings.HasSuffix(lower, ".pdf"):
		return "application/pdf"
	case strings.HasSuffix(lower, ".doc"), strings.HasSuffix(lower, ".docx"):
		return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
	case strings.HasSuffix(lower, ".xls"), strings.HasSuffix(lower, ".xlsx"):
		return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
	case strings.HasSuffix(lower, ".ppt"), strings.HasSuffix(lower, ".pptx"):
		return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
	case strings.HasSuffix(lower, ".odt"):
		return "application/vnd.oasis.opendocument.text"
	case strings.HasSuffix(lower, ".ods"):
		return "application/vnd.oasis.opendocument.spreadsheet"
	case strings.HasSuffix(lower, ".odp"):
		return "application/vnd.oasis.opendocument.presentation"
	// Images
	case strings.HasSuffix(lower, ".png"):
		return "image/png"
	case strings.HasSuffix(lower, ".jpg"), strings.HasSuffix(lower, ".jpeg"):
		return "image/jpeg"
	case strings.HasSuffix(lower, ".gif"):
		return "image/gif"
	case strings.HasSuffix(lower, ".webp"):
		return "image/webp"
	case strings.HasSuffix(lower, ".svg"):
		return "image/svg+xml"
	case strings.HasSuffix(lower, ".bmp"):
		return "image/bmp"
	case strings.HasSuffix(lower, ".ico"):
		return "image/x-icon"
	// Video formats
	case strings.HasSuffix(lower, ".mp4"), strings.HasSuffix(lower, ".m4v"):
		return "video/mp4"
	case strings.HasSuffix(lower, ".webm"):
		return "video/webm"
	case strings.HasSuffix(lower, ".ogv"):
		return "video/ogg"
	case strings.HasSuffix(lower, ".mov"):
		return "video/quicktime"
	case strings.HasSuffix(lower, ".avi"):
		return "video/x-msvideo"
	case strings.HasSuffix(lower, ".mkv"):
		return "video/x-matroska"
	case strings.HasSuffix(lower, ".wmv"):
		return "video/x-ms-wmv"
	case strings.HasSuffix(lower, ".flv"):
		return "video/x-flv"
	case strings.HasSuffix(lower, ".3gp"):
		return "video/3gpp"
	case strings.HasSuffix(lower, ".ts"):
		return "video/mp2t"
	case strings.HasSuffix(lower, ".mpg"), strings.HasSuffix(lower, ".mpeg"):
		return "video/mpeg"
	// Audio formats
	case strings.HasSuffix(lower, ".mp3"):
		return "audio/mpeg"
	case strings.HasSuffix(lower, ".wav"):
		return "audio/wav"
	case strings.HasSuffix(lower, ".ogg"), strings.HasSuffix(lower, ".oga"):
		return "audio/ogg"
	case strings.HasSuffix(lower, ".m4a"), strings.HasSuffix(lower, ".aac"):
		return "audio/aac"
	case strings.HasSuffix(lower, ".flac"):
		return "audio/flac"
	case strings.HasSuffix(lower, ".wma"):
		return "audio/x-ms-wma"
	// Text/code
	case strings.HasSuffix(lower, ".txt"):
		return "text/plain"
	case strings.HasSuffix(lower, ".html"), strings.HasSuffix(lower, ".htm"):
		return "text/html"
	case strings.HasSuffix(lower, ".css"):
		return "text/css"
	case strings.HasSuffix(lower, ".js"):
		return "application/javascript"
	case strings.HasSuffix(lower, ".json"):
		return "application/json"
	case strings.HasSuffix(lower, ".xml"):
		return "application/xml"
	case strings.HasSuffix(lower, ".csv"):
		return "text/csv"
	// Archives
	case strings.HasSuffix(lower, ".zip"):
		return "application/zip"
	case strings.HasSuffix(lower, ".rar"):
		return "application/vnd.rar"
	case strings.HasSuffix(lower, ".7z"):
		return "application/x-7z-compressed"
	case strings.HasSuffix(lower, ".tar"):
		return "application/x-tar"
	case strings.HasSuffix(lower, ".gz"):
		return "application/gzip"
	default:
		return "application/octet-stream"
	}
}

// File share handlers

func getFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to org or is owned by user (for personal files)
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID != nil && *file.OrgID != orgID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID == nil && file.UserID != nil && *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	link, err := db.GetFileShareLinkByFileID(r.Context(), fileUUID)
	if err != nil {
		if err == sql.ErrNoRows {
			// No share link exists
			errors.WriteError(w, errors.CodeNotFound, "Share link not found", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Build full URL
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := "www.b0esche.cloud"
	fullURL := fmt.Sprintf("%s://%s/share/%s", scheme, host, link.Token)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"shareUrl": fullURL,
		"token":    link.Token,
	})
}

func createFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to org or is owned by user (for personal files)
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID != nil && *file.OrgID != orgID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID == nil && file.UserID != nil && *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Revoke existing link if any
	db.RevokeFileShareLink(r.Context(), fileUUID) // Ignore error

	// Generate token
	token, err := storage.GenerateSecurePassword(48)
	if err != nil {
		errors.LogError(r, err, "Failed to generate token")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	link, err := db.CreateFileShareLink(r.Context(), token, fileUUID, &orgID, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to create share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	log.Printf("Share link created: user_id=%s, file_id=%s, org_id=%v", userID, fileUUID, link.OrgID)

	// Build full URL
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := "www.b0esche.cloud"
	fullURL := fmt.Sprintf("%s://%s/share/%s", scheme, host, link.Token)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"shareUrl": fullURL,
		"token":    link.Token,
	})
}

func revokeFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	orgID := r.Context().Value(middleware.OrgKey).(uuid.UUID)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to org or is owned by user (for personal files)
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID != nil && *file.OrgID != orgID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.OrgID == nil && file.UserID != nil && *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	err = db.RevokeFileShareLink(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to revoke share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
	token := chi.URLParam(r, "token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	link, err := db.GetFileShareLinkByToken(r.Context(), token)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get file metadata
	file, err := db.GetFileByID(r.Context(), link.FileID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Generate a short-lived token for download (1 hour)
	var orgIDs []string
	if link.OrgID != nil {
		orgIDs = []string{link.OrgID.String()}
	}
	viewerToken, err := jwtManager.GenerateWithDuration("", orgIDs, "", time.Hour)
	if err != nil {
		errors.LogError(r, err, "Failed to generate viewer token")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Build URLs
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := r.Host
	if host == "" {
		host = "go.b0esche.cloud"
	}
	downloadPath := fmt.Sprintf("%s://%s/public/share/%s/download?token=%s", scheme, host, token, url.QueryEscape(viewerToken))
	viewPath := fmt.Sprintf("%s://%s/public/share/%s/view?token=%s", scheme, host, token, url.QueryEscape(viewerToken))

	// Check if user is authenticated and has access to the file
	var internalOrgId *string
	var internalFileId *string
	authHeader := r.Header.Get("Authorization")
	if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
		jwtToken := strings.TrimPrefix(authHeader, "Bearer ")
		claims, err := jwtManager.Validate(jwtToken)
		if err == nil {
			userID, err := uuid.Parse(claims.Subject)
			if err == nil {
				// Check if user has access
				if file.UserID != nil && *file.UserID == userID {
					// Personal file, user is owner
					fileIDStr := file.ID.String()
					internalOrgId = nil // personal
					internalFileId = &fileIDStr
				} else if link.OrgID != nil {
					// Org file, check if user is in org
					for _, orgIDStr := range claims.OrgIDs {
						orgID, err := uuid.Parse(orgIDStr)
						if err == nil && orgID == *link.OrgID {
							fileIDStr := file.ID.String()
							internalOrgId = &orgIDStr
							internalFileId = &fileIDStr
							break
						}
					}
				}
			}
		}
	}

	// Determine file type
	isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
	mimeType := getMimeType(file.Name)

	viewerSession := map[string]interface{}{
		"fileName":    file.Name,
		"fileSize":    file.Size,
		"downloadUrl": downloadPath,
		"token":       viewerToken,
		"capabilities": map[string]interface{}{
			"canEdit":     false,
			"canAnnotate": false,
			"isPdf":       isPdf,
			"mimeType":    mimeType,
		},
	}

	// Set view URL for PDFs, videos, audio, and documents (for inline viewing)
	if isPdf || strings.HasPrefix(mimeType, "video/") || strings.HasPrefix(mimeType, "audio/") || strings.HasPrefix(mimeType, "image/") {
		viewerSession["viewUrl"] = viewPath
	} else if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "word") || strings.Contains(mimeType, "spreadsheet") || strings.Contains(mimeType, "presentation") {
		// Use Collabora for document viewing
		wopiSrc := fmt.Sprintf("%s://go.b0esche.cloud/public/wopi/share/%s", scheme, token)
		editorUrl := getCollaboraEditorURL("https://of.b0esche.cloud")
		collaboraUrl := fmt.Sprintf("%s?WOPISrc=%s", editorUrl, url.QueryEscape(wopiSrc))
		viewerSession["viewUrl"] = collaboraUrl
	}

	// Add internal access info if user has access
	if internalFileId != nil {
		viewerSession["fileId"] = *internalFileId
		if internalOrgId != nil {
			viewerSession["orgId"] = *internalOrgId
		}
	}

	// Add CORS headers for public access
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Range")

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(viewerSession)
}

func publicFileDownloadHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, jwtManager *jwt.Manager) {
	token := chi.URLParam(r, "token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	viewerToken := r.URL.Query().Get("token")
	if viewerToken == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Viewer token required", http.StatusUnauthorized)
		return
	}

	link, err := db.GetFileShareLinkByToken(r.Context(), token)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Verify viewer token (contains org ID for org files, empty for personal)
	claims, err := jwtManager.Validate(viewerToken)
	if err != nil {
		errors.LogError(r, err, "Invalid viewer token")
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
		return
	}

	if link.OrgID == nil {
		if len(claims.OrgIDs) != 0 {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
	} else {
		if len(claims.OrgIDs) == 0 {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
		orgID, err := uuid.Parse(claims.OrgIDs[0])
		if err != nil {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
		if *link.OrgID != orgID {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
	}

	// Get file metadata
	file, err := db.GetFileByID(r.Context(), link.FileID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	if file.UserID == nil {
		errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
		return
	}

	// Check if it's a folder - if so, create a zip download
	if file.Type == "folder" {
		// Get all files under this folder path
		var folderFiles []database.File
		var err error

		if link.OrgID != nil {
			// Org folder - need user ID from context or file owner
			folderFiles, err = db.GetAllOrgFilesUnderPath(r.Context(), *link.OrgID, *file.UserID, file.Path)
		} else {
			// User folder
			folderFiles, err = db.GetAllUserFilesUnderPath(r.Context(), *file.UserID, file.Path)
		}

		if err != nil {
			errors.LogError(r, err, "Failed to get folder contents")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		// Filter out sub-folders (only include files)
		var filesToZip []database.File
		for _, f := range folderFiles {
			if f.Type == "file" {
				filesToZip = append(filesToZip, f)
			}
		}

		if len(filesToZip) == 0 {
			errors.WriteError(w, errors.CodeInvalidArgument, "Folder is empty", http.StatusBadRequest)
			return
		}

		// Create zip file in memory
		var zipBuffer bytes.Buffer
		zipWriter := zip.NewWriter(&zipBuffer)

		// Get WebDAV client for the file's owner
		client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
		if err != nil {
			errors.LogError(r, err, "Failed to get WebDAV client")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		// Add each file to the zip
		for _, fileToZip := range filesToZip {
			// Download file from storage
			downloadCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
			resp, err := client.Download(downloadCtx, fileToZip.Path, "")
			cancel()
			if err != nil {
				errors.LogError(r, err, fmt.Sprintf("Failed to download file %s for zip", fileToZip.Path))
				continue // Skip this file, continue with others
			}

			// Read file content
			fileData, err := io.ReadAll(resp.Body)
			resp.Body.Close()
			if err != nil {
				errors.LogError(r, err, fmt.Sprintf("Failed to read file %s for zip", fileToZip.Path))
				continue
			}

			// Create zip entry - use relative path within the folder
			relativePath := strings.TrimPrefix(fileToZip.Path, file.Path)
			if after, ok := strings.CutPrefix(relativePath, "/"); ok {
				relativePath = after
			}

			zipFile, err := zipWriter.Create(relativePath)
			if err != nil {
				errors.LogError(r, err, fmt.Sprintf("Failed to create zip entry for %s", relativePath))
				continue
			}

			// Write file data to zip
			_, err = zipFile.Write(fileData)
			if err != nil {
				errors.LogError(r, err, fmt.Sprintf("Failed to write file %s to zip", relativePath))
				continue
			}
		}

		// Close zip writer
		err = zipWriter.Close()
		if err != nil {
			errors.LogError(r, err, "Failed to close zip writer")
			errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
			return
		}

		// Add CORS headers for public access
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Range")

		// Set headers for zip download
		w.Header().Set("Content-Type", "application/zip")
		zipFilename := fmt.Sprintf("%s.zip", file.Name)
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", zipFilename))
		w.Header().Set("Content-Length", fmt.Sprintf("%d", zipBuffer.Len()))

		// Stream the zip file
		w.Write(zipBuffer.Bytes())
		return
	}

	// Determine MIME type
	mimeType := getMimeType(file.Name)

	// Get WebDAV client for the file's owner
	client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get WebDAV client")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Create context with longer timeout for file downloads
	downloadCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	// Stream file
	resp, err := client.Download(downloadCtx, file.Path, "")
	if err != nil {
		errors.LogError(r, err, "Failed to download file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	// Add CORS headers for public access
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Range")

	// Copy headers from Nextcloud response, but skip Content-Type to ensure correct MIME type
	for k, v := range resp.Header {
		if k != "Content-Type" {
			w.Header()[k] = v
		}
	}

	// Set correct Content-Type based on file extension
	w.Header().Set("Content-Type", mimeType)

	// Ensure download behavior
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Name))

	// Copy body
	io.Copy(w, resp.Body)
}

func publicFileViewHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, jwtManager *jwt.Manager) {
	token := chi.URLParam(r, "token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	viewerToken := r.URL.Query().Get("token")
	if viewerToken == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Viewer token required", http.StatusUnauthorized)
		return
	}

	link, err := db.GetFileShareLinkByToken(r.Context(), token)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Verify viewer token (contains org ID for org files, empty for personal)
	claims, err := jwtManager.Validate(viewerToken)
	if err != nil {
		errors.LogError(r, err, "Invalid viewer token")
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
		return
	}

	if link.OrgID == nil {
		if len(claims.OrgIDs) != 0 {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
	} else {
		if len(claims.OrgIDs) == 0 {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
		orgID, err := uuid.Parse(claims.OrgIDs[0])
		if err != nil {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
		if *link.OrgID != orgID {
			errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
			return
		}
	}

	// Get file metadata
	file, err := db.GetFileByID(r.Context(), link.FileID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	if file.UserID == nil {
		errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
		return
	}

	// Check if it's a folder - cannot view folders directly
	if file.Type == "folder" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Cannot view folders", http.StatusBadRequest)
		return
	}

	// Determine MIME type
	mimeType := getMimeType(file.Name)

	// Get WebDAV client for the file's owner
	client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get WebDAV client")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Create context with longer timeout for file downloads
	downloadCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	// Stream file
	resp, err := client.Download(downloadCtx, file.Path, r.Header.Get("Range"))
	if err != nil {
		errors.LogError(r, err, "Failed to download file")
		errors.WriteError(w, errors.CodeInternal, "File temporarily unavailable. Please try again later.", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	// Add CORS headers for public access
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Range")

	// Copy headers from Nextcloud response, but skip Content-Type to ensure correct MIME type
	for k, v := range resp.Header {
		if k != "Content-Type" {
			w.Header()[k] = v
		}
	}

	// Set correct Content-Type based on file extension
	w.Header().Set("Content-Type", mimeType)

	// Ensure inline viewing behavior (no Content-Disposition attachment)
	w.Header().Del("Content-Disposition")
	w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Name))

	// Set status code (200 or 206 for partial)
	w.WriteHeader(resp.StatusCode)

	// Copy body
	io.Copy(w, resp.Body)
}

func getUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to user
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.UserID == nil || *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	link, err := db.GetFileShareLinkByFileID(r.Context(), fileUUID)
	if err != nil {
		if err == sql.ErrNoRows {
			// No share link exists
			errors.WriteError(w, errors.CodeNotFound, "Share link not found", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Build full URL
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := "www.b0esche.cloud"
	fullURL := fmt.Sprintf("%s://%s/share/%s", scheme, host, link.Token)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"shareUrl": fullURL,
		"token":    link.Token,
	})
}

func createUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to user
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.UserID == nil || *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Revoke existing link if any
	db.RevokeFileShareLink(r.Context(), fileUUID) // Ignore error

	// Generate token
	token, err := storage.GenerateSecurePassword(48)
	if err != nil {
		errors.LogError(r, err, "Failed to generate token")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// If the file belongs to an org, prefer binding the share link to that org.
	// db.CreateFileShareLink will attempt to infer org_id from the file if nil is passed.
	link, err := db.CreateFileShareLink(r.Context(), token, fileUUID, nil, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to create share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	log.Printf("Share link created: user_id=%s, file_id=%s, org_id=%v", userID, fileUUID, link.OrgID)

	// Build full URL
	scheme := "https"
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		scheme = proto
	} else if r.TLS == nil {
		scheme = "http"
	}
	host := "www.b0esche.cloud"
	fullURL := fmt.Sprintf("%s://%s/share/%s", scheme, host, link.Token)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"shareUrl": fullURL,
		"token":    link.Token,
	})
}

func revokeUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, _ := middleware.GetUserID(r.Context())
	userID, _ := uuid.Parse(userIDStr)
	fileId := chi.URLParam(r, "fileId")

	fileUUID, err := uuid.Parse(fileId)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid file ID", http.StatusBadRequest)
		return
	}

	// Check if file exists and belongs to user
	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}
	if file.UserID == nil || *file.UserID != userID {
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	err = db.RevokeFileShareLink(r.Context(), fileUUID)
	if err != nil {
		errors.LogError(r, err, "Failed to revoke share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// publicWopiCheckFileInfoHandler handles GET /public/wopi/share/{token}
// Returns metadata about the shared file for Collabora viewer
func publicWopiCheckFileInfoHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
	token := chi.URLParam(r, "token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	// Get share link
	link, err := db.GetFileShareLinkByToken(r.Context(), token)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get file metadata
	file, err := db.GetFileByID(r.Context(), link.FileID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	if file.UserID == nil {
		errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
		return
	}

	lastModifiedTime := file.LastModified
	if lastModifiedTime.IsZero() {
		lastModifiedTime = time.Now()
	}

	response := struct {
		BaseFileName                   string   `json:"BaseFileName"`
		Size                           int64    `json:"Size"`
		OwnerId                        string   `json:"OwnerId"`
		Version                        string   `json:"Version"`
		SupportsExtendedLockLength     bool     `json:"SupportsExtendedLockLength"`
		SupportsGetLock                bool     `json:"SupportsGetLock"`
		SupportsLocks                  bool     `json:"SupportsLocks"`
		SupportsUpdate                 bool     `json:"SupportsUpdate"`
		UserId                         string   `json:"UserId"`
		UserFriendlyName               string   `json:"UserFriendlyName"`
		UserCanWrite                   bool     `json:"UserCanWrite"`
		UserCanNotWriteRelative        bool     `json:"UserCanNotWriteRelative"`
		ReadOnly                       bool     `json:"ReadOnly"`
		RestrictedWebViewOnly          bool     `json:"RestrictedWebViewOnly"`
		UserCanCreateRelativeToFolder  bool     `json:"UserCanCreateRelativeToFolder"`
		EnableOwnerTermination         bool     `json:"EnableOwnerTermination"`
		SupportsCobalt                 bool     `json:"SupportsCobalt"`
		SupportsDelete                 bool     `json:"SupportsDelete"`
		SupportsRename                 bool     `json:"SupportsRename"`
		SupportsRenameRelativeToFolder bool     `json:"SupportsRenameRelativeToFolder"`
		SupportsFolders                bool     `json:"SupportsFolders"`
		SupportsScenarios              []string `json:"SupportsScenarios"`
		LastModifiedTime               string   `json:"LastModifiedTime"`
		IsAnonymousUser                bool     `json:"IsAnonymousUser"`
		TimeZone                       string   `json:"TimeZone"`
	}{
		BaseFileName:                   file.Name,
		Size:                           file.Size,
		OwnerId:                        file.UserID.String(),
		Version:                        file.LastModified.UTC().Format(time.RFC3339),
		SupportsExtendedLockLength:     false,
		SupportsGetLock:                false,
		SupportsLocks:                  false,
		SupportsUpdate:                 false,
		UserId:                         "anonymous",
		UserFriendlyName:               "Anonymous User",
		UserCanWrite:                   false,
		UserCanNotWriteRelative:        true,
		ReadOnly:                       true, // Public sharing is read-only
		RestrictedWebViewOnly:          true, // Only allow web view
		UserCanCreateRelativeToFolder:  false,
		EnableOwnerTermination:         false,
		SupportsCobalt:                 false,
		SupportsDelete:                 false,
		SupportsRename:                 false,
		SupportsRenameRelativeToFolder: false,
		SupportsFolders:                false,
		SupportsScenarios:              []string{"embedview", "view"},
		LastModifiedTime:               lastModifiedTime.UTC().Format(time.RFC3339),
		IsAnonymousUser:                true,
		TimeZone:                       "UTC",
	}

	fmt.Printf("[PUBLIC-WOPI] CheckFileInfo: file=%s token=%s size=%d\n", file.ID.String(), token, file.Size)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(response)
}

// publicWopiGetFileHandler handles GET /public/wopi/share/{token}/contents
// Downloads the shared file content for Collabora
func publicWopiGetFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, jwtManager *jwt.Manager) {
	token := chi.URLParam(r, "token")
	if token == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
		return
	}

	fmt.Printf("[PUBLIC-WOPI-GetFile] START: token=%s\n", token)

	// Get share link
	link, err := db.GetFileShareLinkByToken(r.Context(), token)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get share link")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Get file metadata
	file, err := db.GetFileByID(r.Context(), link.FileID)
	if err != nil {
		errors.LogError(r, err, "Failed to get file")
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	if file.UserID == nil {
		errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
		return
	}

	// Get WebDAV client for the file's owner
	client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
	if err != nil {
		errors.LogError(r, err, "Failed to get WebDAV client")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Create context with longer timeout for file downloads
	downloadCtx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
	defer cancel()

	// Stream file
	resp, err := client.Download(downloadCtx, file.Path, r.Header.Get("Range"))
	if err != nil {
		errors.LogError(r, err, "Failed to download file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	// Copy headers from Nextcloud response
	for k, v := range resp.Header {
		w.Header()[k] = v
	}

	// Set status code (200 or 206 for partial)
	w.WriteHeader(resp.StatusCode)

	// Copy body
	io.Copy(w, resp.Body)
}

// getUserProfileHandler returns the current user's profile information
func getUserProfileHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	var user struct {
		ID          uuid.UUID  `json:"id"`
		Username    string     `json:"username"`
		Email       string     `json:"email"`
		DisplayName *string    `json:"displayName"`
		AvatarURL   *string    `json:"avatarUrl"`
		CreatedAt   time.Time  `json:"createdAt"`
		LastLoginAt *time.Time `json:"lastLoginAt"`
		UpdatedAt   *time.Time `json:"-"`
	}

	err = db.QueryRowContext(r.Context(),
		`SELECT id, username, email, display_name, avatar_url, created_at, last_login_at, updated_at
		 FROM users WHERE id = $1`, userID).
		Scan(&user.ID, &user.Username, &user.Email, &user.DisplayName, &user.AvatarURL, &user.CreatedAt, &user.LastLoginAt, &user.UpdatedAt)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "User not found", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get user profile")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// If avatar exists, return the backend URL instead of the internal WebDAV URL
	if user.AvatarURL != nil && *user.AvatarURL != "" {
		// Use updated_at for versioning if available to allow cache busting when avatar changes
		var v int64
		if user.UpdatedAt != nil {
			v = user.UpdatedAt.Unix()
		} else {
			v = time.Now().Unix()
		}
		// Include token in the avatar URL so frontends that cannot set headers (Image.network) can fetch it
		token, ok := middleware.GetToken(r.Context())
		if ok && token != "" {
			user.AvatarURL = &[]string{fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d&token=%s", v, token)}[0]
		} else {
			user.AvatarURL = &[]string{fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", v)}[0]
		}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

// updateUserProfileHandler updates the current user's profile information
func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	var req struct {
		DisplayName *string `json:"displayName"`
		Email       *string `json:"email"`
	}

	if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid JSON", http.StatusBadRequest)
		return
	}

	// Build dynamic update query
	var setParts []string
	var args []interface{}
	argIndex := 1

	if req.DisplayName != nil {
		setParts = append(setParts, fmt.Sprintf("display_name = $%d", argIndex))
		args = append(args, *req.DisplayName)
		argIndex++
	}
	if req.Email != nil {
		setParts = append(setParts, fmt.Sprintf("email = $%d", argIndex))
		args = append(args, *req.Email)
		argIndex++
	}

	if len(setParts) == 0 {
		// No fields to update
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(map[string]string{"message": "No changes to update"})
		return
	}

	setParts = append(setParts, "updated_at = NOW()")
	query := fmt.Sprintf("UPDATE users SET %s WHERE id = $%d", strings.Join(setParts, ", "), argIndex)
	args = append(args, userID)

	_, err = db.ExecContext(r.Context(), query, args...)
	if err != nil {
		errors.LogError(r, err, "Failed to update user profile")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Audit log
	metadata := make(map[string]interface{})
	if req.DisplayName != nil {
		metadata["displayName"] = *req.DisplayName
	}
	if req.Email != nil {
		metadata["email"] = *req.Email
	}

	auditLogger.Log(r.Context(), audit.Entry{
		UserID:   &userID,
		Action:   "profile_update",
		Success:  true,
		Metadata: metadata,
	})

	// Return updated profile JSON
	var updatedUser struct {
		ID          uuid.UUID  `json:"id"`
		Username    string     `json:"username"`
		Email       string     `json:"email"`
		DisplayName *string    `json:"displayName"`
		AvatarURL   *string    `json:"avatarUrl"`
		CreatedAt   time.Time  `json:"createdAt"`
		LastLoginAt *time.Time `json:"lastLoginAt"`
		UpdatedAt   *time.Time `json:"-"`
	}

	err = db.QueryRowContext(r.Context(),
		`SELECT id, username, email, display_name, avatar_url, created_at, last_login_at, updated_at
		 FROM users WHERE id = $1`, userID).
		Scan(&updatedUser.ID, &updatedUser.Username, &updatedUser.Email, &updatedUser.DisplayName, &updatedUser.AvatarURL, &updatedUser.CreatedAt, &updatedUser.LastLoginAt, &updatedUser.UpdatedAt)
	if err != nil {
		errors.LogError(r, err, "Failed to fetch updated user profile")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	if updatedUser.AvatarURL != nil && *updatedUser.AvatarURL != "" {
		var v int64
		if updatedUser.UpdatedAt != nil {
			v = updatedUser.UpdatedAt.Unix()
		} else {
			v = time.Now().Unix()
		}
		if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
			updatedUser.AvatarURL = &[]string{fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d&token=%s", v, token)}[0]
		} else {
			updatedUser.AvatarURL = &[]string{fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", v)}[0]
		}
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(updatedUser)
}

// changePasswordHandler changes the current user's password
func changePasswordHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	var req struct {
		CurrentPassword string `json:"currentPassword"`
		NewPassword     string `json:"newPassword"`
	}

	if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid JSON", http.StatusBadRequest)
		return
	}

	// For simplicity, since passwords are handled via passkeys, we'll just log and simulate
	// In a real implementation, verify current password and hash new one

	// Audit log
	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "password_change",
		Success: true,
	})

	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]string{"message": "Password changed"})
}

// uploadUserAvatarHandler handles avatar file uploads
func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Parse multipart form
	err = r.ParseMultipartForm(32 << 20) // 32MB max
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Failed to parse form", http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile("avatar")
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "No avatar file provided", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Validate file type
	contentType := header.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "image/") {
		errors.WriteError(w, errors.CodeInvalidArgument, "File must be an image", http.StatusBadRequest)
		return
	}

	// Validate file size (max 5MB)
	if header.Size > 5<<20 {
		errors.WriteError(w, errors.CodeInvalidArgument, "File too large (max 5MB)", http.StatusBadRequest)
		return
	}

	// Read file content
	fileBytes, err := io.ReadAll(file)
	if err != nil {
		errors.LogError(r, err, "Failed to read file")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Generate deterministic filename based on user ID
	ext := filepath.Ext(header.Filename)
	if ext == "" {
		ext = ".png" // default extension
	}
	filename := fmt.Sprintf("%s%s", userID.String(), ext)

	// Upload to Nextcloud at .avatars/<user-id>.<ext>
	client := storage.NewWebDAVClient(cfg)
	avatarPath := fmt.Sprintf(".avatars/%s", filename)
	err = client.Upload(r.Context(), avatarPath, bytes.NewReader(fileBytes), header.Size)
	if err != nil {
		errors.LogError(r, err, "Failed to upload avatar")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	webdavURL := fmt.Sprintf("%s/%s", client.BaseURL, avatarPath)

	// Update user profile with avatar URL and updated_at
	_, err = db.ExecContext(r.Context(),
		`UPDATE users SET avatar_url = $1, updated_at = NOW() WHERE id = $2`,
		webdavURL, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to update user avatar")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Build public URL including version based on updated_at and include token if available
	var version int64 = time.Now().Unix()
	// Try to use updated_at from DB to be more accurate
	var updatedAt time.Time
	err = db.QueryRowContext(r.Context(), `SELECT updated_at FROM users WHERE id = $1`, userID).Scan(&updatedAt)
	if err == nil {
		version = updatedAt.Unix()
	}
	// Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads
	versionStr := fmt.Sprintf("%d", version)
	cached := false
	if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil {
		fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err)
	} else {
		cached = true
	}

	// Audit log
	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "avatar_upload",
		Success: true,
		Metadata: map[string]interface{}{
			"filename": filename,
			"size":     header.Size,
		},
	})

	// Provide avatarData for immediate preview (small images only)
	avatarData := base64.StdEncoding.EncodeToString(fileBytes)

	publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version)
	if token, ok := middleware.GetToken(r.Context()); ok && token != "" {
		publicURL = fmt.Sprintf("%s&token=%s", publicURL, token)
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"message":     "Avatar uploaded successfully",
		"avatarUrl":   publicURL,
		"cached":      cached,
		"avatarData":  avatarData,
		"contentType": contentType,
	})
}

// getUserAvatarHandler serves the user's avatar image
func getUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, cfg *config.Config) {
	// Accept token via query param or Authorization header (Bearer)
	tokenString := r.URL.Query().Get("token")
	if tokenString == "" {
		authHeader := r.Header.Get("Authorization")
		if strings.HasPrefix(authHeader, "Bearer ") {
			tokenString = strings.TrimPrefix(authHeader, "Bearer ")
		}
	}
	if tokenString == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	claims, err := jwtManager.Validate(tokenString)
	if err != nil {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userIDStr := claims.UserID
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	var avatarURL *string
	err = db.QueryRowContext(r.Context(),
		`SELECT avatar_url FROM users WHERE id = $1`, userID).
		Scan(&avatarURL)
	if err != nil {
		if err == sql.ErrNoRows {
			errors.WriteError(w, errors.CodeNotFound, "User not found", http.StatusNotFound)
			return
		}
		errors.LogError(r, err, "Failed to get user avatar")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	if avatarURL == nil || *avatarURL == "" {
		// No avatar, return 404
		w.WriteHeader(http.StatusNotFound)
		return
	}

	v := r.URL.Query().Get("v")
	// If client supplied a version and we have a matching cache, serve it immediately to avoid hitting WebDAV
	if v != "" {
		if data, ct, cerr := readAvatarCache(cfg, userID.String(), v); cerr == nil {
			w.Header().Set("Content-Type", ct)
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.WriteHeader(http.StatusOK)
			w.Write(data)
			fmt.Printf("[INFO] Served avatar for user=%s from cache (v=%s) without contacting WebDAV\n", userID.String(), v)
			return
		}
	}

	// Download from WebDAV with retries and backoff
	client := storage.NewWebDAVClient(cfg)
	if client == nil {
		errors.WriteError(w, errors.CodeInternal, "WebDAV client not configured", http.StatusInternalServerError)
		return
	}
	remotePath := strings.TrimPrefix(*avatarURL, client.BaseURL+"/")

	// Configure retry & timeout from config
	timeoutSeconds := cfg.AvatarDownloadTimeoutSeconds
	if timeoutSeconds <= 0 {
		timeoutSeconds = 10
	}
	retries := cfg.AvatarDownloadRetries
	if retries < 0 {
		retries = 0
	}
	attempts := retries + 1 // total attempts

	var resp *http.Response
	var dlErr error
	for attempt := 0; attempt < attempts; attempt++ {
		ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds)*time.Second)
		resp, dlErr = client.Download(ctx, remotePath, "")
		cancel()
		if dlErr == nil {
			break
		}

		// If 404 on remote storage, the avatar file truly doesn't exist
		if dlErr != nil && strings.Contains(dlErr.Error(), "404") {
			fmt.Printf("[ERROR] Avatar not found on storage for remotePath=%s: %v\n", remotePath, dlErr)
			w.WriteHeader(http.StatusNotFound)
			return
		}

		// Log and apply backoff for retryable errors
		fmt.Printf("[WARN] Avatar download attempt %d/%d failed for remotePath=%s: %v\n", attempt+1, attempts, remotePath, dlErr)
		if attempt < attempts-1 {
			// exponential backoff: 500ms, 1s, 2s, ...
			backoffMs := 500 * (1 << attempt)
			time.Sleep(time.Duration(backoffMs) * time.Millisecond)
		}
	}

	if dlErr != nil || resp == nil {
		// Try to serve the latest cached avatar (if any) as a graceful fallback
		if data, ct, cerr := readAvatarCache(cfg, userID.String(), ""); cerr == nil {
			w.Header().Set("Content-Type", ct)
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.WriteHeader(http.StatusOK)
			w.Write(data)
			fmt.Printf("[INFO] Served latest cached avatar for user=%s after WebDAV failures\n", userID.String())
			return
		}

		// No cached avatar available; surface an explicit error (502) so clients can retry
		fmt.Printf("[ERROR] Avatar download failed after %d attempts for remotePath=%s: %v\n", attempts, remotePath, dlErr)
		w.Header().Set("Retry-After", "30")
		errors.WriteError(w, errors.CodeInternal, "Upstream storage unavailable", http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()

	// Read body (so we can cache it) and determine content-type
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		errors.LogError(r, err, "Failed to read avatar body")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	contentType := resp.Header.Get("Content-Type")
	if contentType == "" {
		contentType = mime.TypeByExtension(filepath.Ext(remotePath))
		if contentType == "" {
			contentType = "application/octet-stream"
		}
	}

	// Save to cache asynchronously (best-effort). Use provided v query param if present.
	v = r.URL.Query().Get("v")
	go func(v string) {
		if err := writeAvatarCache(cfg, userID.String(), v, filepath.Ext(remotePath), bodyBytes); err != nil {
			fmt.Printf("[WARN] failed to write avatar cache for user=%s v=%s: %v\n", userID.String(), v, err)
		}
	}(v)

	// Copy headers but ensure sensible caching
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Cache-Control", "public, max-age=300")
	w.WriteHeader(resp.StatusCode)
	w.Write(bodyBytes)
}

// deleteUserAccountHandler handles user account deletion
func deleteUserAccountHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) {
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Start transaction for atomic deletion
	tx, err := db.BeginTx(r.Context(), nil)
	if err != nil {
		errors.LogError(r, err, "Failed to start transaction")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback()

	// Get user details for audit logging
	var username string
	err = tx.QueryRowContext(r.Context(), "SELECT username FROM users WHERE id = $1", userID).Scan(&username)
	if err != nil {
		errors.LogError(r, err, "Failed to get user details")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Delete file shares (both personal and org shares)
	_, err = tx.ExecContext(r.Context(), `
		DELETE FROM file_share_links
		WHERE created_by_user_id = $1
	`, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to delete file shares")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Delete user files and their Nextcloud data
	rows, err := tx.QueryContext(r.Context(), `
		SELECT path FROM files WHERE user_id = $1 AND org_id IS NULL
	`, userID)
	if err != nil {
		errors.LogError(r, err, "Failed to get user files")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	// Delete files from Nextcloud storage
	client := storage.NewWebDAVClient(cfg)
	for rows.Next() {
		var filePath string
		if err := rows.Scan(&filePath); err != nil {
			continue // Skip on error
		}

		// Try to delete from Nextcloud (ignore errors as files might not exist)
		client.Delete(r.Context(), filePath)
	}

	// Delete database records
	_, err = tx.ExecContext(r.Context(), "DELETE FROM files WHERE user_id = $1 AND org_id IS NULL", userID)
	if err != nil {
		errors.LogError(r, err, "Failed to delete user files")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Remove user from all organizations (this will cascade to org files if needed)
	_, err = tx.ExecContext(r.Context(), "DELETE FROM memberships WHERE user_id = $1", userID)
	if err != nil {
		errors.LogError(r, err, "Failed to remove user from organizations")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Delete user sessions
	_, err = tx.ExecContext(r.Context(), "DELETE FROM sessions WHERE user_id = $1", userID)
	if err != nil {
		errors.LogError(r, err, "Failed to delete user sessions")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Finally, delete the user account
	_, err = tx.ExecContext(r.Context(), "DELETE FROM users WHERE id = $1", userID)
	if err != nil {
		errors.LogError(r, err, "Failed to delete user account")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Commit transaction
	if err = tx.Commit(); err != nil {
		errors.LogError(r, err, "Failed to commit transaction")
		errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
		return
	}

	// Audit log the account deletion
	auditLogger.Log(r.Context(), audit.Entry{
		UserID:  &userID,
		Action:  "account_delete",
		Success: true,
		Metadata: map[string]interface{}{
			"username": username,
		},
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"message": "Account deleted successfully",
	})
}
