package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

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

// ── Request / Response types ────────────────

type createChannelRequest struct {
	Title            string   `json:"title" binding:"required,max=500"`
	Type             string   `json:"type,omitempty"`  // direct (default), dm, group, channel, workflow
	Description      string   `json:"description,omitempty"`
	Model            string   `json:"model,omitempty"`
	SystemPrompt     string   `json:"system_prompt,omitempty"`
	ProviderConfigID *string  `json:"provider_config_id,omitempty"`
	Folder           string   `json:"folder,omitempty"`
	FolderID         *string  `json:"folder_id,omitempty"`
	Tags             []string `json:"tags,omitempty"`
	// v0.23.2: DM creation
	Participants []string `json:"participants,omitempty"` // user IDs for DM (exactly 1 other user)
	AiMode       string   `json:"ai_mode,omitempty"`      // auto (default), mention_only, off
}

type updateChannelRequest struct {
	Title            *string          `json:"title,omitempty"`
	Description      *string          `json:"description,omitempty"`
	Model            *string          `json:"model,omitempty"`
	SystemPrompt     *string          `json:"system_prompt,omitempty"`
	ProviderConfigID *string          `json:"provider_config_id,omitempty"`
	IsArchived       *bool            `json:"is_archived,omitempty"`
	IsPinned         *bool            `json:"is_pinned,omitempty"`
	Folder           *string          `json:"folder,omitempty"`
	FolderID         *string          `json:"folder_id,omitempty"`
	Tags             []string         `json:"tags,omitempty"`
	Settings         *json.RawMessage `json:"settings,omitempty"`     // JSONB merge into existing settings
	WorkspaceID      *string          `json:"workspace_id,omitempty"` // bind workspace (v0.21.5)
	AiMode           *string          `json:"ai_mode,omitempty"`      // v0.23.2: auto, mention_only, off
	Topic            *string          `json:"topic,omitempty"`        // v0.23.2: channel topic
}

type channelResponse struct {
	ID               string          `json:"id"`
	UserID           string          `json:"user_id"`
	Title            string          `json:"title"`
	Type             string          `json:"type"`
	AiMode           string          `json:"ai_mode,omitempty"`
	Topic            *string         `json:"topic,omitempty"`
	Description      *string         `json:"description"`
	Model            *string         `json:"model"`
	ProviderConfigID *string         `json:"provider_config_id"`
	SystemPrompt     *string         `json:"system_prompt"`
	IsArchived       bool            `json:"is_archived"`
	IsPinned         bool            `json:"is_pinned"`
	Folder           *string         `json:"folder"`
	FolderID         *string         `json:"folder_id,omitempty"`
	ProjectID        *string         `json:"project_id,omitempty"`
	WorkspaceID      *string         `json:"workspace_id,omitempty"`
	Tags             []string        `json:"tags"`
	Settings         json.RawMessage `json:"settings,omitempty"`
	MessageCount     int             `json:"message_count"`
	UnreadCount      int             `json:"unread_count,omitempty"`
	CreatedAt        string          `json:"created_at"`
	UpdatedAt        string          `json:"updated_at"`
}

type paginatedResponse struct {
	Data       interface{} `json:"data"`
	Page       int         `json:"page"`
	PerPage    int         `json:"per_page"`
	Total      int         `json:"total"`
	TotalPages int         `json:"total_pages"`
}

// ChannelHandler holds dependencies for channel endpoints.
type ChannelHandler struct {
	stores store.Stores
}

// NewChannelHandler creates a new channel handler.
func NewChannelHandler(stores store.Stores) *ChannelHandler {
	return &ChannelHandler{stores: stores}
}

// channelDeleteHook is called after a channel is successfully deleted.
// Set at startup by SetChannelDeleteHook to clean up storage files.
var channelDeleteHook func(channelID string)

