package handlers

import (
	"database/sql"
	"net/http"

	"github.com/gin-gonic/gin"

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

// ── Channel Participants Handler ──────────────
// ICD §3.7: Manages polymorphic participants per channel.
// Routes:
//   GET    /channels/:id/participants                — list
//   POST   /channels/:id/participants                — add
//   PATCH  /channels/:id/participants/:participantId — update role
//   DELETE /channels/:id/participants/:participantId — remove
//   GET    /channels/:id/presence                    — who's online

type ParticipantHandler struct {
	stores store.Stores
}

func NewParticipantHandler(stores store.Stores) *ParticipantHandler {
	return &ParticipantHandler{stores: stores}
}

// ── List ─────────────────────────────────────

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

	// Caller must be a participant (any role)
	if !h.requireParticipant(c, channelID, userID) {
		return
	}

	participants, err := h.stores.Channels.ListParticipants(c.Request.Context(), channelID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list participants"})
		return
	}
	if participants == nil {
		participants = []models.ChannelParticipant{}
	}
	c.JSON(http.StatusOK, gin.H{"data": participants})
}

// ── Add ──────────────────────────────────────

type addParticipantRequest struct {
	ParticipantType string `json:"participant_type" binding:"required"`
	ParticipantID   string `json:"participant_id" binding:"required"`
	Role            string `json:"role"`
}

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

	// Only owners can add participants
	if !h.requireOwner(c, channelID, userID) {
		return
	}

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

	// Validate participant_type
	switch req.ParticipantType {
	case "user", "persona", "session":
		// valid
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "participant_type must be user, persona, or session"})
		return
	}

	// Default role
	role := req.Role
	if role == "" {
		role = "member"
	}
	switch role {
	case "owner", "member", "observer":
		// valid
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be owner, member, or observer"})
		return
	}

	// Build participant
	p := &models.ChannelParticipant{
		ChannelID:       channelID,
		ParticipantType: req.ParticipantType,
		ParticipantID:   req.ParticipantID,
		Role:            role,
	}

	// If adding a persona, resolve display_name and avatar from persona record,
	// and auto-add persona's model to channel_models roster.
	if req.ParticipantType == "persona" {
		persona, err := h.stores.Personas.GetByID(c.Request.Context(), req.ParticipantID)
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, gin.H{"error": "persona not found"})
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up persona"})
			return
		}

		dn := persona.Name
		p.DisplayName = &dn
		if persona.Avatar != "" {
			p.AvatarURL = &persona.Avatar
		}

		// Auto-add persona's model to channel_models roster
		cm := &models.ChannelModel{
			ChannelID:        channelID,
			ModelID:          persona.BaseModelID,
			ProviderConfigID: derefStr(persona.ProviderConfigID),
			PersonaID:        &req.ParticipantID,
			Handle:           persona.Handle,
			DisplayName:      persona.Name,
			SystemPrompt:     persona.SystemPrompt,
			IsDefault:        false,
		}
		_ = h.stores.Channels.SetModel(c.Request.Context(), cm)

	} else if req.ParticipantType == "user" {
		// Resolve user display name
		user, err := h.stores.Users.GetByID(c.Request.Context(), req.ParticipantID)
		if err == sql.ErrNoRows {
			c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up user"})
			return
		}
		dn := user.DisplayName
		if dn == "" {
			dn = user.Username
		}
		p.DisplayName = &dn
		if user.AvatarURL != "" {
			p.AvatarURL = &user.AvatarURL
		}
	}

	if err := h.stores.Channels.AddParticipant(c.Request.Context(), p); err != nil {
		// Check for duplicate
		if isDuplicateErr(err) {
			c.JSON(http.StatusConflict, gin.H{"error": "participant already in channel"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add participant"})
		return
	}

	// Return updated participant list
	participants, _ := h.stores.Channels.ListParticipants(c.Request.Context(), channelID)
	c.JSON(http.StatusCreated, gin.H{"data": participants})
}

// ── Update Role ─────────────────────────────

type updateParticipantRequest struct {
	Role string `json:"role" binding:"required"`
}

func (h *ParticipantHandler) Update(c *gin.Context) {
	channelID := c.Param("id")
	participantRecordID := c.Param("participantId")
	userID := getUserID(c)

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

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

	switch req.Role {
	case "owner", "member", "observer":
		// valid
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be owner, member, or observer"})
		return
	}

	// Verify participant belongs to this channel
	p, err := h.stores.Channels.GetParticipantByID(c.Request.Context(), participantRecordID)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "participant not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up participant"})
		return
	}
	if p.ChannelID != channelID {
		c.JSON(http.StatusForbidden, gin.H{"error": "participant does not belong to this channel"})
		return
	}

	if err := h.stores.Channels.UpdateParticipantRole(c.Request.Context(), participantRecordID, req.Role); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update participant"})
		return
	}

	participants, _ := h.stores.Channels.ListParticipants(c.Request.Context(), channelID)
	c.JSON(http.StatusOK, gin.H{"data": participants})
}

