package httpx

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo/options"

	"bountyboard/internal/chat"
	"bountyboard/internal/domain"
	"bountyboard/internal/files"
	"bountyboard/internal/store"
	"bountyboard/internal/ws"
)

func (s *Server) routesMessages(mux *http.ServeMux) {
	mux.Handle("GET /api/v1/conversations", s.requireAuth(http.HandlerFunc(s.handleListConversations)))
	mux.Handle("POST /api/v1/conversations", s.authed(s.handleCreateConversation))
	mux.Handle("GET /api/v1/conversations/{id}/messages", s.requireAuth(http.HandlerFunc(s.handleListMessages)))
	mux.Handle("POST /api/v1/conversations/{id}/messages", s.authed(s.handleSendMessage))
	mux.Handle("POST /api/v1/conversations/{id}/read", s.authed(s.handleMarkRead))
	mux.Handle("GET /api/v1/conversations/{id}/members", s.requireAuth(http.HandlerFunc(s.handleListMembers)))
	mux.Handle("POST /api/v1/conversations/{id}/members", s.authed(s.handleAddMember))
	mux.Handle("DELETE /api/v1/conversations/{id}/members/{userId}", s.authed(s.handleRemoveMember))
	mux.Handle("POST /api/v1/files", s.authed(s.handleUploadFile))
	mux.Handle("GET /api/v1/users", s.requireAuth(http.HandlerFunc(s.handleSearchUsers)))
}

// conversation loads + authorizes participant access.
func (s *Server) conversation(w http.ResponseWriter, r *http.Request, id string) (*store.Conversation, bool) {
	conv, err := s.store.ConversationByID(r.Context(), id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "conversation not found")
		} else {
			s.internalError(w, r, "load conversation", err)
		}
		return nil, false
	}
	if !conv.HasParticipant(CurrentUser(r.Context()).ID) {
		writeError(w, http.StatusForbidden, "forbidden", "you are not in this conversation")
		return nil, false
	}
	return conv, true
}

func (s *Server) handleListConversations(w http.ResponseWriter, r *http.Request) {
	me := CurrentUser(r.Context())
	convs, err := s.store.ListConversations(r.Context(), me.ID)
	if err != nil {
		s.internalError(w, r, "list conversations", err)
		return
	}
	ids := make([]string, len(convs))
	for i, c := range convs {
		ids[i] = c.ID
	}
	unread, err := s.store.UnreadCounts(r.Context(), me.ID, ids)
	if err != nil {
		s.internalError(w, r, "unread counts", err)
		return
	}
	// resolve display names for the list
	out := make([]map[string]any, len(convs))
	for i, c := range convs {
		title := c.Title
		if c.Kind == "dm" {
			for _, pid := range c.ParticipantIDs {
				if pid != me.ID {
					if u, err := s.store.UserByID(r.Context(), pid); err == nil {
						title = u.Name
					}
				}
			}
		}
		if title == "" {
			title = "Conversation"
		}
		out[i] = map[string]any{
			"id": c.ID, "kind": c.Kind, "title": title,
			"customerId": c.CustomerID, "participantIds": c.ParticipantIDs,
			"creatorId":     c.CreatorID,
			"lastMessageAt": c.LastMessageAt, "unread": unread[c.ID],
		}
	}
	writeJSON(w, http.StatusOK, map[string]any{"conversations": out})
}

