package http

import (
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"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/models"
	"go.b0esche.cloud/backend/internal/storage"
	"go.b0esche.cloud/backend/pkg/jwt"
)

// Collabora discovery cache
var (
	collaboraEditorURL      string
	collaboraDiscoveryCache time.Time
	collaboraDiscoveryMu    sync.RWMutex
)

// getCollaboraEditorURL fetches the editor URL from Collabora's discovery endpoint
func getCollaboraEditorURL(collaboraBaseURL string) string {
	collaboraDiscoveryMu.RLock()
	// Cache for 5 minutes
	if collaboraEditorURL != "" && time.Since(collaboraDiscoveryCache) < 5*time.Minute {
		url := collaboraEditorURL
		collaboraDiscoveryMu.RUnlock()
		return url
	}
	collaboraDiscoveryMu.RUnlock()

	// Fetch discovery
	collaboraDiscoveryMu.Lock()
	defer collaboraDiscoveryMu.Unlock()

	// Double-check after acquiring write lock
	if collaboraEditorURL != "" && time.Since(collaboraDiscoveryCache) < 5*time.Minute {
		return collaboraEditorURL
	}

	discoveryURL := collaboraBaseURL + "/hosting/discovery"
	resp, err := http.Get(discoveryURL)
	if err != nil {
		fmt.Printf("[COLLABORA] Failed to fetch discovery: %v\n", err)
		// Fallback to guessed URL
		return collaboraBaseURL + "/browser/dist/cool.html"
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("[COLLABORA] Failed to read discovery: %v\n", err)
		return collaboraBaseURL + "/browser/dist/cool.html"
	}

	// Parse XML to extract urlsrc
	type Action struct {
		Name   string `xml:"name,attr"`
		Ext    string `xml:"ext,attr"`
		URLSrc string `xml:"urlsrc,attr"`
	}
	type App struct {
		Name    string   `xml:"name,attr"`
		Actions []Action `xml:"action"`
	}
	type NetZone struct {
		Apps []App `xml:"app"`
	}
	type WopiDiscovery struct {
		NetZone NetZone `xml:"net-zone"`
	}

	var discovery WopiDiscovery
	if err := xml.Unmarshal(body, &discovery); err != nil {
		fmt.Printf("[COLLABORA] Failed to parse discovery XML: %v\n", err)
		return collaboraBaseURL + "/browser/dist/cool.html"
	}

	// Find the first edit action URL (they all have the same base)
	for _, app := range discovery.NetZone.Apps {
		for _, action := range app.Actions {
			if action.URLSrc != "" {
				// Extract base URL (remove query string marker)
				url := strings.TrimSuffix(action.URLSrc, "?")
				collaboraEditorURL = url
				collaboraDiscoveryCache = time.Now()
				fmt.Printf("[COLLABORA] Discovered editor URL: %s\n", url)
				return url
			}
		}
	}

	fmt.Printf("[COLLABORA] No editor URL found in discovery\n")
	return collaboraBaseURL + "/browser/dist/cool.html"
}

// WOPILockManager manages file locks to prevent concurrent editing conflicts
type WOPILockManager struct {
	locks map[string]*models.WOPILockInfo
	mu    sync.RWMutex
}

var lockManager = &WOPILockManager{
	locks: make(map[string]*models.WOPILockInfo),
}

