package handlers

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"chat-switchboard/extraction"
	"chat-switchboard/models"
	"chat-switchboard/storage"
	"chat-switchboard/store"
)

// ── Default Limits ─────────────────────────
// Overridable via global_settings keys.
const (
	defaultMaxFileSize          = 10 * 1024 * 1024 // 10 MB
	defaultMaxUploadSize        = 50 * 1024 * 1024 // 50 MB total per request (future: multi-file)
	defaultMaxFilesPerMsg = 5                 // future: multi-file per message
	defaultOrphanMaxAge         = 24 * time.Hour
)

// allowedMIMETypes is the default allowlist. Admin can override via global_settings.
var allowedMIMETypes = map[string]bool{
	// Images
	"image/jpeg": true, "image/png": true, "image/gif": true,
	"image/webp": true, "image/svg+xml": true,
	// Documents
	"application/pdf": true,
	"text/plain": true, "text/markdown": true, "text/csv": true,
	// Microsoft Office
	"application/vnd.openxmlformats-officedocument.wordprocessingml.document":   true,
	"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":         true,
	"application/vnd.openxmlformats-officedocument.presentationml.presentation": true,
	"application/msword": true, "application/vnd.ms-excel": true,
	// OpenDocument
	"application/vnd.oasis.opendocument.text":         true,
	"application/vnd.oasis.opendocument.spreadsheet":  true,
	"application/vnd.oasis.opendocument.presentation": true,
	// Other
	"application/rtf": true,
}

// ── Handler ────────────────────────────────

type FileHandler struct {
	stores   store.Stores
	objStore storage.ObjectStore
	extQueue *extraction.Queue // nil if extraction disabled
}

func NewFileHandler(stores store.Stores, objStore storage.ObjectStore, extQueue *extraction.Queue) *FileHandler {
	return &FileHandler{stores: stores, objStore: objStore, extQueue: extQueue}
}

// ── Upload ─────────────────────────────────
// POST /api/v1/channels/:id/files
// Multipart form: file field "file", returns file metadata.
func (h *FileHandler) Upload(c *gin.Context) {
	if h.objStore == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "file storage not configured"})
		return
	}

	userID := getUserID(c)
	channelID := c.Param("id")

	// Verify channel ownership
	if !h.verifyChannelAccess(c, channelID, userID) {
		return
	}

	// Parse multipart
	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing file field"})
		return
	}
	defer file.Close()

	// Size check
	if header.Size > defaultMaxFileSize {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("file too large (max %d MB)", defaultMaxFileSize/(1024*1024)),
		})
		return
	}

	// MIME detection: read first 512 bytes for sniffing, then reset
	buf := make([]byte, 512)
	n, _ := file.Read(buf)
	detectedType := http.DetectContentType(buf[:n])

	// Reset reader to start
	if _, err := file.Seek(0, io.SeekStart); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process file"})
		return
	}

	// Normalize MIME type (strip params like charset)
	contentType := detectedType
	if idx := strings.Index(contentType, ";"); idx > 0 {
		contentType = strings.TrimSpace(contentType[:idx])
	}

	// For types that DetectContentType can't distinguish (returns application/octet-stream),
	// fall back to extension-based detection
	if contentType == "application/octet-stream" {
		ext := strings.ToLower(filepath.Ext(header.Filename))
		if mapped, ok := extToMIME[ext]; ok {
			contentType = mapped
		}
	}

	// Allowlist check
	if !allowedMIMETypes[contentType] {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("file type %q not allowed", contentType),
		})
		return
	}

	// Build storage key: files are stored under files/{channel_id}/{file_id}_{filename}
	// We generate the ID first via a temp UUID, then use it in the key.
	att := &models.File{
		ChannelID:   channelID,
		UserID:      userID,
		Origin:      models.FileOriginUserUpload,
		Filename:    sanitizeFilename(header.Filename),
		ContentType: contentType,
		SizeBytes:   header.Size,
		DisplayHint: displayHintFor(contentType),
		Metadata: models.JSONMap{
			"extraction_status": "pending",
		},
	}

	// Create PG row first to get the UUID
	// storage_key is set after we have the ID
	att.StorageKey = "placeholder"
	if err := h.stores.Files.Create(c.Request.Context(), att); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create file record"})
		return
	}

	// Now build the real storage key and update
	att.StorageKey = fmt.Sprintf("files/%s/%s_%s", channelID, att.ID, att.Filename)

	// Write to object store
	if err := h.objStore.Put(c.Request.Context(), att.StorageKey, file, header.Size, contentType); err != nil {
		// Rollback PG row on storage failure
		h.stores.Files.Delete(c.Request.Context(), att.ID)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store file"})
		return
	}

	// Update storage_key
	h.stores.Files.UpdateStorageKey(c.Request.Context(), att.ID, att.StorageKey)

	// For images, mark extraction as not needed (complete immediately)
	if strings.HasPrefix(contentType, "image/") {
		h.stores.Files.UpdateMetadata(c.Request.Context(), att.ID, map[string]interface{}{
			"extraction_status": "complete",
		})
		att.Metadata["extraction_status"] = "complete"
	}

	// For text/plain, extract inline (trivial — just read the file)
	if contentType == "text/plain" || contentType == "text/markdown" || contentType == "text/csv" {
		if _, err := file.Seek(0, io.SeekStart); err == nil {
			if textBytes, err := io.ReadAll(file); err == nil {
				text := string(textBytes)
				h.stores.Files.SetExtractedText(c.Request.Context(), att.ID, text)
				h.stores.Files.UpdateMetadata(c.Request.Context(), att.ID, map[string]interface{}{
					"extraction_status": "complete",
				})
				att.Metadata["extraction_status"] = "complete"
			}
		}
	}

	// For documents requiring extraction (PDF, DOCX, etc.), enqueue for sidecar
	if extraction.IsExtractable(contentType) && h.extQueue != nil {
		if err := h.extQueue.Enqueue(att.ID, att.StorageKey, contentType, att.Filename); err != nil {
			log.Printf("extraction enqueue failed for %s: %v", att.ID, err)
			// Non-fatal: file is uploaded, just won't have extracted text
		}
	}

	c.JSON(http.StatusCreated, att)
}

