// Package cp 承载 CP/兄弟/姐妹关系的用例编排。
package cp

import (
	"context"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	cpdomain "hyapp/services/user-service/internal/domain/cp"
)

const (
	defaultIntimacyLeaderboardPageSize = int32(20)
	maxIntimacyLeaderboardPageSize     = int32(100)
	defaultIntimacyLeaderboardRows     = 10000
	maxIntimacyLeaderboardRows         = 10000
	defaultFormationGiftFeedLimit      = int32(20)
	maxFormationGiftFeedLimit          = int32(100)
)

// Repository 描述 CP 关系 service 需要的持久化能力，便于后续单元测试替换 MySQL 实现。
type Repository interface {
	ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32, nowMs int64) ([]cpdomain.Application, int64, error)
	AcceptApplication(ctx context.Context, userID int64, applicationID string, nowMs int64) (cpdomain.Application, cpdomain.Relationship, error)
	RejectApplication(ctx context.Context, userID int64, applicationID string, reason string, nowMs int64) (cpdomain.Application, error)
	ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error)
	PrepareBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, nowMs int64) (cpdomain.RelationshipBreakup, cpdomain.Relationship, error)
	ConfirmBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, walletTransactionID string, paidCoinAmount int64, coinBalanceAfter int64, nowMs int64) (cpdomain.RelationshipBreakup, cpdomain.Relationship, error)
	CancelBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, reason string, nowMs int64) (cpdomain.RelationshipBreakup, error)
	ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent, nowMs int64) (cpdomain.ConsumeResult, error)
	ListActiveIntimacyLeaderboardEntries(ctx context.Context, limit int) ([]cpdomain.IntimacyLeaderboardEntry, error)
	ListWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int) ([]cpdomain.WeeklyRankEntry, error)
}

// FormationGiftFeedRepository 单独描述成立礼物 feed 的只读查询，避免把展示 feed 混进累计亲密榜接口。
// 该能力保持为独立扩展接口，使不关心 feed 的 CP service 替身不必实现无关读取。
type FormationGiftFeedRepository interface {
	ListFormationGiftFeed(ctx context.Context, limit int) ([]cpdomain.FormationGiftFeedItem, error)
}

// IntimacyLeaderboardStore 是 CP 亲密榜 Redis zset 读模型边界。
type IntimacyLeaderboardStore interface {
	ReplaceIntimacyLeaderboard(ctx context.Context, appCode string, entries []cpdomain.IntimacyLeaderboardEntry) error
	ListIntimacyLeaderboard(ctx context.Context, appCode string, relationType string, page int32, pageSize int32, nowMs int64) (cpdomain.IntimacyLeaderboardPage, error)
}

// AvatarFrameReader 批量读取用户当前佩戴头像框；头像框事实仍归 wallet-service。
type AvatarFrameReader interface {
	BatchGetAvatarFrames(ctx context.Context, appCode string, userIDs []int64) (map[int64]cpdomain.AvatarFrameSnapshot, error)
}

// Service 只编排 CP 用例，不直接投递 IM；IM 由 user_outbox 下游消费者按事实事件处理。
type Service struct {
	repo             Repository
	leaderboardStore IntimacyLeaderboardStore
	avatarFrames     AvatarFrameReader
	clock            func() time.Time
}

// Option 按需挂载 CP service 的读模型依赖。
type Option func(*Service)

// WithIntimacyLeaderboardStore 挂载 CP 亲密榜 Redis 读模型。
func WithIntimacyLeaderboardStore(store IntimacyLeaderboardStore) Option {
	return func(s *Service) {
		s.leaderboardStore = store
	}
}

// WithAvatarFrameReader 挂载头像框批量读取器，刷新榜单时一次性补齐双方头像框快照。
func WithAvatarFrameReader(reader AvatarFrameReader) Option {
	return func(s *Service) {
		s.avatarFrames = reader
	}
}