// AcquireLock tries to acquire a lock for a file
func (m *WOPILockManager) AcquireLock(fileID, userID string) (string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if existing, ok := m.locks[fileID]; ok {
		// Check if lock has expired
		if time.Now().Before(existing.ExpiresAt) {
			// Lock still active - check if same user
			if existing.UserID != userID {
				fmt.Printf("[WOPI-LOCK] Lock conflict: file=%s locked_by=%s requested_by=%s\n", fileID, existing.UserID, userID)
				return "", fmt.Errorf("file locked by another user")
			}
			// Same user, refresh the lock
			lockID := uuid.New().String()
			m.locks[fileID] = &models.WOPILockInfo{
				FileID:    fileID,
				UserID:    userID,
				LockID:    lockID,
				CreatedAt: time.Now(),
				ExpiresAt: time.Now().Add(30 * time.Minute),
			}
			fmt.Printf("[WOPI-LOCK] Lock refreshed: file=%s user=%s lock_id=%s\n", fileID, userID, lockID)
			return lockID, nil
		}
		// Lock expired, remove it
		delete(m.locks, fileID)
	}

	// Acquire new lock
	lockID := uuid.New().String()
	m.locks[fileID] = &models.WOPILockInfo{
		FileID:    fileID,
		UserID:    userID,
		LockID:    lockID,
		CreatedAt: time.Now(),
		ExpiresAt: time.Now().Add(30 * time.Minute),
	}
	fmt.Printf("[WOPI-LOCK] Lock acquired: file=%s user=%s lock_id=%s\n", fileID, userID, lockID)
	return lockID, nil
}

// ReleaseLock releases a lock for a file
func (m *WOPILockManager) ReleaseLock(fileID, userID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if lock, ok := m.locks[fileID]; ok {
		if lock.UserID == userID {
			delete(m.locks, fileID)
			fmt.Printf("[WOPI-LOCK] Lock released: file=%s user=%s\n", fileID, userID)
			return nil
		}
		return fmt.Errorf("lock held by different user")
	}
	return fmt.Errorf("no lock found")
}

// GetLock returns the current lock info for a file
func (m *WOPILockManager) GetLock(fileID string) *models.WOPILockInfo {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if lock, ok := m.locks[fileID]; ok {
		// Check if expired
		if time.Now().Before(lock.ExpiresAt) {
			return lock
		}
		// Expired, will be cleaned up on next acquire attempt
	}
	return nil
}

// validateWOPIAccessToken validates a WOPI access token
func validateWOPIAccessToken(tokenString string, jwtManager *jwt.Manager) (*jwt.Claims, error) {
	claims, err := jwtManager.Validate(tokenString)
	if err != nil {
		fmt.Printf("[WOPI-TOKEN] Token validation failed: %v\n", err)
		return nil, err
	}

	// Check if token has expired
	if time.Now().After(claims.ExpiresAt.Time) {
		fmt.Printf("[WOPI-TOKEN] Token expired: user=%s\n", claims.UserID)
		return nil, fmt.Errorf("token expired")
	}

	fmt.Printf("[WOPI-TOKEN] Token validated: user=%s expires=%v\n", claims.UserID, claims.ExpiresAt.Time)
	return claims, nil
}

