package message

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/outboxpartition"
	"hyapp/pkg/xerr"
	messagedomain "hyapp/services/activity-service/internal/domain/message"
)

const (
	actionConfirmDefaultLockTTL = 30 * time.Second
	actionConfirmDefaultLimit   = 50
	actionConfirmMaxLimit       = 100
)

// ActionConfirmRepository is the durable boundary for confirm rows and their IM outbox.
type ActionConfirmRepository interface {
	SaveActionConfirmWithOutbox(ctx context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error)
	ClaimActionConfirm(ctx context.Context, confirmID string, actorUserID int64, action string, commandID string, workerID string, nowMS int64, lockUntilMS int64) (messagedomain.ActionConfirm, bool, error)
	CompleteActionConfirm(ctx context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error)
	ReleaseActionConfirm(ctx context.Context, confirmID string, commandID string, errorMessage string, nowMS int64) error
	BatchGetActionConfirms(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error)
	ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, limit int, cursorCreatedAtMS int64, cursorConfirmID string) ([]messagedomain.ActionConfirm, error)
	ClaimPendingActionConfirmOutbox(ctx context.Context, workerID string, nowMS int64, lockUntilMS int64, batchSize int) ([]messagedomain.ActionConfirmOutbox, error)
	MarkActionConfirmOutboxDelivered(ctx context.Context, eventID string, nowMS int64) error
	MarkActionConfirmOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64, nowMS int64) error
}

// RoleInvitationClient is the owner-service boundary used when a confirm action maps to a role invitation.
type RoleInvitationClient interface {
	ProcessRoleInvitation(ctx context.Context, input ProcessRoleInvitationInput) (RoleInvitationState, error)
	GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (RoleInvitationState, error)
}

// ProcessRoleInvitationInput is the normalized owner-service command assembled from a confirm row.
type ProcessRoleInvitationInput struct {
	CommandID    string
	ActorUserID  int64
	InvitationID int64
	Action       string
	Reason       string
}

// RoleInvitationState is the small role-invitation projection needed to reconcile confirm status.
type RoleInvitationState struct {
	InvitationID   int64
	InvitationType string
	Status         string
}

// ActionConfirmService owns IM confirm status and delegates business mutations back to owner services.
type ActionConfirmService struct {
	cfg                  Config
	repository           ActionConfirmRepository
	roleInvitationClient RoleInvitationClient
	outboxPartitions     *outboxpartition.Partitions
	now                  func() time.Time
}

// NewActionConfirm creates the generic confirm-message service.
func NewActionConfirm(cfg Config, repository ActionConfirmRepository, roleInvitationClient RoleInvitationClient) *ActionConfirmService {
	return &ActionConfirmService{
		cfg:                  cfg,
		repository:           repository,
		roleInvitationClient: roleInvitationClient,
		outboxPartitions:     outboxpartition.New(cfg.AppCodes, outboxpartition.DefaultDiscoveryRefreshInterval),
		now:                  time.Now,
	}
}

// SetClock injects deterministic time for tests.
func (s *ActionConfirmService) SetClock(now func() time.Time) {
	if now != nil {
		s.now = now
	}
}