// New 创建 CP 关系 service。
func New(repo Repository, options ...Option) *Service {
	service := &Service{
		repo:  repo,
		clock: func() time.Time { return time.Now().UTC() },
	}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

// ListApplications 返回用户收到或发出的 CP 申请；过期固化在 repository 中完成。
func (s *Service) ListApplications(ctx context.Context, userID int64, direction string, status string, page int32, pageSize int32) ([]cpdomain.Application, int64, int64, error) {
	if err := requireUserID(userID); err != nil {
		return nil, 0, 0, err
	}
	nowMs := s.nowMs()
	applications, total, err := s.repo.ListApplications(ctx, userID, normalizeDirection(direction), normalizeStatus(status), page, pageSize, nowMs)
	return applications, total, nowMs, err
}

// AcceptApplication 同意申请并建立关系；同一用户只能有一个 active 关系的互斥由 repository 事务保证。
func (s *Service) AcceptApplication(ctx context.Context, userID int64, applicationID string) (cpdomain.Application, cpdomain.Relationship, error) {
	if err := requireUserID(userID); err != nil {
		return cpdomain.Application{}, cpdomain.Relationship{}, err
	}
	if strings.TrimSpace(applicationID) == "" {
		return cpdomain.Application{}, cpdomain.Relationship{}, xerr.New(xerr.InvalidArgument, "application_id is required")
	}
	return s.repo.AcceptApplication(ctx, userID, strings.TrimSpace(applicationID), s.nowMs())
}

// RejectApplication 拒绝申请；拒绝不阻断同一对用户其他类型的 pending 申请。
func (s *Service) RejectApplication(ctx context.Context, userID int64, applicationID string, reason string) (cpdomain.Application, error) {
	if err := requireUserID(userID); err != nil {
		return cpdomain.Application{}, err
	}
	if strings.TrimSpace(applicationID) == "" {
		return cpdomain.Application{}, xerr.New(xerr.InvalidArgument, "application_id is required")
	}
	return s.repo.RejectApplication(ctx, userID, strings.TrimSpace(applicationID), reason, s.nowMs())
}

// ListRelationships 返回用户当前 active 关系；当前产品固定每个用户同时只能有一条关系。
func (s *Service) ListRelationships(ctx context.Context, userID int64, relationType string, page int32, pageSize int32) ([]cpdomain.Relationship, int64, error) {
	if err := requireUserID(userID); err != nil {
		return nil, 0, err
	}
	return s.repo.ListRelationships(ctx, userID, normalizeRelationType(relationType), page, pageSize)
}

// PrepareBreakRelationship 创建解除关系占位；占位成功后 gateway 才能去 wallet-service 扣费。
func (s *Service) PrepareBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string) (cpdomain.RelationshipBreakup, cpdomain.Relationship, error) {
	if err := requireBreakCommand(userID, relationshipID, commandID); err != nil {
		return cpdomain.RelationshipBreakup{}, cpdomain.Relationship{}, err
	}
	return s.repo.PrepareBreakRelationship(ctx, userID, strings.TrimSpace(relationshipID), strings.TrimSpace(commandID), s.nowMs())
}

// ConfirmBreakRelationship 在 wallet-service 扣费成功后确认解除；paidCoinAmount 必须等于 prepare 时冻结的配置费用。
func (s *Service) ConfirmBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, walletTransactionID string, paidCoinAmount int64, coinBalanceAfter int64) (cpdomain.RelationshipBreakup, cpdomain.Relationship, error) {
	if err := requireBreakCommand(userID, relationshipID, commandID); err != nil {
		return cpdomain.RelationshipBreakup{}, cpdomain.Relationship{}, err
	}
	if paidCoinAmount < 0 {
		return cpdomain.RelationshipBreakup{}, cpdomain.Relationship{}, xerr.New(xerr.InvalidArgument, "paid_coin_amount is invalid")
	}
	if paidCoinAmount > 0 && strings.TrimSpace(walletTransactionID) == "" {
		return cpdomain.RelationshipBreakup{}, cpdomain.Relationship{}, xerr.New(xerr.InvalidArgument, "wallet_transaction_id is required")
	}
	return s.repo.ConfirmBreakRelationship(ctx, userID, strings.TrimSpace(relationshipID), strings.TrimSpace(commandID), strings.TrimSpace(walletTransactionID), paidCoinAmount, coinBalanceAfter, s.nowMs())
}

// CancelBreakRelationship 只取消 pending 占位；已经 confirmed 的解除不能通过取消接口回滚。
func (s *Service) CancelBreakRelationship(ctx context.Context, userID int64, relationshipID string, commandID string, reason string) (cpdomain.RelationshipBreakup, error) {
	if err := requireBreakCommand(userID, relationshipID, commandID); err != nil {
		return cpdomain.RelationshipBreakup{}, err
	}
	return s.repo.CancelBreakRelationship(ctx, userID, strings.TrimSpace(relationshipID), strings.TrimSpace(commandID), reason, s.nowMs())
}

