package feedback

import (
	"context"
	"errors"
	"net/netip"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/google/uuid"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/notify"
)

const (
	// maxBodyRunes caps a feedback message (and an operator reply) length.
	maxBodyRunes = 1024
	// replyVisibleFor is how long an operator reply stays shown to the player after
	// it is delivered (read).
	replyVisibleFor = 7 * 24 * time.Hour
)

// Submit / reply validation errors. The transport layer maps them to stable result
// codes; the UI maps those to the messages shown on the feedback screen.
var (
	ErrEmptyMessage       = errors.New("feedback: empty message")
	ErrMessageTooLong     = errors.New("feedback: message too long")
	ErrAttachmentTooLarge = errors.New("feedback: attachment too large")
	ErrAttachmentType     = errors.New("feedback: attachment type not allowed")
	ErrGuestForbidden     = errors.New("feedback: guests cannot submit feedback")
	ErrBanned             = errors.New("feedback: account is banned from feedback")
	ErrPendingReview      = errors.New("feedback: previous message still pending review")
)

// validChannels enumerates the submitting platforms a client may report; anything
// else is normalised to "web" (the channel is informational, never a gate).
var validChannels = map[string]bool{"telegram": true, "ios": true, "android": true, "web": true}

// Service is the feedback domain: the only writer of feedback_messages. It reads
// accounts for the guest/role gates and publishes the reply notification.
type Service struct {
	store    *Store
	accounts *account.Store
	pub      notify.Publisher
	now      func() time.Time
}

// NewService constructs a Service. store owns feedback_messages; accounts supplies
// the guest flag and the feedback-ban role.
func NewService(store *Store, accounts *account.Store) *Service {
	return &Service{
		store:    store,
		accounts: accounts,
		pub:      notify.Nop{},
		now:      func() time.Time { return time.Now().UTC() },
	}
}

// SetNotifier installs the live-event publisher used to push the "you have a reply"
// signal to the player. It must be called during startup wiring; the default is
// notify.Nop (no live events).
func (svc *Service) SetNotifier(p notify.Publisher) {
	if p != nil {
		svc.pub = p
	}
}

// Submit stores a feedback message from accountID. It rejects guests, feedback-
// banned accounts and a sender who still has a message pending review, then
// validates the body (non-empty, within the rune limit) and the optional
// attachment (size and extension allow-list). senderIP is the gateway-forwarded
// client IP (validated); channel is the submitting platform.
func (svc *Service) Submit(ctx context.Context, accountID uuid.UUID, body string, attachment []byte, attachmentName, channel, version, browserTZ, senderIP string) error {
	acc, err := svc.accounts.GetByID(ctx, accountID)
	if err != nil {
		return err
	}
	if acc.IsGuest {
		return ErrGuestForbidden
	}
	banned, err := svc.accounts.HasRole(ctx, accountID, account.RoleFeedbackBanned)
	if err != nil {
		return err
	}
	if banned {
		return ErrBanned
	}
	pending, err := svc.store.HasUnread(ctx, accountID)
	if err != nil {
		return err
	}
	if pending {
		return ErrPendingReview
	}
	body = strings.TrimSpace(body)
	if body == "" {
		return ErrEmptyMessage
	}
	if utf8.RuneCountInString(body) > maxBodyRunes {
		return ErrMessageTooLong
	}
	if len(attachment) > 0 {
		if len(attachment) > maxAttachmentBytes {
			return ErrAttachmentTooLarge
		}
		if !AllowedAttachment(attachmentName) {
			return ErrAttachmentType
		}
	} else {
		attachmentName = "" // a name without bytes carries no attachment
	}
	ch := normalizeChannel(channel)
	// Snapshot the sender's interface language, the client app version and the client's
	// detected UTC offset at submit time (acc is already loaded for the guest check) so the
	// operator later sees the state as it was.
	_, err = svc.store.Insert(ctx, accountID, body, attachment, attachmentName, ch, acc.PreferredLanguage, version, browserTZ, parseIP(senderIP))
	return err
}

// State is the player's feedback screen state. Reason is "" (can send), "pending"
// (a previous message is unreviewed) or "banned". Reply is the operator's answer to
// show, or nil. Fetching it delivers any pending replies (delivery counts as read),
// clearing the badge.
type State struct {
	CanSend       bool
	BlockedReason string
	Reply         *Reply
}

// Reply is the operator's answer shown back to the player.
type Reply struct {
	Body      string
	RepliedAt time.Time
}