// SetChannelDeleteHook registers a callback invoked after channel deletion.
// Used to clean up channel files on the storage backend.
func SetChannelDeleteHook(fn func(channelID string)) {
	channelDeleteHook = fn
}

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

// getUserID extracts the authenticated user's ID from context.
func getUserID(c *gin.Context) string {
	uid, _ := c.Get("user_id")
	s, _ := uid.(string)
	return s
}

// isSessionAuth returns true if the request is from a session participant (v0.24.3).
func isSessionAuth(c *gin.Context) bool {
	return c.GetString("auth_type") == "session"
}

// sessionCanAccessChannel validates that a session participant is authorized
// for the given channel.
func sessionCanAccessChannel(c *gin.Context, channelID string) bool {
	if !isSessionAuth(c) {
		return false
	}
	return c.GetString("channel_id") == channelID
}

// parsePagination reads page and per_page from query params with defaults.
func parsePagination(c *gin.Context) (page, perPage, offset int) {
	page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
	perPage, _ = strconv.Atoi(c.DefaultQuery("per_page", "50"))

	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 50
	}
	if perPage > 100 {
		perPage = 100
	}
	offset = (page - 1) * perPage
	return
}

// listItemToResponse converts a store.ChannelListItem to a handler response.
func listItemToResponse(item store.ChannelListItem) channelResponse {
	tags := item.Tags
	if tags == nil {
		tags = []string{}
	}
	return channelResponse{
		ID:               item.ID,
		UserID:           item.UserID,
		Title:            item.Title,
		Type:             item.Type,
		AiMode:           item.AiMode,
		Topic:            item.Topic,
		Description:      item.Description,
		Model:            item.Model,
		ProviderConfigID: item.ProviderConfigID,
		SystemPrompt:     item.SystemPrompt,
		IsArchived:       item.IsArchived,
		IsPinned:         item.IsPinned,
		Folder:           item.Folder,
		FolderID:         item.FolderID,
		ProjectID:        item.ProjectID,
		WorkspaceID:      item.WorkspaceID,
		Tags:             tags,
		Settings:         item.Settings,
		MessageCount:     item.MessageCount,
		UnreadCount:      item.UnreadCount,
		CreatedAt:        item.CreatedAt,
		UpdatedAt:        item.UpdatedAt,
	}
}

// ── List Channels ───────────────────────────

func (h *ChannelHandler) ListChannels(c *gin.Context) {
	userID := getUserID(c)
	page, perPage, offset := parsePagination(c)

	// Optional filters
	archived := c.DefaultQuery("archived", "false")

	var channelTypes []string
	if raw := c.Query("types"); raw != "" {
		for _, t := range strings.Split(raw, ",") {
			t = strings.TrimSpace(t)
			if t != "" {
				channelTypes = append(channelTypes, t)
			}
		}
	}
	if len(channelTypes) == 0 {
		if ct := c.DefaultQuery("type", ""); ct != "" {
			channelTypes = []string{ct}
		}
	}

	filter := store.ChannelListFilter{
		ListOptions: store.ListOptions{Limit: perPage, Offset: offset},
		Archived:    archived == "true",
		Types:       channelTypes,
		Folder:      c.Query("folder"),
		FolderID:    c.Query("folder_id"),
		Search:      strings.TrimSpace(c.Query("search")),
		ProjectID:   c.Query("project_id"),
	}

	items, total, err := h.stores.Channels.ListFiltered(c.Request.Context(), userID, filter)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list channels"})
		return
	}

	channels := make([]channelResponse, 0, len(items))
	for _, item := range items {
		channels = append(channels, listItemToResponse(item))
	}

	// Resolve DM titles: show the other participant's name, not the creator's label
	resolveDMTitles(c.Request.Context(), channels, userID)

	SafeJSON(c, http.StatusOK, paginatedResponse{
		Data:       channels,
		Page:       page,
		PerPage:    perPage,
		Total:      total,
		TotalPages: int(math.Ceil(float64(total) / float64(perPage))),
	})
}

