package server

import (
	"context"
	"html/template"
	"net/http"
	"net/url"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/adminconsole"
	"scrabble/backend/internal/feedback"
)

// consoleFeedback renders the paginated feedback queue, filtered by state
// (unread/read/archived), optionally pinned to one sender (?user=) and narrowed by
// sender glob masks (?name / ?ext).
func (s *Server) consoleFeedback(c *gin.Context) {
	ctx := c.Request.Context()
	page := consolePage(c)
	status := normalizeFeedbackStatus(c.Query("status"))
	user, _ := uuid.Parse(strings.TrimSpace(c.Query("user")))
	filter := feedback.AdminFilter{
		Status:    status,
		NameMask:  c.Query("name"),
		ExtMask:   c.Query("ext"),
		AccountID: user,
	}
	total, _ := s.feedback.AdminCount(ctx, filter)
	rows, err := s.feedback.AdminList(ctx, filter, adminPageSize, (page-1)*adminPageSize)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	q := url.Values{}
	q.Set("status", status)
	if filter.AccountID != uuid.Nil {
		q.Set("user", filter.AccountID.String())
	}
	if strings.TrimSpace(filter.NameMask) != "" {
		q.Set("name", filter.NameMask)
	}
	if strings.TrimSpace(filter.ExtMask) != "" {
		q.Set("ext", filter.ExtMask)
	}
	view := adminconsole.FeedbackView{
		Status: status, Pager: adminconsole.NewPager(page, adminPageSize, total),
		NameMask: filter.NameMask, ExtMask: filter.ExtMask,
		FilterQuery: template.URL(q.Encode()),
	}
	if filter.AccountID != uuid.Nil {
		view.UserID = filter.AccountID.String()
	}
	for _, r := range rows {
		view.Items = append(view.Items, adminconsole.FeedbackRow{
			ID: r.ID.String(), AccountID: r.AccountID.String(), SenderName: r.SenderName,
			Source: r.Source, Channel: r.Channel, HasAttachment: r.HasAttachment,
			Read: r.Read, Replied: r.Replied, Archived: r.Archived, CreatedAt: fmtTime(r.CreatedAt),
		})
	}
	s.renderConsole(c, "feedback", "feedback", "Feedback", view)
}

// consoleFeedbackDetail renders one feedback message with its actions. Opening it
// does NOT mark it read (only the explicit actions do).
func (s *Server) consoleFeedbackDetail(c *gin.Context) {
	ctx := c.Request.Context()
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	m, err := s.feedback.AdminGet(ctx, id)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	// Filed time in three zones so the operator can tell what is certainly known from what is
	// merely defaulted: always UTC; the client's offset detected at submit (when the build
	// reported one); and the sender's saved profile zone (when set beyond the UTC default). An
	// empty rendered time makes the template show "N/A" for that line.
	browserCreated, userCreated := "", ""
	if m.BrowserTZ != "" {
		browserCreated = fmtTimeIn(m.CreatedAt, m.BrowserTZ)
	}
	if m.TimeZone != "" && m.TimeZone != "UTC" {
		userCreated = fmtTimeIn(m.CreatedAt, m.TimeZone)
	}
	view := adminconsole.FeedbackDetailView{
		ID: m.ID.String(), AccountID: m.AccountID.String(), SenderName: m.SenderName,
		Source: m.Source, Channel: m.Channel, InterfaceLanguage: m.Lang,
		IP: m.SenderIP, Body: m.Body,
		HasAttachment: m.HasAttachment, AttachmentName: m.AttachmentName, IsImage: feedback.IsImage(m.AttachmentName),
		Read: m.Read, Archived: m.Archived, Replied: m.Replied, ReplyBody: m.ReplyBody,
		RepliedAt: fmtTime(m.RepliedAt), CreatedAt: fmtTime(m.CreatedAt),
		Version:          m.Version,
		CreatedAtBrowser: browserCreated, BrowserTZ: m.BrowserTZ,
		CreatedAtUser: userCreated, UserTZ: m.TimeZone,
	}
	if banned, err := s.accounts.HasRole(ctx, m.AccountID, account.RoleFeedbackBanned); err == nil {
		view.Banned = banned
	}
	s.renderConsole(c, "feedback_detail", "feedback", "Feedback", view)
}

// consoleFeedbackAttachment serves a message's attachment. It is always sent with
// X-Content-Type-Options: nosniff; images are served with their type for an inline
// <img> preview (which never executes), everything else as an octet-stream download
// (so a non-image — including a renamed one — is downloaded, never rendered).
func (s *Server) consoleFeedbackAttachment(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	name, data, ok, err := s.feedback.Attachment(c.Request.Context(), id)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	if !ok {
		s.renderConsoleMessage(c, "No attachment", "this message has no attachment", "/_gm/feedback/"+id.String())
		return
	}
	ctype := "application/octet-stream"
	disposition := `attachment; filename="` + sanitizeFilename(name) + `"`
	if feedback.IsImage(name) {
		ctype = feedback.ContentType(name)
		disposition = "inline"
	}
	c.Header("X-Content-Type-Options", "nosniff")
	c.Header("Content-Disposition", disposition)
	c.Data(http.StatusOK, ctype, data)
}