// WOPICheckFileInfoHandler handles GET /wopi/files/{fileId}
// Returns metadata about the file and user permissions
func wopiCheckFileInfoHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

	// Get access token from query parameter
	accessToken := r.URL.Query().Get("access_token")
	if accessToken == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Missing access_token", http.StatusUnauthorized)
		return
	}

	// Validate token
	claims, err := validateWOPIAccessToken(accessToken, jwtManager)
	if err != nil {
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid or expired token", http.StatusUnauthorized)
		return
	}

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

	// Get file info from database
	fileUUID, err := uuid.Parse(fileID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid fileId format", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		fmt.Printf("[WOPI-REQUEST] File not found: file=%s error=%v\n", fileID, err)
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	fmt.Printf("[WOPI-CheckFileInfo] START: file=%s user=%s size=%d path=%s\n", fileID, userID.String(), file.Size, file.Path)

	// Get user info for UserFriendlyName
	user, err := db.GetUserByID(r.Context(), userID)
	if err != nil {
		fmt.Printf("[WOPI-REQUEST] Failed to get user info: user=%s error=%v\n", userID.String(), err)
		errors.WriteError(w, errors.CodeInternal, "Failed to get user info", http.StatusInternalServerError)
		return
	}

	// Verify user has access to this file
	canAccess := false
	var ownerID string

	if file.UserID != nil && *file.UserID == userID {
		canAccess = true
		ownerID = userID.String()
	} else if file.OrgID != nil {
		// Check if user is member of the org
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userID)
		if err == nil && member != nil {
			canAccess = true
			ownerID = file.OrgID.String()
		}
	}

	if !canAccess {
		fmt.Printf("[WOPI-REQUEST] Access denied: file=%s user=%s\n", fileID, userID.String())
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Ensure LastModifiedTime is not zero
	lastModifiedTime := file.LastModified
	if lastModifiedTime.IsZero() {
		lastModifiedTime = file.CreatedAt
	}
	if lastModifiedTime.IsZero() {
		lastModifiedTime = time.Now()
	}

	// Build response
	response := models.WOPICheckFileInfoResponse{
		BaseFileName:                   file.Name,
		Size:                           file.Size,
		Version:                        file.ID.String(),
		OwnerId:                        ownerID,
		UserId:                         userID.String(),
		UserFriendlyName:               user.DisplayName,
		UserCanWrite:                   true,
		UserCanRename:                  false,
		UserCanNotWriteRelative:        false,
		ReadOnly:                       false,
		RestrictedWebViewOnly:          false,
		UserCanCreateRelativeToFolder:  false,
		EnableOwnerTermination:         false,
		SupportsUpdate:                 true,
		SupportsCobalt:                 false,
		SupportsLocks:                  true,
		SupportsExtendedLockLength:     false,
		SupportsGetLock:                true,
		SupportsDelete:                 false,
		SupportsRename:                 false,
		SupportsRenameRelativeToFolder: false,
		SupportsFolders:                false,
		SupportsScenarios:              []string{"default"},
		LastModifiedTime:               lastModifiedTime.UTC().Format(time.RFC3339),
		IsAnonymousUser:                false,
		TimeZone:                       "UTC",
	}

	fmt.Printf("[WOPI-REQUEST] CheckFileInfo: file=%s user=%s size=%d\n", fileID, userID.String(), file.Size)

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

// WOPIGetFileHandler handles GET /wopi/files/{fileId}/contents
// Downloads the document file content
func wopiGetFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, cfg *config.Config) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

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

	// Get access token from query parameter
	accessToken := r.URL.Query().Get("access_token")
	if accessToken == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Missing access_token", http.StatusUnauthorized)
		return
	}

	// Validate token
	claims, err := validateWOPIAccessToken(accessToken, jwtManager)
	if err != nil {
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid or expired token", http.StatusUnauthorized)
		return
	}

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

	// Get file info from database
	fileUUID, err := uuid.Parse(fileID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid fileId format", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		fmt.Printf("[WOPI-REQUEST] GetFile - File not found: file=%s error=%v\n", fileID, err)
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Verify user has access to this file
	canAccess := false
	var webDAVClient *storage.WebDAVClient
	var remotePath string

	if file.UserID != nil && *file.UserID == userID {
		canAccess = true
		// Get user's WebDAV client - use config
		webDAVClient, err = getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
		if err != nil {
			fmt.Printf("[WOPI-STORAGE] Failed to get user WebDAV client: %v\n", err)
			errors.WriteError(w, errors.CodeInternal, "Storage error", http.StatusInternalServerError)
			return
		}
		// User files: path is relative to user's WebDAV root
		remotePath = file.Path
	} else if file.OrgID != nil {
		// Check if user is member of the org
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userID)
		if err == nil && member != nil {
			canAccess = true
			// Use user's WebDAV client for org files too
			webDAVClient, err = getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
			if err != nil {
				fmt.Printf("[WOPI-STORAGE] Failed to get user WebDAV client: %v\n", err)
				errors.WriteError(w, errors.CodeInternal, "Storage error", http.StatusInternalServerError)
				return
			}
			// Org files: stored under /orgs/{orgID}/ prefix
			rel := strings.TrimPrefix(file.Path, "/")
			remotePath = path.Join("/orgs", file.OrgID.String(), rel)
		}
	}

	if !canAccess {
		fmt.Printf("[WOPI-REQUEST] GetFile - Access denied: file=%s user=%s\n", fileID, userID.String())
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Download file from storage
	fmt.Printf("[WOPI-STORAGE] GetFile downloading: file=%s remotePath=%s\n", fileID, remotePath)
	resp, err := webDAVClient.Download(r.Context(), remotePath, "")
	if err != nil {
		fmt.Printf("[WOPI-STORAGE] Failed to download file: file=%s path=%s error=%v\n", fileID, file.Path, err)
		errors.WriteError(w, errors.CodeNotFound, "File not found in storage", http.StatusNotFound)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("[WOPI-STORAGE] Download response status: %d\n", resp.StatusCode)

	// Set response headers
	contentType := getMimeType(file.Name)
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", file.Size))
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Name))
	w.WriteHeader(http.StatusOK)

	fmt.Printf("[WOPI-STORAGE] GetFile: file=%s user=%s bytes=%d\n", fileID, userID.String(), file.Size)

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