// ConsumeGiftEvent 消费 room-service 已扣费并落房间状态的送礼事实。
func (s *Service) ConsumeGiftEvent(ctx context.Context, event cpdomain.GiftEvent) (cpdomain.ConsumeResult, error) {
	event.CPRelationType = normalizeRelationType(event.CPRelationType)
	// gift_value 是亲密值和周榜分数增量，沿用 room-service 已扣费后的结算值；0 值仍可创建申请，但不会增加已有关联亲密值。
	if event.GiftValue < 0 {
		event.GiftValue = 0
	}
	// coin_spent 是 wallet 结算出的礼物金币价值；缺失或非法的历史事件保留 0，绝不能用 gift_value 的热度口径兜底。
	if event.CoinSpent < 0 {
		event.CoinSpent = 0
	}
	return s.repo.ConsumeGiftEvent(ctx, event, s.nowMs())
}

// ListFormationGiftFeed 返回已接受的 CP 成立礼物事件流；门槛由仓储固定，调用方只能控制有上限的条数。
func (s *Service) ListFormationGiftFeed(ctx context.Context, limit int32) ([]cpdomain.FormationGiftFeedItem, error) {
	if s == nil || s.repo == nil {
		return nil, xerr.New(xerr.Unavailable, "cp repository is not configured")
	}
	feedRepo, ok := s.repo.(FormationGiftFeedRepository)
	if !ok {
		return nil, xerr.New(xerr.Unavailable, "cp formation gift feed repository is not configured")
	}
	if limit <= 0 {
		limit = defaultFormationGiftFeedLimit
	}
	if limit > maxFormationGiftFeedLimit {
		limit = maxFormationGiftFeedLimit
	}
	return feedRepo.ListFormationGiftFeed(ctx, int(limit))
}

// ListWeeklyRankEntries 给 activity-service 的结算链路读取 CP 周榜快照；它不写 Redis，也不发奖。
func (s *Service) ListWeeklyRankEntries(ctx context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]cpdomain.WeeklyRankEntry, int64, error) {
	if s == nil || s.repo == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "cp repository is not configured")
	}
	if startMS <= 0 || endMS <= startMS {
		return nil, 0, xerr.New(xerr.InvalidArgument, "weekly rank time window is invalid")
	}
	if limit <= 0 {
		limit = 3
	}
	if limit > 100 {
		limit = 100
	}
	entries, err := s.repo.ListWeeklyRankEntries(ctx, normalizeRelationType(relationType), startMS, endMS, int(limit))
	return entries, s.nowMs(), err
}

// RefreshIntimacyLeaderboard 重建 CP 亲密值排行榜读模型；cron 只触发，关系、用户资料和头像框快照都由 owner 服务读取。
func (s *Service) RefreshIntimacyLeaderboard(ctx context.Context, limit int) (int, error) {
	// 榜单刷新是定时异步链路：任何核心依赖缺失都必须显式失败，让 cron run 记录失败状态，
	// 不能静默返回空榜，否则 App 会误以为当前没有亲密关系。
	if s == nil || s.repo == nil {
		return 0, xerr.New(xerr.Unavailable, "cp repository is not configured")
	}
	if s.leaderboardStore == nil {
		return 0, xerr.New(xerr.Unavailable, "cp intimacy leaderboard store is not configured")
	}
	if s.avatarFrames == nil {
		return 0, xerr.New(xerr.Unavailable, "cp avatar frame reader is not configured")
	}
	// batch_size 在 cron 配置里表达本轮榜单保留上限；service 再做一次夹紧，避免配置异常导致全表无界扫描。
	limit = normalizeLeaderboardRowLimit(limit)
	entries, err := s.repo.ListActiveIntimacyLeaderboardEntries(ctx, limit)
	if err != nil {
		return 0, err
	}
	// 头像框归 wallet-service 管理，关系和用户资料归 user-service 管理；刷新时批量读钱包快照并固化进 zset member，
	// App 查询榜单时只读 Redis，不再跨服务补资料，避免高频榜单接口放大 wallet-service 压力。
	frames, err := s.avatarFrames.BatchGetAvatarFrames(ctx, appcode.FromContext(ctx), intimacyLeaderboardUserIDs(entries))
	if err != nil {
		return 0, err
	}
	applyAvatarFrames(entries, frames)
	// Redis 写入采用全量替换语义；关系结束、改名、换头像、换头像框都能在下一轮被覆盖，不需要单独维护删除事件。
	if err := s.leaderboardStore.ReplaceIntimacyLeaderboard(ctx, appcode.FromContext(ctx), entries); err != nil {
		return 0, err
	}
	return len(entries), nil
}