// ── Download ───────────────────────────────
// GET /api/v1/files/:id/download
// Streams file content with auth check via channel membership.
func (h *FileHandler) Download(c *gin.Context) {
	if h.objStore == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "file storage not configured"})
		return
	}

	userID := getUserID(c)
	attID := c.Param("id")

	att, err := h.stores.Files.GetByID(c.Request.Context(), attID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
		return
	}

	// Channel-scoped access check
	if !h.verifyChannelAccess(c, att.ChannelID, userID) {
		return
	}

	reader, size, _, err := h.objStore.Get(c.Request.Context(), att.StorageKey)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
		return
	}
	defer reader.Close()

	c.Header("Content-Type", att.ContentType)
	c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, att.Filename))
	c.Header("Content-Length", fmt.Sprintf("%d", size))
	c.Status(http.StatusOK)
	io.Copy(c.Writer, reader)
}

// ── Get Metadata ───────────────────────────
// GET /api/v1/files/:id
func (h *FileHandler) GetMetadata(c *gin.Context) {
	userID := getUserID(c)
	attID := c.Param("id")

	att, err := h.stores.Files.GetByID(c.Request.Context(), attID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
		return
	}

	if !h.verifyChannelAccess(c, att.ChannelID, userID) {
		return
	}

	c.JSON(http.StatusOK, att)
}

// ── List Channel Files ───────────────
// GET /api/v1/channels/:id/files
func (h *FileHandler) ListByChannel(c *gin.Context) {
	userID := getUserID(c)
	channelID := c.Param("id")

	if !h.verifyChannelAccess(c, channelID, userID) {
		return
	}

	files, err := h.stores.Files.GetByChannel(c.Request.Context(), channelID, "")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}
	if files == nil {
		files = []models.File{}
	}

	c.JSON(http.StatusOK, gin.H{"files": files})
}

// ── List by Message ───────────────────────
// GET /api/v1/messages/:id/files
// Returns files attached to a specific message (inline artifacts, tool output).
func (h *FileHandler) ListByMessage(c *gin.Context) {
	messageID := c.Param("id")

	files, err := h.stores.Files.GetByMessage(c.Request.Context(), messageID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}
	if files == nil {
		files = []models.File{}
	}

	c.JSON(http.StatusOK, gin.H{"files": files})
}

// ── List by User (File Manager) ───────────
// GET /api/v1/files?page=1&per_page=50
// Paginated list of all files owned by the authenticated user.
func (h *FileHandler) ListByUser(c *gin.Context) {
	userID := getUserID(c)
	page, perPage, _ := parsePagination(c)

	files, total, err := h.stores.Files.GetByUser(c.Request.Context(), userID, page, perPage)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}
	if files == nil {
		files = []models.File{}
	}

	c.JSON(http.StatusOK, gin.H{
		"files":    files,
		"total":    total,
		"page":     page,
		"per_page": perPage,
	})
}