// ConsumeRoleInvitationCreated materializes one user-service invitation fact into an actionable IM confirm.
func (s *ActionConfirmService) ConsumeRoleInvitationCreated(ctx context.Context, event messagedomain.RoleInvitationConfirmEvent) (messagedomain.ActionConfirm, bool, error) {
	if err := s.requireRepository(); err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	if event.SourceEventID == "" || event.InvitationID <= 0 || event.InviterUserID <= 0 || event.TargetUserID <= 0 {
		return messagedomain.ActionConfirm{}, false, xerr.New(xerr.InvalidArgument, "role invitation confirm event is incomplete")
	}
	subtype := strings.TrimSpace(event.InvitationType)
	if subtype == "" {
		return messagedomain.ActionConfirm{}, false, xerr.New(xerr.InvalidArgument, "invitation_type is required")
	}
	nowMS := firstPositiveInt64(event.CreatedAtMS, s.now().UnixMilli())
	messageText := roleInvitationMessageText(event)
	metadataJSON := strings.TrimSpace(event.RawPayloadJSON)
	if metadataJSON == "" || !json.Valid([]byte(metadataJSON)) {
		metadataJSON = "{}"
	}
	confirmID := idgen.New("cfm")
	payloadJSON, err := confirmIMPayloadJSON(confirmID, event, messageText, nowMS)
	if err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	confirm := messagedomain.ActionConfirm{
		ConfirmID:       confirmID,
		SourceName:      messagedomain.ActionConfirmSourceUserOutbox,
		SourceEventID:   event.SourceEventID,
		BusinessType:    messagedomain.ActionConfirmBusinessRoleInvitation,
		BusinessSubtype: subtype,
		BusinessID:      strconv.FormatInt(event.InvitationID, 10),
		SenderUserID:    event.InviterUserID,
		ReceiverUserID:  event.TargetUserID,
		MessageText:     messageText,
		Status:          messagedomain.ActionConfirmStatusPending,
		MetadataJSON:    metadataJSON,
		CreatedAtMS:     nowMS,
		UpdatedAtMS:     nowMS,
	}
	outbox := messagedomain.ActionConfirmOutbox{
		EventID:     idgen.New("maout"),
		ConfirmID:   confirm.ConfirmID,
		EventType:   messagedomain.ActionConfirmEventCreated,
		Status:      "pending",
		PayloadJSON: payloadJSON,
		CreatedAtMS: nowMS,
		UpdatedAtMS: nowMS,
	}
	created, saved, err := s.repository.SaveActionConfirmWithOutbox(ctx, confirm, outbox)
	return saved, created, err
}

// AcceptActionConfirm executes the accept branch for the current receiver.
func (s *ActionConfirmService) AcceptActionConfirm(ctx context.Context, actorUserID int64, confirmID string, commandID string) (messagedomain.ActionConfirm, error) {
	return s.processAction(ctx, actorUserID, confirmID, commandID, messagedomain.ActionConfirmActionAccept, "")
}

// RejectActionConfirm executes the reject branch for the current receiver.
func (s *ActionConfirmService) RejectActionConfirm(ctx context.Context, actorUserID int64, confirmID string, commandID string, reason string) (messagedomain.ActionConfirm, error) {
	return s.processAction(ctx, actorUserID, confirmID, commandID, messagedomain.ActionConfirmActionReject, reason)
}

func (s *ActionConfirmService) processAction(ctx context.Context, actorUserID int64, confirmID string, commandID string, action string, reason string) (messagedomain.ActionConfirm, error) {
	if err := s.requireRepository(); err != nil {
		return messagedomain.ActionConfirm{}, err
	}
	if s.roleInvitationClient == nil {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.Unavailable, "role invitation client is not configured")
	}
	confirmID = strings.TrimSpace(confirmID)
	commandID = strings.TrimSpace(commandID)
	if actorUserID <= 0 || confirmID == "" || commandID == "" {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "actor_user_id, confirm_id and command_id are required")
	}
	nowMS := s.now().UnixMilli()
	claimed, ok, err := s.repository.ClaimActionConfirm(ctx, confirmID, actorUserID, action, commandID, s.workerID(), nowMS, nowMS+actionConfirmDefaultLockTTL.Milliseconds())
	if err != nil || !ok {
		return claimed, err
	}
	if isTerminalConfirmStatus(claimed.Status) {
		return claimed, nil
	}
	invitationID, err := strconv.ParseInt(claimed.BusinessID, 10, 64)
	if err != nil || invitationID <= 0 {
		_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, "invalid business_id", s.now().UnixMilli())
		return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_id is invalid")
	}
	if claimed.BusinessType != messagedomain.ActionConfirmBusinessRoleInvitation {
		_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, "unsupported business_type", s.now().UnixMilli())
		return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_type is unsupported")
	}
	state, err := s.roleInvitationClient.ProcessRoleInvitation(ctx, ProcessRoleInvitationInput{
		CommandID:    commandID,
		ActorUserID:  actorUserID,
		InvitationID: invitationID,
		Action:       action,
		Reason:       strings.TrimSpace(reason),
	})
	if err != nil {
		reconciled, reconcileErr := s.reconcileRoleInvitation(ctx, claimed, actorUserID, commandID, action)
		if reconcileErr == nil && isTerminalConfirmStatus(reconciled.Status) {
			return reconciled, nil
		}
		_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, err.Error(), s.now().UnixMilli())
		return messagedomain.ActionConfirm{}, err
	}
	return s.repository.CompleteActionConfirm(ctx, confirmID, commandID, confirmStatusFromRoleInvitation(state.Status), actionFromRoleInvitationStatus(state.Status, action), s.now().UnixMilli())
}