// ── Remove ──────────────────────────────────

func (h *ParticipantHandler) Remove(c *gin.Context) {
	channelID := c.Param("id")
	participantRecordID := c.Param("participantId")
	userID := getUserID(c)

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

	// Verify participant belongs to this channel
	p, err := h.stores.Channels.GetParticipantByID(c.Request.Context(), participantRecordID)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "participant not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up participant"})
		return
	}
	if p.ChannelID != channelID {
		c.JSON(http.StatusForbidden, gin.H{"error": "participant does not belong to this channel"})
		return
	}

	// Cannot remove the last owner
	if p.Role == "owner" {
		owners, _ := h.stores.Channels.CountParticipantsByRole(c.Request.Context(), channelID, "owner")
		if owners <= 1 {
			c.JSON(http.StatusConflict, gin.H{"error": "cannot remove the last owner"})
			return
		}
	}

	// v0.23.2: DM guard — cannot drop below 2 human participants
	channelType, _, _ := h.stores.Channels.GetTypeAndTeamID(c.Request.Context(), channelID)

	if channelType == "dm" && p.ParticipantType == "user" {
		userCount, _ := h.stores.Channels.CountParticipantsByType(c.Request.Context(), channelID, "user")
		if userCount <= 2 {
			c.JSON(http.StatusConflict, gin.H{"error": "DMs require at least 2 participants"})
			return
		}
	}

	// v0.23.2: Group guard — cannot remove the last persona
	if channelType == "group" && p.ParticipantType == "persona" {
		personaCount, _ := h.stores.Channels.CountParticipantsByType(c.Request.Context(), channelID, "persona")
		if personaCount <= 1 {
			c.JSON(http.StatusConflict, gin.H{"error": "group chats require at least 1 persona"})
			return
		}
	}

	// If removing a persona, also remove its auto-created model roster entry
	if p.ParticipantType == "persona" {
		_ = h.stores.Channels.DeleteModelByPersona(c.Request.Context(), channelID, p.ParticipantID)
	}

	if err := h.stores.Channels.RemoveParticipant(c.Request.Context(), participantRecordID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove participant"})
		return
	}

	participants, _ := h.stores.Channels.ListParticipants(c.Request.Context(), channelID)
	c.JSON(http.StatusOK, gin.H{"data": participants})
}

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

// requireParticipant checks the user is any participant in the channel.
func (h *ParticipantHandler) requireParticipant(c *gin.Context, channelID, userID string) bool {
	ok, err := h.stores.Channels.IsParticipant(c.Request.Context(), channelID, "user", userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check participation"})
		return false
	}
	if !ok {
		// Fall back to legacy channels.user_id ownership for direct channels
		if userOwnsChannel(c, channelID, userID) {
			return true
		}
		return false
	}
	return true
}

// requireOwner checks the user is an owner participant in the channel.
func (h *ParticipantHandler) requireOwner(c *gin.Context, channelID, userID string) bool {
	role, err := h.stores.Channels.GetParticipantRole(c.Request.Context(), channelID, "user", userID)
	if err != nil {
		// Fall back to legacy channels.user_id ownership for direct channels
		if userOwnsChannel(c, channelID, userID) {
			return true
		}
		return false
	}
	if role != "owner" {
		c.JSON(http.StatusForbidden, gin.H{"error": "owner role required"})
		return false
	}
	return true
}

func derefStr(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}

// isDuplicateErr detects unique constraint violations across both Postgres and SQLite.
func isDuplicateErr(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	// Postgres: "duplicate key value violates unique constraint"
	// SQLite: "UNIQUE constraint failed"
	return contains(msg, "duplicate key") || contains(msg, "UNIQUE constraint")
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && searchStr(s, substr)
}

func searchStr(s, sub string) bool {
	for i := 0; i <= len(s)-len(sub); i++ {
		if s[i:i+len(sub)] == sub {
			return true
		}
	}
	return false
}