// ── Delete ─────────────────────────────────
// DELETE /api/v1/files/:id
func (h *FileHandler) Delete(c *gin.Context) {
	userID := getUserID(c)
	attID := c.Param("id")

	att, err := h.stores.Files.GetByID(c.Request.Context(), attID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
		return
	}

	if !h.verifyChannelAccess(c, att.ChannelID, userID) {
		return
	}

	// Delete from PG (returns the row for storage cleanup)
	deleted, err := h.stores.Files.Delete(c.Request.Context(), attID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete file"})
		return
	}

	// Clean up storage (async-safe: fire and forget with background context)
	if h.objStore != nil && deleted != nil {
		storageKey := deleted.StorageKey
		go func() {
			ctx := context.Background()
			if err := h.objStore.Delete(ctx, storageKey); err != nil {
				log.Printf("storage cleanup failed for %s: %v", storageKey, err)
			}
			// Also clean up thumbnail if it exists
			h.objStore.Delete(ctx, storageKey+"_thumb.jpg")
		}()
	}

	c.JSON(http.StatusOK, gin.H{"message": "file deleted"})
}

// ── Admin: Orphan Cleanup ──────────────────
// POST /admin/storage/cleanup
func (h *FileHandler) CleanupOrphans(c *gin.Context) {
	orphans, err := h.stores.Files.ListOrphans(c.Request.Context(), defaultOrphanMaxAge)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list orphans"})
		return
	}

	deleted := 0
	var freedBytes int64

	for _, att := range orphans {
		if _, err := h.stores.Files.Delete(c.Request.Context(), att.ID); err != nil {
			log.Printf("orphan cleanup: failed to delete %s from PG: %v", att.ID, err)
			continue
		}
		if h.objStore != nil {
			h.objStore.Delete(c.Request.Context(), att.StorageKey)
			h.objStore.Delete(c.Request.Context(), att.StorageKey+"_thumb.jpg")
		}
		deleted++
		freedBytes += att.SizeBytes
	}

	c.JSON(http.StatusOK, gin.H{
		"deleted":     deleted,
		"freed_bytes": freedBytes,
		"scanned":     len(orphans),
	})
}

// ── Admin: Orphan Count ────────────────────
// GET /admin/storage/orphans (for the admin panel card)
func (h *FileHandler) OrphanCount(c *gin.Context) {
	orphans, err := h.stores.Files.ListOrphans(c.Request.Context(), defaultOrphanMaxAge)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to count orphans"})
		return
	}

	var totalBytes int64
	for _, att := range orphans {
		totalBytes += att.SizeBytes
	}

	c.JSON(http.StatusOK, gin.H{
		"count":             len(orphans),
		"reclaimable_bytes": totalBytes,
	})
}

// ── Admin: Extraction Queue Status ─────────
// GET /admin/storage/extraction
func (h *FileHandler) ExtractionStatus(c *gin.Context) {
	if h.extQueue == nil {
		c.JSON(http.StatusOK, gin.H{
			"enabled": false,
			"items":   []interface{}{},
		})
		return
	}

	items, err := h.extQueue.ListAll()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list extraction queue"})
		return
	}
	if items == nil {
		items = []extraction.QueueItem{}
	}

	// Count by status
	counts := map[string]int{}
	for _, item := range items {
		counts[item.Status]++
	}

	c.JSON(http.StatusOK, gin.H{
		"enabled": true,
		"total":   len(items),
		"counts":  counts,
		"items":   items,
	})
}

// ── Channel Delete Hook ────────────────────
// Called by ChannelHandler.DeleteChannel to clean up storage.
func (h *FileHandler) CleanupChannelStorage(channelID string) {
	if h.objStore == nil {
		return
	}
	// CASCADE already deleted PG rows. Clean up filesystem.
	prefix := fmt.Sprintf("files/%s", channelID)
	if err := h.objStore.DeletePrefix(context.Background(), prefix); err != nil {
		log.Printf("storage cleanup for channel %s failed: %v", channelID, err)
	}
}

// ── Helpers ────────────────────────────────

// verifyChannelAccess checks that the requesting user owns the channel.
// Future RBAC (v0.20.0): replace with rbac.Can(userID, channelID, permission).
func (h *FileHandler) verifyChannelAccess(c *gin.Context, channelID, userID string) bool {
	owns, err := h.stores.Channels.UserOwns(c.Request.Context(), channelID, userID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return false
	}
	if !owns {
		// Check if user is admin (admins can access any channel)
		role, _ := c.Get("role")
		if role != "admin" {
			c.JSON(http.StatusForbidden, gin.H{"error": "access denied"})
			return false
		}
	}
	return true
}

// sanitizeFilename cleans a filename for safe storage.
func sanitizeFilename(name string) string {
	// Take only the base name (strip path separators)
	name = filepath.Base(name)
	// Replace problematic characters
	replacer := strings.NewReplacer(
		"/", "_", "\\", "_", "..", "_", "\x00", "",
	)
	name = replacer.Replace(name)
	if name == "" || name == "." {
		name = "unnamed"
	}
	// Truncate to 200 chars (leave room for UUID prefix in storage key)
	if len(name) > 200 {
		ext := filepath.Ext(name)
		name = name[:200-len(ext)] + ext
	}
	return name
}

