package handlers

import (
	"context"
	"database/sql"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/gin-gonic/gin"

	"chat-switchboard/models"
	"chat-switchboard/store"
	"chat-switchboard/workspace"
)

// ── Request Types ───────────────────────────

type createWorkspaceRequest struct {
	Name      string `json:"name" binding:"required,max=200"`
	OwnerType string `json:"owner_type" binding:"required,oneof=user project channel team"`
	OwnerID   string `json:"owner_id" binding:"required"`
	MaxBytes  *int64 `json:"max_bytes,omitempty"`
}

type updateWorkspaceRequest = models.WorkspacePatch

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

// WorkspaceHandler handles workspace CRUD, file operations, and archive management.
type WorkspaceHandler struct {
	stores store.Stores
	wfs    *workspace.FS
}

// NewWorkspaceHandler creates a new workspace handler.
func NewWorkspaceHandler(s store.Stores, wfs *workspace.FS) *WorkspaceHandler {
	return &WorkspaceHandler{stores: s, wfs: wfs}
}

// ── Workspace CRUD ──────────────────────────

func (h *WorkspaceHandler) Create(c *gin.Context) {
	var req createWorkspaceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Authorization: verify the caller owns or can access the owner entity
	if !h.canAccessOwner(c, req.OwnerType, req.OwnerID) {
		c.JSON(http.StatusForbidden, gin.H{"error": "access denied"})
		return
	}

	w := &models.Workspace{
		OwnerType: req.OwnerType,
		OwnerID:   req.OwnerID,
		Name:      req.Name,
		MaxBytes:  req.MaxBytes,
		Status:    models.WorkspaceStatusActive,
	}

	// Pre-generate ID so root_path can be set before DB insert.
	// Works for both Postgres (overrides gen_random_uuid()) and SQLite (store.NewID()).
	w.ID = store.NewID()
	w.RootPath = "workspaces/" + w.ID

	if err := h.stores.Workspaces.Create(c.Request.Context(), w); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create workspace"})
		log.Printf("workspace create: %v", err)
		return
	}

	// Create directory on disk
	if err := h.wfs.CreateDir(w); err != nil {
		log.Printf("workspace mkdir: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create workspace directory"})
		return
	}

	c.JSON(http.StatusCreated, w)
}

func (h *WorkspaceHandler) Get(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	// Enrich with stats
	stats, err := h.stores.Workspaces.GetStats(c.Request.Context(), w.ID)
	if err == nil {
		w.FileCount = stats.FileCount
		w.TotalBytes = stats.TotalBytes
	}

	c.JSON(http.StatusOK, w)
}