// consoleFeedbackRead marks a message dealt-with without any other action.
func (s *Server) consoleFeedbackRead(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	back := "/_gm/feedback/" + id.String()
	if err := s.feedback.MarkRead(c.Request.Context(), id); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Marked read", "the message is marked read", back)
}

// consoleFeedbackReply sends the operator's reply to the player (marking the
// message read and notifying the player's app).
func (s *Server) consoleFeedbackReply(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	back := "/_gm/feedback/" + id.String()
	body := trimForm(c, "reply")
	if body == "" {
		s.renderConsoleMessage(c, "Empty reply", "enter a reply before sending", back)
		return
	}
	if err := s.feedback.Reply(c.Request.Context(), id, body); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Replied", "the reply was sent to the player", back)
}

// consoleFeedbackArchive files a handled message away (marking it read).
func (s *Server) consoleFeedbackArchive(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	if err := s.feedback.Archive(c.Request.Context(), id); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Archived", "the message was archived", "/_gm/feedback")
}

// consoleFeedbackDelete physically deletes one message, optionally banning the
// sender from feedback (the "block" checkbox).
func (s *Server) consoleFeedbackDelete(c *gin.Context) {
	ctx := c.Request.Context()
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	m, err := s.feedback.AdminGet(ctx, id)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	if err := s.feedback.Delete(ctx, id); err != nil {
		s.consoleError(c, err)
		return
	}
	extra, err := s.maybeBan(ctx, m.AccountID, trimForm(c, "block") != "")
	if err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Deleted", "the message was deleted"+extra, "/_gm/feedback")
}

// consoleFeedbackDeleteAll physically deletes every message from the sender of the
// given message, optionally banning the sender from feedback.
func (s *Server) consoleFeedbackDeleteAll(c *gin.Context) {
	ctx := c.Request.Context()
	id, ok := s.consoleUUID(c, "/_gm/feedback")
	if !ok {
		return
	}
	m, err := s.feedback.AdminGet(ctx, id)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	if err := s.feedback.DeleteAllByAccount(ctx, m.AccountID); err != nil {
		s.consoleError(c, err)
		return
	}
	extra, err := s.maybeBan(ctx, m.AccountID, trimForm(c, "block") != "")
	if err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Deleted", "all messages from the player were deleted"+extra, "/_gm/feedback")
}

// maybeBan grants the feedback-ban role to accountID when ban is set, returning a
// suffix for the result message.
func (s *Server) maybeBan(ctx context.Context, accountID uuid.UUID, ban bool) (string, error) {
	if !ban {
		return "", nil
	}
	if err := s.accounts.GrantRole(ctx, accountID, account.RoleFeedbackBanned); err != nil {
		return "", err
	}
	return " and the player was banned from feedback", nil
}

// consoleGrantRole grants a known role to an account (the /users role panel).
func (s *Server) consoleGrantRole(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/users")
	if !ok {
		return
	}
	back := "/_gm/users/" + id.String()
	role := trimForm(c, "role")
	if !account.IsKnownRole(role) {
		s.renderConsoleMessage(c, "Invalid role", "the selected role is not recognised", back)
		return
	}
	if err := s.accounts.GrantRole(c.Request.Context(), id, role); err != nil {
		s.consoleError(c, err)
		return
	}
	if role == account.RoleNoBanner {
		s.publishBannerChange(id)
	}
	if role == account.RoleChatMuted {
		s.publishChatAccessChange(id)
	}
	s.renderConsoleMessage(c, "Role granted", "granted "+role, back)
}

// consoleRevokeRole removes a role from an account (the /users role panel).
func (s *Server) consoleRevokeRole(c *gin.Context) {
	id, ok := s.consoleUUID(c, "/_gm/users")
	if !ok {
		return
	}
	back := "/_gm/users/" + id.String()
	role := trimForm(c, "role")
	if role == "" {
		s.renderConsoleMessage(c, "Invalid role", "no role given", back)
		return
	}
	if err := s.accounts.RevokeRole(c.Request.Context(), id, role); err != nil {
		s.consoleError(c, err)
		return
	}
	if role == account.RoleNoBanner {
		s.publishBannerChange(id)
	}
	if role == account.RoleChatMuted {
		s.publishChatAccessChange(id)
	}
	s.renderConsoleMessage(c, "Role revoked", "revoked "+role, back)
}

// normalizeFeedbackStatus keeps only a recognised feedback status filter, else
// "unread" (the default queue).
func normalizeFeedbackStatus(s string) string {
	switch s {
	case "read", "archived":
		return s
	default:
		return "unread"
	}
}

// sanitizeFilename strips characters unsafe in a Content-Disposition filename
// (control chars, quotes and path separators) to prevent header injection.
func sanitizeFilename(name string) string {
	name = strings.Map(func(r rune) rune {
		if r < 0x20 || r == '"' || r == '\\' || r == '/' {
			return -1
		}
		return r
	}, name)
	if name == "" {
		return "attachment"
	}
	return name
}
