package message

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/embed"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
	"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
	"github.com/go-chi/chi/v5"
)

type Handler struct {
	db          *sql.DB
	hub         *gateway.Hub
	mentions    *MentionHandler
	pushHandler *push.Handler
	logger      *slog.Logger
	checker     *permissions.Checker
}

func NewHandler(db *sql.DB, hub *gateway.Hub, pushHandler *push.Handler, logger *slog.Logger, checker *permissions.Checker) *Handler {
	return &Handler{
		db:          db,
		hub:         hub,
		mentions:    NewMentionHandler(db, pushHandler, logger),
		pushHandler: pushHandler,
		logger:      logger,
		checker:     checker,
	}
}

func (h *Handler) RegisterRoutes(r chi.Router) {
	r.Get("/", h.List)
	r.Post("/", h.Create)
	r.Get("/search", h.Search)
	r.Post("/bulk-delete", h.BulkDelete)
	r.Patch("/{messageID}", h.Update)
	r.Delete("/{messageID}", h.Delete)
}

type bulkDeleteRequest struct {
	Messages []string `json:"messages"`
}

type bulkDeleteResponse struct {
	Deleted int `json:"deleted"`
}

// BulkDelete deletes up to 100 messages in a channel. Requires MANAGE_MESSAGES
// and messages must be within the last 14 days.
func (h *Handler) BulkDelete(w http.ResponseWriter, r *http.Request) {
	channelID := chi.URLParam(r, "channelID")
	userID, serverID, ok := h.requireChannelAccess(w, r, channelID, 0)
	if !ok {
		return
	}

	allowed, err := h.checker.CheckPermission(r.Context(), serverID, userID, permissions.MANAGE_MESSAGES)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	if !allowed {
		http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
		return
	}

	var req bulkDeleteRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}
	if len(req.Messages) == 0 {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(bulkDeleteResponse{Deleted: 0})
		return
	}
	if len(req.Messages) > 100 {
		http.Error(w, `{"error":"too many messages (max 100)"}`, http.StatusBadRequest)
		return
	}

	// ponytail: simple IN query with 14-day guard and channel_id filter in DB.
	placeholders := make([]string, len(req.Messages))
	args := make([]interface{}, 0, len(req.Messages)+1)
	for i, id := range req.Messages {
		placeholders[i] = fmt.Sprintf("$%d", i+1)
		args = append(args, id)
	}
	args = append(args, channelID)
	query := fmt.Sprintf(`
		DELETE FROM messages
		WHERE id IN (%s) AND channel_id = $%d AND created_at > NOW() - INTERVAL '14 days'
	`, strings.Join(placeholders, ","), len(args))
	res, err := h.db.ExecContext(r.Context(), query, args...)
	if err != nil {
		http.Error(w, `{"error":"failed to delete messages"}`, http.StatusInternalServerError)
		return
	}
	deleted, _ := res.RowsAffected()

	if h.hub != nil {
		for _, id := range req.Messages {
			h.hub.BroadcastToServer(serverID, gateway.Event{
				Type: gateway.EventMessageDelete,
				Data: map[string]string{
					"id":         id,
					"channel_id": channelID,
				},
			})
		}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(bulkDeleteResponse{Deleted: int(deleted)})
}

type embedResponse struct {
	ID          string `json:"id"`
	URL         string `json:"url"`
	Title       string `json:"title"`
	Description string `json:"description"`
	ImageURL    string `json:"image_url"`
	SiteName    string `json:"site_name"`
}

type messageResponse struct {
	ID          string          `json:"id"`
	ChannelID   string          `json:"channel_id"`
	AuthorID    string          `json:"author_id"`
	AuthorName  string          `json:"author_username"`
	DisplayName *string         `json:"author_display_name"`
	Content     string          `json:"content"`
	ReplyTo     *string         `json:"reply_to,omitempty"`
	EditedAt    *string         `json:"edited_at"`
	CreatedAt   string          `json:"created_at"`
	Embeds      []embedResponse `json:"embeds"`
}