func (s *ActionConfirmService) reconcileRoleInvitation(ctx context.Context, confirm messagedomain.ActionConfirm, actorUserID int64, commandID string, action string) (messagedomain.ActionConfirm, error) {
	invitationID, err := strconv.ParseInt(confirm.BusinessID, 10, 64)
	if err != nil || invitationID <= 0 {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_id is invalid")
	}
	state, err := s.roleInvitationClient.GetRoleInvitation(ctx, actorUserID, invitationID)
	if err != nil {
		return messagedomain.ActionConfirm{}, err
	}
	status := confirmStatusFromRoleInvitation(state.Status)
	if !isTerminalConfirmStatus(status) {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.Conflict, "role invitation is not terminal")
	}
	return s.repository.CompleteActionConfirm(ctx, confirm.ConfirmID, commandID, status, actionFromRoleInvitationStatus(state.Status, action), s.now().UnixMilli())
}

// BatchGetActionConfirmStatus returns only confirms visible to the current user.
func (s *ActionConfirmService) BatchGetActionConfirmStatus(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error) {
	if err := s.requireRepository(); err != nil {
		return nil, err
	}
	if userID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	normalized := make([]string, 0, len(confirmIDs))
	seen := make(map[string]struct{}, len(confirmIDs))
	for _, confirmID := range confirmIDs {
		confirmID = strings.TrimSpace(confirmID)
		if confirmID == "" {
			continue
		}
		if _, ok := seen[confirmID]; ok {
			continue
		}
		seen[confirmID] = struct{}{}
		normalized = append(normalized, confirmID)
	}
	if len(normalized) == 0 {
		return []messagedomain.ActionConfirm{}, nil
	}
	if len(normalized) > 100 {
		return nil, xerr.New(xerr.InvalidArgument, "confirm_ids is too large")
	}
	return s.repository.BatchGetActionConfirms(ctx, userID, normalized)
}

// ListConversationActionConfirms returns confirm states for one C2C peer conversation.
func (s *ActionConfirmService) ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, pageSize int32, pageToken string) ([]messagedomain.ActionConfirm, string, error) {
	if err := s.requireRepository(); err != nil {
		return nil, "", err
	}
	if userID <= 0 || peerUserID <= 0 {
		return nil, "", xerr.New(xerr.InvalidArgument, "user_id and peer_user_id are required")
	}
	limit := int(pageSize)
	if limit <= 0 {
		limit = actionConfirmDefaultLimit
	}
	if limit > actionConfirmMaxLimit {
		limit = actionConfirmMaxLimit
	}
	cursor, err := decodeActionConfirmCursor(pageToken)
	if err != nil {
		return nil, "", err
	}
	items, err := s.repository.ListConversationActionConfirms(ctx, userID, peerUserID, limit+1, cursor.CreatedAtMS, cursor.ConfirmID)
	if err != nil {
		return nil, "", err
	}
	if len(items) <= limit {
		return items, "", nil
	}
	page := items[:limit]
	return page, encodeActionConfirmCursor(page[len(page)-1]), nil
}