// List returns all workspaces accessible to the current user.
// Includes user-owned workspaces and those owned by the user's teams.
func (h *WorkspaceHandler) List(c *gin.Context) {
	userID := getUserID(c)
	ctx := c.Request.Context()

	// Fetch user-owned workspaces
	userWs, err := h.stores.Workspaces.ListByOwner(ctx, "user", userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	all := make([]models.Workspace, 0, len(userWs))
	all = append(all, userWs...)

	// Also include team-owned workspaces
	teamIDs, _ := h.stores.Teams.GetUserTeamIDs(ctx, userID)
	for _, tid := range teamIDs {
		teamWs, err := h.stores.Workspaces.ListByOwner(ctx, "team", tid)
		if err == nil {
			all = append(all, teamWs...)
		}
	}

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

func (h *WorkspaceHandler) Update(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	var req updateWorkspaceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if err := h.stores.Workspaces.Update(c.Request.Context(), w.ID, req); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update workspace"})
		return
	}

	// Re-fetch to return the updated object
	updated, err := h.stores.Workspaces.GetByID(c.Request.Context(), w.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload workspace"})
		return
	}
	c.JSON(http.StatusOK, updated)
}

func (h *WorkspaceHandler) Delete(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	// Mark as deleting
	status := models.WorkspaceStatusDeleting
	h.stores.Workspaces.Update(c.Request.Context(), w.ID, models.WorkspacePatch{Status: &status})

	// Async cleanup (use background context — request ctx will be cancelled)
	go func() {
		ctx := context.Background()
		if err := h.wfs.Destroy(ctx, w); err != nil {
			log.Printf("workspace destroy %s: %v", w.ID, err)
		}
		h.stores.Workspaces.Delete(ctx, w.ID)
		log.Printf("workspace deleted: %s", w.ID)
	}()

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

// ── File Operations ─────────────────────────

func (h *WorkspaceHandler) ListFiles(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	path := c.Query("path")
	recursive := c.Query("recursive") == "true"

	files, err := h.wfs.ListDir(c.Request.Context(), w, path, recursive)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}
	if files == nil {
		files = []models.WorkspaceFile{}
	}
	c.JSON(http.StatusOK, gin.H{"data": files})
}

func (h *WorkspaceHandler) ReadFile(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	path := c.Query("path")
	if path == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "path parameter required"})
		return
	}

	rc, size, err := h.wfs.ReadFile(c.Request.Context(), w, path)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		}
		return
	}
	defer rc.Close()

	// Detect content type for response header
	f, _ := h.wfs.Stat(c.Request.Context(), w, path)
	contentType := "application/octet-stream"
	if f != nil && f.ContentType != "" {
		contentType = f.ContentType
	}

	c.Header("Content-Length", fmt.Sprintf("%d", size))
	c.DataFromReader(http.StatusOK, size, contentType, rc, nil)
}

func (h *WorkspaceHandler) WriteFile(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	path := c.Query("path")
	if path == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "path parameter required"})
		return
	}

	// Quota check
	stats, _ := h.stores.Workspaces.GetStats(c.Request.Context(), w.ID)
	quota := workspace.DefaultMaxBytes
	if w.MaxBytes != nil {
		quota = *w.MaxBytes
	}
	if stats != nil && stats.TotalBytes+c.Request.ContentLength > quota {
		c.JSON(http.StatusRequestEntityTooLarge, gin.H{
			"error": fmt.Sprintf("workspace quota exceeded (%d bytes max)", quota),
		})
		return
	}

	if err := h.wfs.WriteFile(c.Request.Context(), w, path, c.Request.Body, c.Request.ContentLength); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"ok": true, "path": path})
}

func (h *WorkspaceHandler) DeleteFileHandler(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	path := c.Query("path")
	if path == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "path parameter required"})
		return
	}

	recursive := c.Query("recursive") == "true"

	if err := h.wfs.DeleteFile(c.Request.Context(), w, path, recursive); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

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

func (h *WorkspaceHandler) Mkdir(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	path := c.Query("path")
	if path == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "path parameter required"})
		return
	}

	if err := h.wfs.Mkdir(c.Request.Context(), w, path); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, gin.H{"ok": true, "path": path})
}

// ── Archive Operations ──────────────────────

func (h *WorkspaceHandler) UploadArchive(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

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

	// Detect format from filename
	format := detectArchiveFormat(header.Filename)
	if format == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported archive format (use .zip or .tar.gz)"})
		return
	}

	// Save to temp file (archive extraction needs seekable file for zip)
	tmp, err := os.CreateTemp("", "ws-upload-*")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create temp file"})
		return
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)

	if _, err := io.Copy(tmp, file); err != nil {
		tmp.Close()
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save upload"})
		return
	}
	tmp.Close()

	count, err := h.wfs.ExtractArchive(c.Request.Context(), w, tmpName, format)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"ok":         true,
		"files_extracted": count,
	})
}