// isMember checks whether the given user is a member of the given server.
func (h *Handler) isMember(ctx context.Context, userID, serverID string) (bool, error) {
	var exists bool
	err := h.db.QueryRowContext(ctx, `
		SELECT EXISTS(SELECT 1 FROM members WHERE user_id = $1 AND server_id = $2)
	`, userID, serverID).Scan(&exists)
	return exists, err
}

type createMessageRequest struct {
	Content string  `json:"content"`
	ReplyTo *string `json:"reply_to,omitempty"`
}

// @Summary Create a message
// @Description Send a message to a channel
// @Tags messages
// @Accept json
// @Produce json
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Param body body createMessageRequest true "Message data"
// @Success 201 {object} messageResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /channels/{channelID}/messages [post]
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
	channelID := chi.URLParam(r, "channelID")
	userID, serverID, ok := h.requireChannelAccess(w, r, channelID, permissions.SEND_MESSAGES)
	if !ok {
		return
	}

	var req createMessageRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}
	if req.Content == "" {
		http.Error(w, `{"error":"content is required"}`, http.StatusBadRequest)
		return
	}
	if len(req.Content) > 4000 {
		http.Error(w, `{"error":"message too long (max 4000 chars)"}`, http.StatusBadRequest)
		return
	}

	// Check server mute.
	muted, err := moderation.IsMuted(r.Context(), h.db, serverID, userID)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}
	if muted {
		http.Error(w, `{"error":"you are muted in this server"}`, http.StatusForbidden)
		return
	}

	// Check slowmode.
	if retryAfter, active := h.checkSlowmode(r.Context(), channelID, userID); active {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusTooManyRequests)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"error":          "slowmode",
			"retry_after":    retryAfter,
			"retry_after_ms": retryAfter * 1000,
		})
		return
	}

	var msg messageResponse
	var editedAt sql.NullString
	var createdAt sql.NullString
	var replyTo sql.NullString
	if req.ReplyTo != nil {
		replyTo = sql.NullString{String: *req.ReplyTo, Valid: true}
	}
	err = h.db.QueryRowContext(r.Context(), `
		INSERT INTO messages (channel_id, author_id, content, reply_to)
		VALUES ($1, $2, $3, $4)
		RETURNING id, channel_id, author_id, content, reply_to::text, edited_at::text, created_at::text
	`, channelID, userID, req.Content, replyTo).Scan(
		&msg.ID, &msg.ChannelID, &msg.AuthorID, &msg.Content, &replyTo, &editedAt, &createdAt,
	)
	if err != nil {
		http.Error(w, `{"error":"failed to create message"}`, http.StatusInternalServerError)
		return
	}

	// Fetch author info
	err = h.db.QueryRowContext(r.Context(), `
		SELECT username, display_name FROM users WHERE id = $1
	`, userID).Scan(&msg.AuthorName, &msg.DisplayName)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}

	if replyTo.Valid {
		msg.ReplyTo = &replyTo.String
	}
	if editedAt.Valid {
		msg.EditedAt = &editedAt.String
	}
	msg.CreatedAt = createdAt.String

	// Broadcast MESSAGE_CREATE event via WebSocket (scoped to server)
	h.hub.BroadcastToServer(serverID, gateway.Event{
		Type: gateway.EventMessageCreate,
		Data: msg,
	})

	// Dispatch push notifications for @mentions
	go h.mentions.ParseAndNotify(r.Context(), channelID, userID, req.Content)

	// Dispatch push notifications to channel members
	go func() {
		var channelName string
		_ = h.db.QueryRowContext(context.Background(), `SELECT name FROM channels WHERE id = $1`, channelID).Scan(&channelName)
		if channelName == "" {
			channelName = channelID
		}
		if h.pushHandler != nil {
			h.pushHandler.SendChannelNotification(context.Background(), channelID, userID, channelName, req.Content)
		}
	}()

	// Fetch and store link embeds asynchronously.
	go embed.FetchAndStore(h.db, msg.ID, req.Content)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(msg)
}