// ClaimPendingOutbox locks pending action-message outbox rows for the MQ publisher.
func (s *ActionConfirmService) ClaimPendingOutbox(ctx context.Context, batchSize int, lockTTL time.Duration) ([]messagedomain.ActionConfirmOutbox, error) {
	if err := s.requireRepository(); err != nil {
		return nil, err
	}
	if batchSize <= 0 {
		batchSize = 100
	}
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	nowMS := s.now().UnixMilli()
	appCodes, err := s.outboxPartitions.Resolve(ctx, nil)
	if err != nil {
		return nil, err
	}
	if len(appCodes) == 0 {
		// 直接构造 service 的本地调用仍沿用当前 context；常驻 worker 必须由配置提供完整 App 清单。
		appCodes = []string{appcode.FromContext(ctx)}
	}
	// 每个 App 继续走仓储层现有的 app_code 前缀索引和 SKIP LOCKED 事务；
	// 公平配额避免 Lalu 积压占满批次后让 Fami/Huwaa 的邀请 C2C 长期饥饿。
	return outboxpartition.ClaimFair(ctx, s.outboxPartitions.Order(appCodes), batchSize, func(claimCtx context.Context, app string, limit int) ([]messagedomain.ActionConfirmOutbox, error) {
		return s.repository.ClaimPendingActionConfirmOutbox(
			appcode.WithContext(claimCtx, app),
			s.workerID(),
			nowMS,
			nowMS+lockTTL.Milliseconds(),
			limit,
		)
	})
}

func (s *ActionConfirmService) MarkOutboxDelivered(ctx context.Context, eventID string) error {
	return s.repository.MarkActionConfirmOutboxDelivered(ctx, eventID, s.now().UnixMilli())
}

func (s *ActionConfirmService) MarkOutboxRetryable(ctx context.Context, eventID string, cause error, nextRetryAtMS int64) error {
	message := ""
	if cause != nil {
		message = cause.Error()
	}
	return s.repository.MarkActionConfirmOutboxRetryable(ctx, eventID, message, nextRetryAtMS, s.now().UnixMilli())
}

func (s *ActionConfirmService) requireRepository() error {
	if s == nil || s.repository == nil {
		return xerr.New(xerr.Unavailable, "action confirm repository is not configured")
	}
	return nil
}

func (s *ActionConfirmService) workerID() string {
	nodeID := strings.TrimSpace(s.cfg.NodeID)
	if nodeID == "" {
		nodeID = "activity"
	}
	return "message-action-confirm-" + nodeID
}

type confirmIMPayload struct {
	Type            string            `json:"type"`
	Version         int               `json:"version"`
	ConfirmID       string            `json:"confirm_id"`
	BusinessType    string            `json:"business_type"`
	BusinessSubtype string            `json:"business_subtype"`
	SenderUserID    string            `json:"sender_user_id"`
	ReceiverUserID  string            `json:"receiver_user_id"`
	Msg             string            `json:"msg"`
	Actions         []confirmIMAction `json:"actions"`
	CreatedAtMS     int64             `json:"created_at_ms"`
}

type confirmIMAction struct {
	Action string `json:"action"`
	Label  string `json:"label"`
	Text   string `json:"text"`
	Style  string `json:"style,omitempty"`
}