// ── Create Channel ──────────────────────────

func (h *ChannelHandler) CreateChannel(c *gin.Context) {
	userID := getUserID(c)

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

	if req.Tags == nil {
		req.Tags = []string{}
	}

	// Default type is "direct" (1:1 AI chat)
	channelType := req.Type
	if channelType == "" {
		channelType = "direct"
	}

	// Default ai_mode: mention_only for DMs, auto for everything else
	aiMode := req.AiMode
	if aiMode == "" {
		if channelType == "dm" {
			aiMode = "mention_only"
		} else {
			aiMode = "auto"
		}
	}

	ctx := c.Request.Context()

	// ── DM dedup: check for existing DM between these two users ──
	if channelType == "dm" && len(req.Participants) == 1 {
		otherUserID := req.Participants[0]
		if otherUserID == userID {
			c.JSON(http.StatusBadRequest, gin.H{"error": "cannot create DM with yourself"})
			return
		}

		existingID, _ := h.stores.Channels.FindExistingDM(ctx, userID, otherUserID)
		if existingID != "" {
			// Return existing channel instead of creating duplicate
			item, err := h.stores.Channels.GetForUser(ctx, existingID, userID)
			if err == nil {
				SafeJSON(c, http.StatusOK, listItemToResponse(*item)) // 200, not 201 — existing resource
				return
			}
			// If read fails, fall through and create new (shouldn't happen)
		}
	}

	// Build channel model
	ch := &models.Channel{
		UserID:           userID,
		Title:            req.Title,
		Type:             channelType,
		Description:      req.Description,
		Model:            req.Model,
		SystemPrompt:     req.SystemPrompt,
		ProviderConfigID: req.ProviderConfigID,
		FolderID:         req.FolderID,
	}

	dmPartners := []string{}
	if channelType == "dm" {
		dmPartners = req.Participants
	}

	defaultConfigID := ""
	if req.ProviderConfigID != nil {
		defaultConfigID = *req.ProviderConfigID
	}

	if err := h.stores.Channels.CreateFull(ctx, ch, req.Folder, req.Tags, aiMode,
		userID, dmPartners, req.Model, defaultConfigID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
		return
	}

	// Fetch full response with message count
	item, err := h.stores.Channels.GetForUser(ctx, ch.ID, userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read created channel"})
		return
	}

	SafeJSON(c, http.StatusCreated, listItemToResponse(*item))
}

// ── Get Channel ─────────────────────────────

func (h *ChannelHandler) GetChannel(c *gin.Context) {
	userID := getUserID(c)
	channelID := c.Param("id")

	item, err := h.stores.Channels.GetForUser(c.Request.Context(), channelID, userID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}

	SafeJSON(c, http.StatusOK, listItemToResponse(*item))
}

// ── Update Channel ──────────────────────────