func (h *Handler) checkSlowmode(ctx context.Context, channelID, userID string) (int, bool) {
	var slowmode int
	err := h.db.QueryRowContext(ctx, `SELECT slowmode_seconds FROM channels WHERE id = $1`, channelID).Scan(&slowmode)
	if err != nil || slowmode <= 0 {
		return 0, false
	}

	var elapsed int
	err = h.db.QueryRowContext(ctx, `
		SELECT COALESCE(EXTRACT(EPOCH FROM (NOW() - created_at))::int, 0)
		FROM messages
		WHERE channel_id = $1 AND author_id = $2
		ORDER BY created_at DESC
		LIMIT 1
	`, channelID, userID).Scan(&elapsed)
	if err != nil {
		return 0, false
	}
	if elapsed < slowmode {
		return slowmode - elapsed, true
	}
	return 0, false
}

type updateMessageRequest struct {
	Content string `json:"content"`
}

// @Summary Update a message
// @Description Edit a message (author only)
// @Tags messages
// @Accept json
// @Produce json
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Param messageID path string true "Message ID"
// @Param body body updateMessageRequest true "Message update data"
// @Success 200 {object} messageResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /channels/{channelID}/messages/{messageID} [patch]
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
	messageID := chi.URLParam(r, "messageID")
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	var req updateMessageRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
		return
	}
	if req.Content == "" {
		http.Error(w, `{"error":"content is required"}`, http.StatusBadRequest)
		return
	}
	if len(req.Content) > 4000 {
		http.Error(w, `{"error":"message too long (max 4000 chars)"}`, http.StatusBadRequest)
		return
	}

	var msg messageResponse
	var editedAt sql.NullString
	var createdAt sql.NullString
	var replyTo sql.NullString
	err := h.db.QueryRowContext(r.Context(), `
		UPDATE messages SET content = $1, edited_at = NOW()
		WHERE id = $2 AND author_id = $3
		RETURNING id, channel_id, author_id, content, reply_to::text, edited_at::text, created_at::text
	`, req.Content, messageID, userID).Scan(
		&msg.ID, &msg.ChannelID, &msg.AuthorID, &msg.Content, &replyTo, &editedAt, &createdAt,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, `{"error":"message not found or not authorized"}`, http.StatusNotFound)
			return
		}
		http.Error(w, `{"error":"failed to update message"}`, http.StatusInternalServerError)
		return
	}

	err = h.db.QueryRowContext(r.Context(), `
		SELECT username, display_name FROM users WHERE id = $1
	`, userID).Scan(&msg.AuthorName, &msg.DisplayName)
	if err != nil {
		http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
		return
	}

	if replyTo.Valid {
		msg.ReplyTo = &replyTo.String
	}
	if editedAt.Valid {
		msg.EditedAt = &editedAt.String
	}
	msg.CreatedAt = createdAt.String

	// Re-fetch embeds after edit.
	msg.Embeds = h.attachEmbeds(r.Context(), []messageResponse{msg})[0].Embeds

	// Look up serverID for scoped broadcast
	serverID, _ := h.hub.ServerIDForChannel(r.Context(), msg.ChannelID)
	if serverID != "" {
		h.hub.BroadcastToServer(serverID, gateway.Event{
			Type: gateway.EventMessageUpdate,
			Data: msg,
		})
	}

	go embed.FetchAndStore(h.db, msg.ID, req.Content)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(msg)
}