// WOPIPutFileHandler handles POST /wopi/files/{fileId}/contents
// Uploads edited document back to storage
func wopiPutFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, cfg *config.Config) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

	// Get access token from Authorization header
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, "Bearer ") {
		errors.WriteError(w, errors.CodeUnauthenticated, "Missing authorization", http.StatusUnauthorized)
		return
	}
	accessToken := strings.TrimPrefix(authHeader, "Bearer ")

	// Validate token
	claims, err := validateWOPIAccessToken(accessToken, jwtManager)
	if err != nil {
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid or expired token", http.StatusUnauthorized)
		return
	}

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

	// Get file info from database
	fileUUID, err := uuid.Parse(fileID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid fileId format", http.StatusBadRequest)
		return
	}

	file, err := db.GetFileByID(r.Context(), fileUUID)
	if err != nil {
		fmt.Printf("[WOPI-REQUEST] PutFile - File not found: file=%s\n", fileID)
		errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
		return
	}

	// Verify user has access to this file
	canAccess := false
	var webDAVClient *storage.WebDAVClient
	var remotePath string

	if file.UserID != nil && *file.UserID == userID {
		canAccess = true
		webDAVClient, err = getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
		if err != nil {
			fmt.Printf("[WOPI-STORAGE] Failed to get user WebDAV client: %v\n", err)
			errors.WriteError(w, errors.CodeInternal, "Storage error", http.StatusInternalServerError)
			return
		}
		// User files: path is relative to user's WebDAV root
		remotePath = file.Path
	} else if file.OrgID != nil {
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userID)
		if err == nil && member != nil {
			canAccess = true
			// Use user's WebDAV client for org files too
			webDAVClient, err = getUserWebDAVClient(r.Context(), db, userID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
			if err != nil {
				fmt.Printf("[WOPI-STORAGE] Failed to get user WebDAV client: %v\n", err)
				errors.WriteError(w, errors.CodeInternal, "Storage error", http.StatusInternalServerError)
				return
			}
			// Org files: stored under /orgs/{orgID}/ prefix
			rel := strings.TrimPrefix(file.Path, "/")
			remotePath = path.Join("/orgs", file.OrgID.String(), rel)
		}
	}

	if !canAccess {
		fmt.Printf("[WOPI-REQUEST] PutFile - Access denied: file=%s user=%s\n", fileID, userID.String())
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Check lock
	lock := lockManager.GetLock(fileID)
	if lock != nil && lock.UserID != userID.String() {
		fmt.Printf("[WOPI-LOCK] Put conflict: file=%s locked_by=%s user=%s\n", fileID, lock.UserID, userID.String())
		w.WriteHeader(http.StatusConflict)
		return
	}

	// Read file content from request body
	content, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Printf("[WOPI-STORAGE] Failed to read request body: %v\n", err)
		errors.WriteError(w, errors.CodeInternal, "Failed to read content", http.StatusInternalServerError)
		return
	}
	defer r.Body.Close()

	// Upload to storage
	fmt.Printf("[WOPI-STORAGE] PutFile uploading: file=%s remotePath=%s\n", fileID, remotePath)
	err = webDAVClient.Upload(r.Context(), remotePath, strings.NewReader(string(content)), int64(len(content)))
	if err != nil {
		fmt.Printf("[WOPI-STORAGE] Failed to upload file: file=%s path=%s error=%v\n", fileID, file.Path, err)
		errors.WriteError(w, errors.CodeInternal, "Failed to save file", http.StatusInternalServerError)
		return
	}

	// Update file size and modification time in database
	newSize := int64(len(content))
	err = db.UpdateFileSize(r.Context(), fileUUID, newSize, &userID)
	if err != nil {
		fmt.Printf("[WOPI-STORAGE] Failed to update file size: file=%s error=%v\n", fileID, err)
		// Don't fail the upload, just log the warning
	}

	fmt.Printf("[WOPI-STORAGE] PutFile: file=%s user=%s bytes=%d\n", fileID, userID.String(), newSize)

	// Return response
	response := models.WOPIPutFileResponse{
		ItemVersion: fileUUID.String(),
	}

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