func (h *WorkspaceHandler) DownloadArchive(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	format := c.DefaultQuery("format", "zip")
	if format != "zip" && format != "tar.gz" && format != "tgz" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "format must be zip or tar.gz"})
		return
	}

	archivePath, err := h.wfs.CreateArchive(c.Request.Context(), w, format)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	defer os.Remove(archivePath)

	ext := "zip"
	contentType := "application/zip"
	if format == "tar.gz" || format == "tgz" {
		ext = "tar.gz"
		contentType = "application/gzip"
	}

	filename := fmt.Sprintf("%s.%s", w.Name, ext)
	c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
	c.File(archivePath)
	_ = contentType // c.File sets the content type from the file
}

// ── Reconcile + Stats ───────────────────────

func (h *WorkspaceHandler) Reconcile(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	added, removed, updated, err := h.wfs.Reconcile(c.Request.Context(), w)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"added":   added,
		"removed": removed,
		"updated": updated,
	})
}

func (h *WorkspaceHandler) Stats(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	stats, err := h.stores.Workspaces.GetStats(c.Request.Context(), w.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get stats"})
		return
	}

	c.JSON(http.StatusOK, stats)
}

// IndexStatus returns indexing progress for all files in the workspace (v0.21.2).
func (h *WorkspaceHandler) IndexStatus(c *gin.Context) {
	w, ok := h.loadAndAuthorize(c)
	if !ok {
		return
	}

	ctx := c.Request.Context()
	files, err := h.stores.Workspaces.ListFiles(ctx, w.ID, "", true)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
		return
	}

	// Aggregate counts by status
	counts := map[string]int{
		"pending": 0, "indexing": 0, "ready": 0,
		"error": 0, "skipped": 0,
	}
	totalChunks := 0
	for _, f := range files {
		if f.IsDirectory {
			continue
		}
		status := f.IndexStatus
		if status == "" {
			status = "pending"
		}
		counts[status]++
		totalChunks += f.ChunkCount
	}

	c.JSON(http.StatusOK, gin.H{
		"workspace_id":     w.ID,
		"indexing_enabled": w.IndexingEnabled,
		"status_counts":    counts,
		"total_chunks":     totalChunks,
	})
}

// ── Authorization Helpers ───────────────────

// loadAndAuthorize loads a workspace by :id param and checks user access.
func (h *WorkspaceHandler) loadAndAuthorize(c *gin.Context) (*models.Workspace, bool) {
	wsID := c.Param("id")
	ctx := c.Request.Context()

	w, err := h.stores.Workspaces.GetByID(ctx, wsID)
	if err != nil {
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, gin.H{"error": "workspace not found"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load workspace"})
		}
		return nil, false
	}

	if !h.canAccessOwner(c, w.OwnerType, w.OwnerID) {
		c.JSON(http.StatusForbidden, gin.H{"error": "access denied"})
		return nil, false
	}

	return w, true
}

// canAccessOwner checks if the current user can access the workspace's owner entity.
func (h *WorkspaceHandler) canAccessOwner(c *gin.Context, ownerType, ownerID string) bool {
	userID := getUserID(c)
	ctx := c.Request.Context()

	switch ownerType {
	case models.WorkspaceOwnerUser:
		return ownerID == userID

	case models.WorkspaceOwnerChannel:
		// User must own the channel
		ch, err := h.stores.Channels.GetByID(ctx, ownerID)
		if err != nil {
			return false
		}
		return ch.UserID == userID

	case models.WorkspaceOwnerProject:
		teamIDs, _ := h.stores.Teams.GetUserTeamIDs(ctx, userID)
		ok, _ := h.stores.Projects.UserCanAccess(ctx, userID, ownerID, teamIDs)
		return ok

	case models.WorkspaceOwnerTeam:
		// User must be a member of the team
		_, err := h.stores.Teams.GetMember(ctx, ownerID, userID)
		return err == nil

	default:
		return false
	}
}

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

// detectArchiveFormat returns "zip" or "tar.gz" based on filename, or "" if unsupported.
func detectArchiveFormat(filename string) string {
	lower := strings.ToLower(filename)
	if strings.HasSuffix(lower, ".zip") {
		return "zip"
	}
	if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
		return "tar.gz"
	}
	return ""
}