// @Summary Delete a message
// @Description Delete a message (author only)
// @Tags messages
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Param messageID path string true "Message ID"
// @Success 204
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /channels/{channelID}/messages/{messageID} [delete]
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
	messageID := chi.URLParam(r, "messageID")
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return
	}

	var channelID string
	err := h.db.QueryRowContext(r.Context(), `
		DELETE FROM messages WHERE id = $1 AND author_id = $2
		RETURNING channel_id
	`, messageID, userID).Scan(&channelID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, `{"error":"message not found or not authorized"}`, http.StatusNotFound)
			return
		}
		http.Error(w, `{"error":"failed to delete message"}`, http.StatusInternalServerError)
		return
	}

	// Look up serverID for scoped broadcast
	serverID, _ := h.hub.ServerIDForChannel(r.Context(), channelID)
	if serverID != "" {
		h.hub.BroadcastToServer(serverID, gateway.Event{
			Type: gateway.EventMessageDelete,
			Data: map[string]string{
				"id":         messageID,
				"channel_id": channelID,
			},
		})
	}

	w.WriteHeader(http.StatusNoContent)
}

// @Summary List messages
// @Description List messages in a channel with pagination
// @Tags messages
// @Produce json
// @Security SessionAuth
// @Param channelID path string true "Channel ID"
// @Param limit query int false "Max messages to return (1-100, default 50)"
// @Param before query string false "Message ID to fetch messages before"
// @Success 200 {array} messageResponse
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /channels/{channelID}/messages [get]
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
	channelID := chi.URLParam(r, "channelID")
	userID, _, ok := h.requireChannelAccess(w, r, channelID, permissions.VIEW_CHANNEL)
	if !ok {
		return
	}

	limitStr := r.URL.Query().Get("limit")
	limit := 50
	if limitStr != "" {
		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
			limit = l
		}
	}

	before := r.URL.Query().Get("before")

	var rows *sql.Rows
	var err error
	if before != "" {
		rows, err = h.db.QueryContext(r.Context(), `
			SELECT m.id, m.channel_id, m.author_id, u.username, u.display_name, m.content, m.reply_to::text, m.edited_at::text, m.created_at::text
			FROM messages m
			JOIN users u ON m.author_id = u.id
			WHERE m.channel_id = $1 AND m.created_at < (SELECT created_at FROM messages WHERE id = $2)
			ORDER BY m.created_at DESC
			LIMIT $3
		`, channelID, before, limit)
	} else {
		rows, err = h.db.QueryContext(r.Context(), `
			SELECT m.id, m.channel_id, m.author_id, u.username, u.display_name, m.content, m.reply_to::text, m.edited_at::text, m.created_at::text
			FROM messages m
			JOIN users u ON m.author_id = u.id
			WHERE m.channel_id = $1
			ORDER BY m.created_at DESC
			LIMIT $2
		`, channelID, limit)
	}
	if err != nil {
		http.Error(w, `{"error":"failed to list messages"}`, http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	messages := make([]messageResponse, 0)
	for rows.Next() {
		var msg messageResponse
		var editedAt sql.NullString
		var createdAt sql.NullString
		var replyTo sql.NullString
		err := rows.Scan(&msg.ID, &msg.ChannelID, &msg.AuthorID, &msg.AuthorName, &msg.DisplayName, &msg.Content, &replyTo, &editedAt, &createdAt)
		if err != nil {
			continue
		}
		if replyTo.Valid {
			msg.ReplyTo = &replyTo.String
		}
		if editedAt.Valid {
			msg.EditedAt = &editedAt.String
		}
		msg.CreatedAt = createdAt.String
		messages = append(messages, msg)
	}

	messages = h.attachEmbeds(r.Context(), messages)

	// Reverse: query returns DESC (newest first), client expects ASC (oldest first).
	for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
		messages[i], messages[j] = messages[j], messages[i]
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(messages)

	_ = userID
}

// attachEmbeds loads embeds for the given messages and attaches them.
func (h *Handler) attachEmbeds(ctx context.Context, messages []messageResponse) []messageResponse {
	if len(messages) == 0 {
		return messages
	}
	ids := make([]string, len(messages))
	for i, m := range messages {
		ids[i] = m.ID
	}
	embedMap, err := embed.LoadForMessages(ctx, h.db, ids)
	if err != nil {
		return messages
	}
	for i := range messages {
		if embs, ok := embedMap[messages[i].ID]; ok {
			out := make([]embedResponse, len(embs))
			for j, e := range embs {
				out[j] = embedResponse{
					ID:          e.ID,
					URL:         e.URL,
					Title:       e.Title,
					Description: e.Description,
					ImageURL:    e.ImageURL,
					SiteName:    e.SiteName,
				}
			}
			messages[i].Embeds = out
		}
	}
	return messages
}

// Search searches messages in a channel using PostgreSQL full-text search.
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
	channelID := chi.URLParam(r, "channelID")
	_, _, ok := h.requireChannelAccess(w, r, channelID, permissions.VIEW_CHANNEL)
	if !ok {
		return
	}

	q := strings.TrimSpace(r.URL.Query().Get("q"))
	if q == "" {
		http.Error(w, `{"error":"missing q parameter"}`, http.StatusBadRequest)
		return
	}

	limitStr := r.URL.Query().Get("limit")
	limit := 25
	if limitStr != "" {
		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
			limit = l
		}
	}

	rows, err := h.db.QueryContext(r.Context(), `
		SELECT m.id, m.channel_id, m.author_id, u.username, u.display_name, m.content, m.reply_to::text, m.edited_at::text, m.created_at::text,
		       ts_rank(m.search_vector, plainto_tsquery('english', $2)) AS rank
		FROM messages m
		JOIN users u ON m.author_id = u.id
		WHERE m.channel_id = $1 AND m.search_vector @@ plainto_tsquery('english', $2)
		ORDER BY rank DESC, m.created_at DESC
		LIMIT $3
	`, channelID, q, limit)
	if err != nil {
		http.Error(w, `{"error":"failed to search messages"}`, http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	messages := make([]messageResponse, 0)
	for rows.Next() {
		var msg messageResponse
		var editedAt sql.NullString
		var createdAt sql.NullString
		var replyTo sql.NullString
		var rank float64
		err := rows.Scan(&msg.ID, &msg.ChannelID, &msg.AuthorID, &msg.AuthorName, &msg.DisplayName, &msg.Content, &replyTo, &editedAt, &createdAt, &rank)
		if err != nil {
			continue
		}
		if replyTo.Valid {
			msg.ReplyTo = &replyTo.String
		}
		if editedAt.Valid {
			msg.EditedAt = &editedAt.String
		}
		msg.CreatedAt = createdAt.String
		messages = append(messages, msg)
	}

	messages = h.attachEmbeds(r.Context(), messages)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(messages)
}

// requireChannelAccess checks if user is authenticated and is a member of the channel's server.
// Returns (userID, serverID, ok). If required is non-zero, also checks the permission against
// channel-level overrides.
func (h *Handler) requireChannelAccess(w http.ResponseWriter, r *http.Request, channelID string, required int64) (string, string, bool) {
	userID, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
		return "", "", false
	}

	var serverID string
	err := h.db.QueryRowContext(r.Context(), `SELECT server_id FROM channels WHERE id = $1`, channelID).Scan(&serverID)
	if err != nil {
		http.Error(w, `{"error":"channel not found"}`, http.StatusNotFound)
		return "", "", false
	}

	isMember, err := h.isMember(r.Context(), userID, serverID)
	if err != nil || !isMember {
		http.Error(w, `{"error":"not a member"}`, http.StatusForbidden)
		return "", "", false
	}

	// Check channel-specific permission if a bitmap was provided.
	if required != 0 {
		allowed, checkErr := h.checker.CheckChannelPermission(r.Context(), serverID, userID, channelID, required)
		if checkErr != nil {
			h.logger.Error("permission check failed", "error", checkErr)
			http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
			return "", "", false
		}
		if !allowed {
			http.Error(w, `{"error":"missing permissions"}`, http.StatusForbidden)
			return "", "", false
		}
	}

	return userID, serverID, true
}