func (h *ChannelHandler) UpdateChannel(c *gin.Context) {
	userID := getUserID(c)
	channelID := c.Param("id")
	ctx := c.Request.Context()

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

	// Verify ownership (participants can only move to folder)
	owns, err := h.stores.Channels.UserOwns(ctx, channelID, userID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}
	if !owns {
		// Participants may move channels to their own folders
		folderOnly := req.FolderID != nil && req.Title == nil && req.Description == nil &&
			req.Model == nil && req.SystemPrompt == nil && req.ProviderConfigID == nil &&
			req.IsArchived == nil && req.IsPinned == nil && req.Folder == nil &&
			req.Tags == nil && req.WorkspaceID == nil && req.AiMode == nil &&
			req.Topic == nil && req.Settings == nil
		if !folderOnly {
			c.JSON(http.StatusForbidden, gin.H{"error": "not your channel"})
			return
		}
		// Verify they are a participant
		canAccess, _ := h.stores.Channels.UserCanAccess(ctx, channelID, userID)
		if !canAccess {
			c.JSON(http.StatusForbidden, gin.H{"error": "not your channel"})
			return
		}
	}

	// Build fields map for store.Update
	fields := map[string]interface{}{}

	if req.Title != nil {
		fields["title"] = *req.Title
	}
	if req.Description != nil {
		fields["description"] = *req.Description
	}
	if req.Model != nil {
		fields["model"] = *req.Model
	}
	if req.SystemPrompt != nil {
		fields["system_prompt"] = *req.SystemPrompt
	}
	if req.ProviderConfigID != nil {
		if *req.ProviderConfigID == "" {
			fields["provider_config_id"] = nil
		} else {
			fields["provider_config_id"] = *req.ProviderConfigID
		}
	}
	if req.IsArchived != nil {
		fields["is_archived"] = *req.IsArchived
	}
	if req.IsPinned != nil {
		fields["is_pinned"] = *req.IsPinned
	}
	if req.Folder != nil {
		fields["folder"] = *req.Folder
	}
	if req.FolderID != nil {
		if *req.FolderID == "" {
			fields["folder_id"] = nil // unbind from folder
		} else {
			fields["folder_id"] = *req.FolderID
		}
	}
	if req.Tags != nil {
		fields["tags"] = req.Tags
	}
	if req.WorkspaceID != nil {
		if *req.WorkspaceID == "" {
			fields["workspace_id"] = nil // unbind
		} else {
			fields["workspace_id"] = *req.WorkspaceID
		}
	}
	if req.AiMode != nil {
		mode := *req.AiMode
		if mode != "auto" && mode != "mention_only" && mode != "off" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "ai_mode must be auto, mention_only, or off"})
			return
		}
		fields["ai_mode"] = mode
	}
	if req.Topic != nil {
		fields["topic"] = *req.Topic
	}

	hasFields := len(fields) > 0
	hasSettings := req.Settings != nil

	if !hasFields && !hasSettings {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
		return
	}

	if hasSettings {
		if !json.Valid([]byte(*req.Settings)) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "settings must be valid JSON"})
			return
		}
	}

	if hasFields {
		if err := h.stores.Channels.Update(ctx, channelID, fields); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update channel"})
			return
		}
	}

	if hasSettings {
		if err := h.stores.Channels.MergeSettings(ctx, channelID, json.RawMessage(*req.Settings)); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update settings"})
			return
		}
	}

	// Return updated channel
	h.GetChannel(c)
}

// ── Delete Channel ──────────────────────────