// State computes the feedback screen state for accountID and marks any pending
// replies delivered.
func (svc *Service) State(ctx context.Context, accountID uuid.UUID) (State, error) {
	banned, err := svc.accounts.HasRole(ctx, accountID, account.RoleFeedbackBanned)
	if err != nil {
		return State{}, err
	}
	pending, err := svc.store.HasUnread(ctx, accountID)
	if err != nil {
		return State{}, err
	}
	st := State{CanSend: !banned && !pending}
	switch {
	case banned:
		st.BlockedReason = "banned"
	case pending:
		st.BlockedReason = "pending"
	}
	vr, ok, err := svc.store.LatestVisibleReply(ctx, accountID, svc.now().Add(-replyVisibleFor))
	if err != nil {
		return State{}, err
	}
	if ok {
		st.Reply = &Reply{Body: vr.Body, RepliedAt: vr.RepliedAt}
	}
	// Opening the screen delivers every pending reply; mark them read to clear the
	// badge (only the latest is shown, but all are now delivered).
	if err := svc.store.MarkRepliesDelivered(ctx, accountID, svc.now()); err != nil {
		return State{}, err
	}
	return st, nil
}

// ReplyUnread reports whether the account has an operator reply not yet delivered —
// the lobby/Info badge condition. It has no side effect (the lobby may poll it).
func (svc *Service) ReplyUnread(ctx context.Context, accountID uuid.UUID) (bool, error) {
	return svc.store.HasUnreadReply(ctx, accountID)
}

// AdminList returns the filtered console feedback list, paginated.
func (svc *Service) AdminList(ctx context.Context, f AdminFilter, limit, offset int) ([]AdminRow, error) {
	return svc.store.AdminList(ctx, f, limit, offset)
}

// AdminCount counts the filtered console feedback list.
func (svc *Service) AdminCount(ctx context.Context, f AdminFilter) (int, error) {
	return svc.store.AdminCount(ctx, f)
}

// AdminGet loads one message for the console detail view, or ErrNotFound.
func (svc *Service) AdminGet(ctx context.Context, id uuid.UUID) (AdminMessage, error) {
	return svc.store.AdminGet(ctx, id)
}

// CountUnread counts the active (unread, not archived) feedback queue for the
// dashboard.
func (svc *Service) CountUnread(ctx context.Context) (int, error) {
	return svc.store.CountUnread(ctx)
}

// CountSince counts feedback created after since, for the operator alert worker.
func (svc *Service) CountSince(ctx context.Context, since time.Time) (int, error) {
	return svc.store.CountSince(ctx, since)
}

// Attachment returns a message's file name and bytes, reporting false when absent.
func (svc *Service) Attachment(ctx context.Context, id uuid.UUID) (string, []byte, bool, error) {
	return svc.store.Attachment(ctx, id)
}

// MarkRead marks a message dealt-with (the manual "read" action).
func (svc *Service) MarkRead(ctx context.Context, id uuid.UUID) error {
	return svc.store.MarkRead(ctx, id, svc.now())
}

// Reply sets the operator reply on a message (marking it read), then pushes the
// "you have a reply" notification to the player.
func (svc *Service) Reply(ctx context.Context, id uuid.UUID, body string) error {
	body = strings.TrimSpace(body)
	if body == "" {
		return ErrEmptyMessage
	}
	if utf8.RuneCountInString(body) > maxBodyRunes {
		return ErrMessageTooLong
	}
	accountID, err := svc.store.Reply(ctx, id, body, svc.now())
	if err != nil {
		return err
	}
	svc.pub.Publish(notify.Notification(accountID, notify.NotifyAdminReply))
	return nil
}

// Archive files a handled message away (marking it read).
func (svc *Service) Archive(ctx context.Context, id uuid.UUID) error {
	return svc.store.Archive(ctx, id, svc.now())
}

// Delete physically removes a message and its attachment.
func (svc *Service) Delete(ctx context.Context, id uuid.UUID) error {
	return svc.store.Delete(ctx, id)
}

// DeleteAllByAccount physically removes every message of an account.
func (svc *Service) DeleteAllByAccount(ctx context.Context, accountID uuid.UUID) error {
	return svc.store.DeleteAllByAccount(ctx, accountID)
}

// parseIP returns a validated canonical IP string, or nil when raw is empty or not
// a valid address.
func parseIP(raw string) *string {
	addr, err := netip.ParseAddr(strings.TrimSpace(raw))
	if err != nil {
		return nil
	}
	canon := addr.String()
	return &canon
}

// normalizeChannel lower-cases and validates the client-reported channel, falling
// back to "web" for anything unrecognised.
func normalizeChannel(c string) string {
	c = strings.ToLower(strings.TrimSpace(c))
	if validChannels[c] {
		return c
	}
	return "web"
}