// WOPILockHandler handles POST /wopi/files/{fileId} with X-WOPI-Override header for lock operations
func wopiLockHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

	// Get access token from Authorization header
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, "Bearer ") {
		errors.WriteError(w, errors.CodeUnauthenticated, "Missing authorization", http.StatusUnauthorized)
		return
	}
	accessToken := strings.TrimPrefix(authHeader, "Bearer ")

	// Validate token
	claims, err := validateWOPIAccessToken(accessToken, jwtManager)
	if err != nil {
		errors.WriteError(w, errors.CodeUnauthenticated, "Invalid or expired token", http.StatusUnauthorized)
		return
	}

	userID := claims.UserID
	override := r.Header.Get("X-WOPI-Override")

	// Get file to verify access
	fileUUID, err := uuid.Parse(fileID)
	if err != nil {
		errors.WriteError(w, errors.CodeInvalidArgument, "Invalid fileId format", http.StatusBadRequest)
		return
	}

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

	// Verify access
	canAccess := false
	if file.UserID != nil && file.UserID.String() == userID {
		canAccess = true
	} else if file.OrgID != nil {
		userUUID, _ := uuid.Parse(userID)
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userUUID)
		canAccess = (err == nil && member != nil)
	}

	if !canAccess {
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Handle lock operations
	switch override {
	case "LOCK":
		// Acquire lock
		lockID, err := lockManager.AcquireLock(fileID, userID)
		if err != nil {
			fmt.Printf("[WOPI-LOCK] Lock acquisition failed: file=%s user=%s error=%s\n", fileID, userID, err.Error())
			w.WriteHeader(http.StatusConflict)
			w.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
			return
		}

		w.Header().Set("X-WOPI-LockID", lockID)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{}`))

	case "UNLOCK":
		// Release lock
		err := lockManager.ReleaseLock(fileID, userID)
		if err != nil {
			fmt.Printf("[WOPI-LOCK] Lock release failed: file=%s user=%s error=%s\n", fileID, userID, err.Error())
			w.WriteHeader(http.StatusConflict)
			w.Write([]byte(fmt.Sprintf(`{"error": "%s"}`, err.Error())))
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{}`))

	case "GET_LOCK":
		// Get lock info
		lock := lockManager.GetLock(fileID)
		if lock == nil {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(`{}`))
			return
		}

		w.Header().Set("X-WOPI-LockID", lock.LockID)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{}`))

	default:
		errors.WriteError(w, errors.CodeInvalidArgument, "Unknown X-WOPI-Override value", http.StatusBadRequest)
	}
}

// WOPISessionHandler handles POST /user/files/{fileId}/wopi-session and /orgs/{orgId}/files/{fileId}/wopi-session
// Returns WOPISrc URL and access token for opening document in Collabora
func wopiSessionHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, collaboraURL string) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

	// Get user from context (from auth middleware)
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok || userIDStr == "" {
		errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized)
		return
	}

	userID, _ := uuid.Parse(userIDStr)

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

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

	// Verify access
	canAccess := false
	if file.UserID != nil && *file.UserID == userID {
		canAccess = true
	} else if file.OrgID != nil {
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userID)
		canAccess = (err == nil && member != nil)
	}

	if !canAccess {
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Generate WOPI access token (1 hour duration)
	accessToken, err := jwtManager.GenerateWithDuration(userID.String(), nil, "", 1*time.Hour)
	if err != nil {
		errors.WriteError(w, errors.CodeInternal, "Failed to generate token", http.StatusInternalServerError)
		return
	}

	// Build WOPISrc URL
	wopisrc := fmt.Sprintf("https://go.b0esche.cloud/wopi/files/%s?access_token=%s", fileID, accessToken)

	response := models.WOPISessionResponse{
		WOPISrc:     wopisrc,
		AccessToken: accessToken,
	}

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

	fmt.Printf("[WOPI-REQUEST] Session created: file=%s user=%s\n", fileID, userID.String())
}

// CollaboraProxyHandler serves an HTML page that POSTs WOPISrc to Collabora
// This avoids CORS issues by having the POST originate from our domain
func collaboraProxyHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtManager *jwt.Manager, collaboraURL string) {
	fileID := r.PathValue("fileId")
	if fileID == "" {
		errors.WriteError(w, errors.CodeInvalidArgument, "Missing fileId", http.StatusBadRequest)
		return
	}

	// Get user from context (from auth middleware)
	userIDStr, ok := middleware.GetUserID(r.Context())
	if !ok {
		errors.WriteError(w, errors.CodeUnauthenticated, "Not authenticated", http.StatusUnauthorized)
		return
	}

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

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

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

	// Verify access
	canAccess := false
	if file.UserID != nil && *file.UserID == userID {
		canAccess = true
	} else if file.OrgID != nil {
		member, err := db.GetOrgMember(r.Context(), *file.OrgID, userID)
		canAccess = (err == nil && member != nil)
	}

	if !canAccess {
		errors.WriteError(w, errors.CodePermissionDenied, "Access denied", http.StatusForbidden)
		return
	}

	// Generate WOPI access token (1 hour duration)
	accessToken, err := jwtManager.GenerateWithDuration(userID.String(), nil, "", 1*time.Hour)
	if err != nil {
		errors.WriteError(w, errors.CodeInternal, "Failed to generate token", http.StatusInternalServerError)
		return
	}

	// Build WOPISrc URL (without access_token - that goes in a separate form field)
	wopiSrc := fmt.Sprintf("https://go.b0esche.cloud/wopi/files/%s", fileID)

	// Get the correct Collabora editor URL from discovery (includes version hash)
	editorURL := getCollaboraEditorURL(collaboraURL)

	// URL-encode the WOPISrc for use in the form action URL
	encodedWopiSrc := url.QueryEscape(wopiSrc)

	// Build the full Collabora URL with WOPISrc as query parameter
	// Collabora expects: cool.html?WOPISrc=<encoded-url>
	collaboraFullURL := fmt.Sprintf("%s?WOPISrc=%s", editorURL, encodedWopiSrc)

	// Return HTML page with auto-submitting form
	// The form POSTs to Collabora with access_token in the body
	// WOPISrc must be in the URL as a query parameter
	htmlContent := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
  <title>Loading Document...</title>
  <meta charset="utf-8">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%%; height: 100%%; overflow: hidden; }
    .loading { position: fixed; top: 50%%; left: 50%%; transform: translate(-50%%, -50%%); text-align: center; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
    .loading p { color: #666; margin-top: 10px; font-family: system-ui, sans-serif; }
  </style>
</head>
<body>
  <div class="loading">
    <p>Loading Collabora Online...</p>
  </div>
  <form method="POST" action="%s" target="_self" id="collaboraForm" style="display: none;">
    <input type="hidden" id="access_token" name="access_token" value="%s">
  </form>
  <script>
    // Auto-submit the form to Collabora
    var form = document.getElementById('collaboraForm');
    if (form) {
      console.log('[COLLABORA] Submitting form to %s');
      form.submit();
    } else {
      console.error('[COLLABORA] Form not found');
    }
  </script>
</body>
</html>`, collaboraFullURL, accessToken, collaboraFullURL)

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	// Don't set X-Frame-Options - this endpoint is meant to be loaded in an iframe
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(htmlContent))

	fmt.Printf("[COLLABORA-PROXY] Served HTML form: file=%s user=%s wopi_src=%s editor_url=%s\n", fileID, userID.String(), wopiSrc, collaboraFullURL)
}