// ListIntimacyLeaderboard 返回 cron 刷新的 CP 亲密值排行榜，避免 App 请求实时扫关系表和跨服务补头像框。
func (s *Service) ListIntimacyLeaderboard(ctx context.Context, relationType string, page int32, pageSize int32) (cpdomain.IntimacyLeaderboardPage, error) {
	// 查询接口只依赖 Redis 读模型；如果刷新链路没有装配，直接返回 unavailable，避免回退 MySQL 实时扫表。
	if s == nil || s.leaderboardStore == nil {
		return cpdomain.IntimacyLeaderboardPage{}, xerr.New(xerr.Unavailable, "cp intimacy leaderboard store is not configured")
	}
	// relation_type 只允许 cp/brother/sister；空值和非法值都归并到 all 榜，由 Redis store 统一落 key。
	relationType = normalizeRelationType(relationType)
	page, pageSize = normalizeLeaderboardPage(page, pageSize)
	return s.leaderboardStore.ListIntimacyLeaderboard(ctx, appcode.FromContext(ctx), relationType, page, pageSize, s.nowMs())
}

func (s *Service) nowMs() int64 {
	if s == nil || s.clock == nil {
		return time.Now().UTC().UnixMilli()
	}
	return s.clock().UTC().UnixMilli()
}

func requireUserID(userID int64) error {
	if userID <= 0 {
		return xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	return nil
}

func requireBreakCommand(userID int64, relationshipID string, commandID string) error {
	if err := requireUserID(userID); err != nil {
		return err
	}
	if strings.TrimSpace(relationshipID) == "" {
		return xerr.New(xerr.InvalidArgument, "relationship_id is required")
	}
	if strings.TrimSpace(commandID) == "" {
		return xerr.New(xerr.InvalidArgument, "command_id is required")
	}
	if len(strings.TrimSpace(commandID)) > 128 {
		return xerr.New(xerr.InvalidArgument, "command_id is too long")
	}
	return nil
}

func normalizeDirection(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "outgoing":
		return "outgoing"
	default:
		return "incoming"
	}
}

func normalizeStatus(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case cpdomain.ApplicationStatusPending,
		cpdomain.ApplicationStatusAccepted,
		cpdomain.ApplicationStatusRejected,
		cpdomain.ApplicationStatusExpired,
		cpdomain.ApplicationStatusBlocked:
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return ""
	}
}

func normalizeLeaderboardPage(page int32, pageSize int32) (int32, int32) {
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = defaultIntimacyLeaderboardPageSize
	}
	if pageSize > maxIntimacyLeaderboardPageSize {
		pageSize = maxIntimacyLeaderboardPageSize
	}
	return page, pageSize
}

func normalizeLeaderboardRowLimit(limit int) int {
	if limit <= 0 {
		return defaultIntimacyLeaderboardRows
	}
	if limit > maxIntimacyLeaderboardRows {
		return maxIntimacyLeaderboardRows
	}
	return limit
}

func intimacyLeaderboardUserIDs(entries []cpdomain.IntimacyLeaderboardEntry) []int64 {
	seen := make(map[int64]struct{}, len(entries)*2)
	userIDs := make([]int64, 0, len(entries)*2)
	for _, entry := range entries {
		for _, userID := range []int64{entry.UserA.UserID, entry.UserB.UserID} {
			if userID <= 0 {
				continue
			}
			// 同一个用户可能出现在多条兄弟/姐妹关系里，批量读钱包资源前先去重，避免无意义的重复 RPC payload。
			if _, exists := seen[userID]; exists {
				continue
			}
			seen[userID] = struct{}{}
			userIDs = append(userIDs, userID)
		}
	}
	return userIDs
}

func applyAvatarFrames(entries []cpdomain.IntimacyLeaderboardEntry, frames map[int64]cpdomain.AvatarFrameSnapshot) {
	for index := range entries {
		// 没有佩戴头像框或 wallet-service 没返回资源时保持 nil，gateway 会省略 avatar_frame 字段。
		if frame, ok := frames[entries[index].UserA.UserID]; ok && frame.ResourceID > 0 {
			entries[index].UserA.AvatarFrame = &frame
		}
		if frame, ok := frames[entries[index].UserB.UserID]; ok && frame.ResourceID > 0 {
			entries[index].UserB.AvatarFrame = &frame
		}
	}
}

func normalizeRelationType(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case cpdomain.RelationTypeCP:
		return cpdomain.RelationTypeCP
	case cpdomain.RelationTypeBrother:
		return cpdomain.RelationTypeBrother
	case cpdomain.RelationTypeSister:
		return cpdomain.RelationTypeSister
	default:
		return ""
	}
}