func (h *ChannelHandler) DeleteChannel(c *gin.Context) {
	userID := getUserID(c)
	channelID := c.Param("id")
	ctx := c.Request.Context()

	// Check ownership first (without deleting)
	owns, err := h.stores.Channels.UserOwns(ctx, channelID, userID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}
	if !owns {
		// Not the owner — if participant, leave the channel instead of deleting.
		res, _ := database.DB.ExecContext(ctx, database.Q(`
			DELETE FROM channel_participants
			WHERE channel_id = $1 AND participant_type = 'user' AND participant_id = $2
		`), channelID, userID)
		if rows, _ := res.RowsAffected(); rows > 0 {
			c.JSON(http.StatusOK, gin.H{"message": "left channel"})
			return
		}
		c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"})
		return
	}

	// Check if retention policy applies (global/team provider + TTL > 0)
	if h.shouldRetain(ctx, channelID) {
		ttl := h.retentionTTL(ctx)
		purgeAfter := time.Now().Add(time.Duration(ttl) * 24 * time.Hour)
		if err := h.stores.Channels.ArchiveForRetention(ctx, channelID, purgeAfter); err != nil {
			log.Printf("[retention] ArchiveForRetention(%s) error: %v", channelID, err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to archive channel"})
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"message":     "channel archived for retention",
			"purge_after": purgeAfter.UTC().Format("2006-01-02T15:04:05Z"),
		})
		return
	}

	// Exempt — hard delete
	n, err := h.stores.Channels.DeleteByOwner(ctx, channelID, userID)
	if err != nil || n == 0 {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete channel"})
		return
	}

	if channelDeleteHook != nil {
		go channelDeleteHook(channelID)
	}

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

// shouldRetain returns true if a channel uses a global or team provider
// and the retention TTL is configured (> 0).
func (h *ChannelHandler) shouldRetain(ctx context.Context, channelID string) bool {
	ttl := h.retentionTTL(ctx)
	if ttl <= 0 {
		return false
	}

	// Only BYOK (personal) provider channels are exempt.
	// All others — global, team, or no explicit provider — follow retention.
	var providerConfigID *string
	_ = database.DB.QueryRowContext(ctx, database.Q(`
		SELECT provider_config_id FROM channels WHERE id = $1
	`), channelID).Scan(&providerConfigID)

	if providerConfigID == nil || *providerConfigID == "" {
		return true // no explicit provider → defaults to global → retain
	}

	cfg, err := h.stores.Providers.GetByID(ctx, *providerConfigID)
	if err != nil {
		return true // provider deleted (FK SET NULL already handled above) → retain
	}

	return cfg.Scope != models.ScopePersonal
}

// retentionTTL reads the configured retention TTL in days from global settings.
func (h *ChannelHandler) retentionTTL(ctx context.Context) int {
	cfg, err := h.stores.GlobalConfig.Get(ctx, "retention_ttl_days")
	if err != nil {
		return 0
	}
	if v, ok := cfg["value"].(float64); ok {
		return int(v)
	}
	return 0
}

// ── Mark Read (v0.23.2) ───────────────────────

func (h *ChannelHandler) MarkRead(c *gin.Context) {
	userID := getUserID(c)
	channelID := c.Param("id")

	err := h.stores.Channels.MarkRead(c.Request.Context(), channelID, userID)
	if err != nil {
		// Participant row may not exist for legacy direct chats — that's OK
	}

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

// ── DM Title Resolution ──────────────────────

// resolveDMTitles replaces DM channel titles with the other participant's
// display name so each user sees "DM <other_person>" instead of the
// creator's original label.
func resolveDMTitles(ctx context.Context, channels []channelResponse, viewerID string) {
	// Collect DM channel IDs
	var dmIDs []string
	dmIdx := map[string]int{} // channel_id → index in channels slice
	for i, ch := range channels {
		if ch.Type == "dm" {
			dmIDs = append(dmIDs, ch.ID)
			dmIdx[ch.ID] = i
		}
	}
	if len(dmIDs) == 0 {
		return
	}

	// Query: for each DM, get the OTHER participant's display name
	// Uses channel_participants to find the peer, then joins users for name
	placeholders := make([]string, len(dmIDs))
	args := make([]interface{}, 0, len(dmIDs)+1)
	for i, id := range dmIDs {
		placeholders[i] = fmt.Sprintf("$%d", i+1)
		args = append(args, id)
	}
	args = append(args, viewerID)
	viewerPlaceholder := fmt.Sprintf("$%d", len(args))

	query := database.Q(fmt.Sprintf(`
		SELECT cp.channel_id,
		       COALESCE(NULLIF(u.display_name, ''), u.username, 'Unknown')
		FROM channel_participants cp
		JOIN users u ON u.id = cp.participant_id
		WHERE cp.channel_id IN (%s)
		  AND cp.participant_type = 'user'
		  AND cp.participant_id != %s
		LIMIT %d
	`, strings.Join(placeholders, ","), viewerPlaceholder, len(dmIDs)))

	rows, err := database.DB.QueryContext(ctx, query, args...)
	if err != nil {
		return // non-fatal: keep original titles
	}
	defer rows.Close()

	for rows.Next() {
		var chID, peerName string
		if rows.Scan(&chID, &peerName) == nil {
			if idx, ok := dmIdx[chID]; ok {
				channels[idx].Title = "DM " + peerName
			}
		}
	}
}