func (s *Server) handleCreateConversation(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Kind           string   `json:"kind"`
		Title          string   `json:"title"`
		CustomerID     string   `json:"customerId"`
		ParticipantIDs []string `json:"participantIds"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	me := CurrentUser(r.Context())

	// validate participants exist
	seen := map[string]bool{me.ID: true}
	participants := []string{me.ID}
	for _, id := range req.ParticipantIDs {
		if seen[id] {
			continue
		}
		if _, err := s.store.UserByID(r.Context(), id); err != nil {
			writeError(w, http.StatusBadRequest, "bad_participant", "unknown user "+id)
			return
		}
		seen[id] = true
		participants = append(participants, id)
	}

	switch req.Kind {
	case "dm":
		if len(participants) != 2 {
			writeError(w, http.StatusBadRequest, "bad_request", "dm needs exactly one other participant")
			return
		}
		conv, err := s.store.FindOrCreateDM(r.Context(), participants[0], participants[1])
		if err != nil {
			s.internalError(w, r, "create dm", err)
			return
		}
		writeJSON(w, http.StatusOK, map[string]any{"conversation": conv})
		return
	case "group", "project":
		if len(participants) < 2 {
			writeError(w, http.StatusBadRequest, "bad_request", "need at least one other participant")
			return
		}
		conv := &store.Conversation{
			Kind:           req.Kind,
			Title:          strings.TrimSpace(req.Title),
			CreatorID:      me.ID,
			ParticipantIDs: participants,
		}
		if req.Kind == "project" {
			if req.CustomerID == "" {
				writeError(w, http.StatusBadRequest, "bad_request", "project conversations need customerId")
				return
			}
			if _, err := s.store.CustomerByID(r.Context(), req.CustomerID); err != nil {
				writeError(w, http.StatusBadRequest, "bad_customer", "unknown customer")
				return
			}
			conv.CustomerID = req.CustomerID
		}
		if conv.Title == "" && req.Kind == "group" {
			writeError(w, http.StatusBadRequest, "bad_request", "group conversations need a title")
			return
		}
		if err := s.store.CreateConversation(r.Context(), conv); err != nil {
			s.internalError(w, r, "create conversation", err)
			return
		}
		writeJSON(w, http.StatusCreated, map[string]any{"conversation": conv})
		return
	default:
		writeError(w, http.StatusBadRequest, "bad_kind", "kind must be dm, group or project")
	}
}

func (s *Server) handleListMessages(w http.ResponseWriter, r *http.Request) {
	conv, ok := s.conversation(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	q := r.URL.Query()
	msgs, err := s.store.ListMessages(r.Context(), conv.ID, q.Get("cursor"),
		atoiDefault(q.Get("limit"), 50))
	if err != nil {
		s.internalError(w, r, "list messages", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"messages": msgs})
}

func (s *Server) handleSendMessage(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Body        string   `json:"body"`
		Attachments []string `json:"attachments"` // fileIds from POST /api/v1/files
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	conv, ok := s.conversation(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	me := CurrentUser(r.Context())
	body := chat.SanitizeHTML(req.Body)
	if strings.TrimSpace(chat.SanitizePlain(body)) == "" && len(req.Attachments) == 0 {
		writeError(w, http.StatusBadRequest, "empty_message", "message needs text or attachments")
		return
	}

	atts := []store.MessageAttachment{}
	for _, fid := range req.Attachments {
		rc, meta, err := s.files.Open(r.Context(), fid)
		if err != nil {
			writeError(w, http.StatusBadRequest, "bad_attachment", "unknown file "+fid)
			return
		}
		rc.Close()
		if meta.OwnerID != me.ID {
			writeError(w, http.StatusForbidden, "forbidden", "attachment was uploaded by someone else")
			return
		}
		atts = append(atts, store.MessageAttachment{
			FileID: meta.ID, Name: meta.Name, MimeType: meta.MimeType,
			Size: meta.Size, IsImage: strings.HasPrefix(meta.MimeType, "image/"),
		})
	}

	msg := &store.Message{ConversationID: conv.ID, SenderID: me.ID, Body: body, Attachments: atts}
	if err := s.store.InsertMessage(r.Context(), msg); err != nil {
		s.internalError(w, r, "insert message", err)
		return
	}
	s.metrics.Inc("messages_sent_total", 1)

	// live delivery to all participants + mention notifications (§11.1)
	if s.sendTo != nil {
		s.sendTo(conv.ParticipantIDs, "chat", "message", msg)
	}
	plain := chat.SanitizePlain(body)
	for _, pid := range conv.ParticipantIDs {
		if pid == me.ID {
			continue
		}
		if u, err := s.store.UserByID(r.Context(), pid); err == nil {
			first := strings.ToLower(strings.SplitN(u.Name, " ", 2)[0])
			lower := strings.ToLower(plain)
			if strings.Contains(lower, "@"+strings.ToLower(u.Email)) || strings.Contains(lower, "@"+first) {
				s.notifyUser(r.Context(), pid, "mention",
					me.Name+" mentioned you in a chat", truncateStr(plain, 120), "/messages?c="+conv.ID)
			}
		}
	}
	writeJSON(w, http.StatusCreated, map[string]any{"message": msg})
}

func (s *Server) handleMarkRead(w http.ResponseWriter, r *http.Request) {
	conv, ok := s.conversation(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	n, err := s.store.MarkConversationRead(r.Context(), conv.ID, CurrentUser(r.Context()).ID)
	if err != nil {
		s.internalError(w, r, "mark read", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"marked": n})
}

// groupOwner loads a group/project conversation and authorizes the creator.
func (s *Server) groupOwner(w http.ResponseWriter, r *http.Request, id string) (*store.Conversation, bool) {
	conv, ok := s.conversation(w, r, id)
	if !ok {
		return nil, false
	}
	if conv.Kind == "dm" {
		writeError(w, http.StatusBadRequest, "bad_request", "direct messages have no managed members")
		return nil, false
	}
	if conv.CreatorID != CurrentUser(r.Context()).ID {
		writeError(w, http.StatusForbidden, "forbidden", "only the group creator can manage members")
		return nil, false
	}
	return conv, true
}

// handleListMembers returns the participants; any member may view.
func (s *Server) handleListMembers(w http.ResponseWriter, r *http.Request) {
	conv, ok := s.conversation(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	me := CurrentUser(r.Context())
	out := make([]map[string]any, 0, len(conv.ParticipantIDs))
	for _, id := range conv.ParticipantIDs {
		u, err := s.store.UserByID(r.Context(), id)
		if err != nil {
			continue
		}
		out = append(out, map[string]any{
			"id": u.ID, "name": u.Name, "email": u.Email,
			"avatarFileId": u.AvatarFileID, "isCreator": id == conv.CreatorID,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"members":   out,
		"creatorId": conv.CreatorID,
		"canManage": conv.Kind != "dm" && conv.CreatorID == me.ID,
	})
}

// handleAddMember adds a participant to a group (creator only).
func (s *Server) handleAddMember(w http.ResponseWriter, r *http.Request) {
	conv, ok := s.groupOwner(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	var req struct {
		UserID string `json:"userId"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	if req.UserID == "" {
		writeError(w, http.StatusBadRequest, "bad_request", "userId is required")
		return
	}
	if _, err := s.store.UserByID(r.Context(), req.UserID); err != nil {
		writeError(w, http.StatusBadRequest, "bad_user", "unknown user")
		return
	}
	if err := s.store.AddParticipant(r.Context(), conv.ID, req.UserID); err != nil {
		s.internalError(w, r, "add member", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

// handleRemoveMember removes a participant from a group (creator only).
func (s *Server) handleRemoveMember(w http.ResponseWriter, r *http.Request) {
	conv, ok := s.groupOwner(w, r, r.PathValue("id"))
	if !ok {
		return
	}
	target := r.PathValue("userId")
	if target == conv.CreatorID {
		writeError(w, http.StatusBadRequest, "bad_request", "the creator cannot be removed")
		return
	}
	if err := s.store.RemoveParticipant(r.Context(), conv.ID, target); err != nil {
		s.internalError(w, r, "remove member", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

// handleUploadFile is the generic upload endpoint (POST /files, §6) used by
// chat; scope=chat unless the caller passes scope=task (consultants).
func (s *Server) handleUploadFile(w http.ResponseWriter, r *http.Request) {
	me := CurrentUser(r.Context())
	if !s.loginLimiter.Allow("upload|" + ClientIP(r.Context()).String()) {
		writeError(w, http.StatusTooManyRequests, "rate_limited", "too many uploads, slow down")
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, int64(s.cfg.MaxUploadMB)<<20+1<<20)
	file, header, err := r.FormFile("file")
	if err != nil {
		writeError(w, http.StatusBadRequest, "bad_request", "multipart field 'file' is required")
		return
	}
	defer file.Close()
	scope := files.ScopeChat
	if r.FormValue("scope") == files.ScopeTask && (me.Roles.Consultant || me.Roles.Admin) {
		scope = files.ScopeTask
	}
	meta, err := s.files.Save(r.Context(), scope, me.ID, header.Filename,
		header.Header.Get("Content-Type"), file)
	if err != nil {
		if errors.Is(err, files.ErrTooLarge) {
			writeError(w, http.StatusRequestEntityTooLarge, "too_large", err.Error())
			return
		}
		s.internalError(w, r, "save upload", err)
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{
		"fileId": meta.ID, "name": meta.Name, "mimeType": meta.MimeType,
		"size": meta.Size, "isImage": strings.HasPrefix(meta.MimeType, "image/"),
	})
}

// handleSearchUsers is the small directory for picking conversation
// participants (§11.10).
func (s *Server) handleSearchUsers(w http.ResponseWriter, r *http.Request) {
	q := strings.TrimSpace(r.URL.Query().Get("q"))
	filter := bson.M{"disabled": false}
	if q != "" {
		filter["$or"] = []bson.M{
			{"email": bson.M{"$regex": regexEscape(q), "$options": "i"}},
			{"name": bson.M{"$regex": regexEscape(q), "$options": "i"}},
		}
	}
	cur, err := s.store.DB.Collection("users").Find(r.Context(), filter,
		options.Find().SetLimit(20).SetSort(bson.D{{Key: "name", Value: 1}}))
	if err != nil {
		s.internalError(w, r, "search users", err)
		return
	}
	var users []domain.User
	if err := cur.All(r.Context(), &users); err != nil {
		s.internalError(w, r, "decode users", err)
		return
	}
	out := make([]map[string]any, 0, len(users))
	for _, u := range users {
		out = append(out, map[string]any{
			"id": u.ID, "name": u.Name, "email": u.Email, "avatarFileId": u.AvatarFileID,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"users": out})
}

// HandleInboundWS relays ephemeral client events (typing indicator).
func (s *Server) HandleInboundWS(userID string, msg ws.Message) {
	if msg.Channel != "chat" || msg.Event != "typing" {
		return
	}
	var data struct {
		ConversationID string `json:"conversationId"`
	}
	if err := json.Unmarshal(msg.Data, &data); err != nil || data.ConversationID == "" {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	conv, err := s.store.ConversationByID(ctx, data.ConversationID)
	if err != nil || !conv.HasParticipant(userID) {
		return
	}
	others := []string{}
	for _, pid := range conv.ParticipantIDs {
		if pid != userID {
			others = append(others, pid)
		}
	}
	if s.sendTo != nil {
		s.sendTo(others, "chat", "typing", map[string]string{
			"conversationId": conv.ID, "userId": userID,
		})
	}
}

func truncateStr(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "…"
}
