package http

import (
	"archive/zip"
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"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
}

// 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
		fmt.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.RateLimit)

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

	// 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")
		})

		// 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)
				})
				// 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)
			})
		})
	}) // Close protected routes

	return r
}

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 = "/"
	}
	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
	}

	// 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
	}

	// 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)
}

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

	members, err := db.GetOrgMembers(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
	}

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

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 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 = "/"
		}
		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
	}

	// 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
	}

	// 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 = "/"
		}
		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
	}

	// 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
	}

	// 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", "*")
	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", "*")
	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"
	}
}