func confirmIMPayloadJSON(confirmID string, event messagedomain.RoleInvitationConfirmEvent, messageText string, createdAtMS int64) (string, error) {
	payload := confirmIMPayload{
		Type:            "im_confirm",
		Version:         1,
		ConfirmID:       confirmID,
		BusinessType:    messagedomain.ActionConfirmBusinessRoleInvitation,
		BusinessSubtype: event.InvitationType,
		SenderUserID:    strconv.FormatInt(event.InviterUserID, 10),
		ReceiverUserID:  strconv.FormatInt(event.TargetUserID, 10),
		Msg:             messageText,
		Actions: []confirmIMAction{
			// label 是当前 App 渲染按钮的字段；text 保留给早期自定义消息解析，避免协议兼容回退。
			{Action: messagedomain.ActionConfirmActionReject, Label: "Reject", Text: "Reject", Style: "secondary"},
			{Action: messagedomain.ActionConfirmActionAccept, Label: "Accept", Text: "Accept", Style: "primary"},
		},
		CreatedAtMS: createdAtMS,
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(raw), nil
}

func roleInvitationMessageText(event messagedomain.RoleInvitationConfirmEvent) string {
	name := firstNonEmptyText(event.Inviter.Username, event.Inviter.DisplayUserID, strconv.FormatInt(event.InviterUserID, 10))
	agencyName := firstNonEmptyText(event.AgencyName, "guild")
	switch event.InvitationType {
	case "host", "external_host":
		// 外管邀请使用独立 subtype 隔离 Lalu 现有角色链路，但目标用户
		// 看到并处理的仍是同一种 Host 入会确认。
		return fmt.Sprintf("%s has invited you to join the %s guild", name, agencyName)
	case "agency", "external_agency":
		return fmt.Sprintf("%s has invited you to create the %s guild", name, agencyName)
	case "bd", "external_bd":
		return fmt.Sprintf("%s has invited you to become BD", name)
	default:
		return fmt.Sprintf("%s has sent you a confirmation request", name)
	}
}

func confirmStatusFromRoleInvitation(status string) string {
	switch strings.TrimSpace(status) {
	case "accepted":
		return messagedomain.ActionConfirmStatusAccepted
	case "rejected":
		return messagedomain.ActionConfirmStatusRejected
	case "cancelled":
		return messagedomain.ActionConfirmStatusCanceled
	case "expired":
		return messagedomain.ActionConfirmStatusExpired
	default:
		return messagedomain.ActionConfirmStatusPending
	}
}

func actionFromRoleInvitationStatus(status string, fallback string) string {
	switch strings.TrimSpace(status) {
	case "accepted":
		return messagedomain.ActionConfirmActionAccept
	case "rejected":
		return messagedomain.ActionConfirmActionReject
	default:
		return fallback
	}
}

func isTerminalConfirmStatus(status string) bool {
	switch status {
	case messagedomain.ActionConfirmStatusAccepted, messagedomain.ActionConfirmStatusRejected, messagedomain.ActionConfirmStatusExpired, messagedomain.ActionConfirmStatusCanceled, messagedomain.ActionConfirmStatusFailed:
		return true
	default:
		return false
	}
}

func firstNonEmptyText(values ...string) string {
	for _, value := range values {
		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}
	return ""
}

func firstPositiveInt64(values ...int64) int64 {
	for _, value := range values {
		if value > 0 {
			return value
		}
	}
	return 0
}

type actionConfirmCursor struct {
	CreatedAtMS int64  `json:"created_at_ms"`
	ConfirmID   string `json:"confirm_id"`
}

func encodeActionConfirmCursor(confirm messagedomain.ActionConfirm) string {
	raw, err := json.Marshal(actionConfirmCursor{CreatedAtMS: confirm.CreatedAtMS, ConfirmID: confirm.ConfirmID})
	if err != nil {
		return ""
	}
	return base64.RawURLEncoding.EncodeToString(raw)
}

func decodeActionConfirmCursor(value string) (actionConfirmCursor, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return actionConfirmCursor{}, nil
	}
	raw, err := base64.RawURLEncoding.DecodeString(value)
	if err != nil {
		return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
	}
	var cursor actionConfirmCursor
	if err := json.Unmarshal(raw, &cursor); err != nil {
		return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
	}
	if cursor.CreatedAtMS <= 0 || strings.TrimSpace(cursor.ConfirmID) == "" {
		return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
	}
	return cursor, nil
}