// ── Project Files (v0.22.4) ────────────────

// UploadToProject handles project-scoped file uploads.
// POST /api/v1/projects/:id/files
func (h *FileHandler) UploadToProject(c *gin.Context) {
	if h.objStore == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "file storage not configured"})
		return
	}

	userID := getUserID(c)
	projectID := c.Param("id")

	// Verify project access (user owns or is team member; admins bypass)
	if c.GetString("role") != "admin" {
		teamIDs, _ := h.stores.Teams.GetUserTeamIDs(c.Request.Context(), userID)
		ok, err := h.stores.Projects.UserCanAccess(c.Request.Context(), userID, projectID, teamIDs)
		if err != nil || !ok {
			c.JSON(http.StatusForbidden, gin.H{"error": "project not found or access denied"})
			return
		}
	}

	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no file uploaded"})
		return
	}
	defer file.Close()

	if header.Size > int64(defaultMaxFileSize) {
		c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file exceeds size limit"})
		return
	}

	// Detect content type
	buf := make([]byte, 512)
	n, _ := file.Read(buf)
	contentType := http.DetectContentType(buf[:n])
	file.Seek(0, io.SeekStart)

	ext := strings.ToLower(filepath.Ext(header.Filename))
	if better, ok := extToMIME[ext]; ok && contentType == "application/octet-stream" {
		contentType = better
	}

	// Store file
	storageKey := fmt.Sprintf("projects/%s/%s/%s", projectID, store.NewID()[:8], sanitizeFilename(header.Filename))
	if err := h.objStore.Put(c.Request.Context(), storageKey, file, header.Size, contentType); err != nil {
		log.Printf("error: project file upload: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store file"})
		return
	}

	att := models.File{
		ChannelID:   "", // no channel association
		UserID:      userID,
		ProjectID:   &projectID,
		Origin:      models.FileOriginUserUpload,
		Filename:    header.Filename,
		ContentType: contentType,
		SizeBytes:   header.Size,
		StorageKey:  storageKey,
		DisplayHint: displayHintFor(contentType),
	}

	if err := h.stores.Files.Create(c.Request.Context(), &att); err != nil {
		log.Printf("error: project file create: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save file metadata"})
		return
	}

	if h.extQueue != nil {
		if err := h.extQueue.Enqueue(att.ID, att.StorageKey, contentType, att.Filename); err != nil {
			log.Printf("warning: project file extraction enqueue failed: %v", err)
		}
	}

	c.JSON(http.StatusCreated, att)
}

// ListByProject returns all files uploaded to a project.
// GET /api/v1/projects/:id/files
func (h *FileHandler) ListByProject(c *gin.Context) {
	userID := getUserID(c)
	projectID := c.Param("id")

	// Admins bypass project ownership/membership checks
	if c.GetString("role") != "admin" {
		teamIDs, _ := h.stores.Teams.GetUserTeamIDs(c.Request.Context(), userID)
		ok, err := h.stores.Projects.UserCanAccess(c.Request.Context(), userID, projectID, teamIDs)
		if err != nil || !ok {
			c.JSON(http.StatusForbidden, gin.H{"error": "project not found or access denied"})
			return
		}
	}

	files, err := h.stores.Files.GetByProject(c.Request.Context(), projectID)
	if err != nil {
		log.Printf("error: list project files: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}
	if files == nil {
		files = []models.File{}
	}

	c.JSON(http.StatusOK, gin.H{"files": files, "count": len(files)})
}

// extToMIME maps file extensions to MIME types for cases where
// http.DetectContentType returns application/octet-stream.
var extToMIME = map[string]string{
	".pdf":  "application/pdf",
	".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
	".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
	".doc":  "application/msword",
	".xls":  "application/vnd.ms-excel",
	".odt":  "application/vnd.oasis.opendocument.text",
	".ods":  "application/vnd.oasis.opendocument.spreadsheet",
	".odp":  "application/vnd.oasis.opendocument.presentation",
	".rtf":  "application/rtf",
	".md":   "text/markdown",
	".csv":  "text/csv",
	".svg":  "image/svg+xml",
}

// displayHintFor returns the appropriate display hint for a content type.
func displayHintFor(contentType string) string {
	switch {
	case strings.HasPrefix(contentType, "image/"):
		return models.FileHintInline
	case strings.HasPrefix(contentType, "video/"):
		return models.FileHintInline
	case strings.HasPrefix(contentType, "audio/"):
		return models.FileHintInline
	case contentType == "application/pdf":
		return models.FileHintThumbnail
	case strings.HasPrefix(contentType, "text/"):
		return models.FileHintInline
	case contentType == "application/json":
		return models.FileHintInline
	default:
		return models.FileHintDownload
	}
}
