package user

import (
	"context"
	"strings"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	hostservice "hyapp/services/user-service/internal/service/host"
)

const (
	userStatusRevokeReason    = "USER_STATUS_CHANGED"
	countryChangeRevokeReason = "USER_COUNTRY_CHANGED"
)

const (
	OperatorTypeAdmin   = "admin"
	OperatorTypeAppUser = "app_user"
	OperatorTypeSystem  = "system"
)

const (
	ManagerUserBlockStatusActive   = "active"
	ManagerUserBlockStatusReleased = "released"
	ManagerUserBlockStatusExpired  = "expired"

	managerUserBlockReasonPrefix = "manager_center_block:"
)

// UserStatusCommand 是用户治理入口的服务层命令。
type UserStatusCommand struct {
	AppCode        string
	TargetUserID   int64
	Status         userdomain.Status
	OperatorType   string
	OperatorUserID int64
	Reason         string
	RequestID      string
	NowMs          int64
}

// UserStatusPersistenceResult 是状态事务提交后的数据库事实。
type UserStatusPersistenceResult struct {
	User              userdomain.User
	RevokedSessionIDs []string
}

// UserStatusResult 汇总状态事实和封禁后需要立即执行的外部副作用结果。
type UserStatusResult struct {
	User                userdomain.User
	RevokedSessionCount int64
	AccessTokenRevoked  bool
	AccessTokenError    string
	IMKicked            bool
	IMKickError         string
	RoomEvicted         bool
	RoomID              string
	RTCKicked           bool
	RTCKickError        string
	RoomEvictError      string
}

// ManagerUserBlockCommand 是经理中心封禁用户的服务层命令。
// command_id 是 H5 写操作幂等边界，blocked_until_ms 决定 cron 何时自动解封。
type ManagerUserBlockCommand struct {
	CommandID      string
	ManagerUserID  int64
	TargetUserID   int64
	BlockedUntilMS int64
	Reason         string
	RequestID      string
	NowMS          int64
}

// ReleaseManagerUserBlockCommand 是手动或系统到期释放经理封禁的命令。
type ReleaseManagerUserBlockCommand struct {
	ManagerUserID int64
	BlockID       string
	Status        string
	Reason        string
	RequestID     string
	NowMS         int64
}

// ManagerUserBlock 是经理封禁列表的稳定读模型。
type ManagerUserBlock struct {
	BlockID             string
	ManagerUserID       int64
	TargetUserID        int64
	TargetDisplayUserID string
	TargetUsername      string
	TargetAvatar        string
	Country             string
	TargetOldStatus     userdomain.Status
	BlockedUntilMS      int64
	Status              string
	Reason              string
	CreatedAtMS         int64
	UpdatedAtMS         int64
}

// ManagerUserBlockRelease 带上是否需要恢复 users.status，避免释放记录时误放仍被其他经理封禁的用户。
type ManagerUserBlockRelease struct {
	Block         ManagerUserBlock
	User          userdomain.User
	ShouldRestore bool
}

// SetUserStatus 是封禁、停用和恢复用户的通用用例入口。
// 非 active 状态会先提交用户状态和 session 吊销事实，再执行 Redis/IM/Room 的可重试副作用。
func (s *Service) SetUserStatus(ctx context.Context, command UserStatusCommand) (UserStatusResult, error) {
	if command.TargetUserID <= 0 {
		return UserStatusResult{}, xerr.New(xerr.InvalidArgument, "target_user_id is required")
	}
	command.Status = normalizeUserStatus(command.Status)
	if !validUserStatus(command.Status) {
		return UserStatusResult{}, xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	if s.moderationRepository == nil {
		return UserStatusResult{}, xerr.New(xerr.Unavailable, "moderation repository is not configured")
	}
	if command.NowMs <= 0 {
		command.NowMs = s.now().UTC().UnixMilli()
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	if command.AppCode == "" {
		command.AppCode = appcode.FromContext(ctx)
	}
	command.Reason = normalizeModerationReason(command.Reason, command.Status)
	command.OperatorType = normalizeOperatorType(command.OperatorType)

	persisted, err := s.moderationRepository.SetUserStatus(ctx, command)
	if err != nil {
		return UserStatusResult{}, err
	}
	return s.statusResultAfterPersistence(ctx, command, persisted, true), nil
}

// statusResultAfterPersistence 统一把已经提交的用户状态事实转换为外部副作用结果。
// runBanSideEffects=false 用于“用户原本已 banned、只新增重叠封禁事实”的场景，避免重复踢 IM/房间。
func (s *Service) statusResultAfterPersistence(ctx context.Context, command UserStatusCommand, persisted UserStatusPersistenceResult, runBanSideEffects bool) UserStatusResult {
	result := UserStatusResult{
		User:                persisted.User,
		RevokedSessionCount: int64(len(persisted.RevokedSessionIDs)),
		AccessTokenRevoked:  true,
	}
	if command.Status == userdomain.StatusActive || !runBanSideEffects {
		// 恢复 active 只改变准入状态，不重建旧 session、IM 登录或房间 presence。
		return result
	}

	result.AccessTokenRevoked, result.AccessTokenError = s.writeSessionDenylist(ctx, command, persisted.RevokedSessionIDs, userStatusRevokeReason)
	result.IMKicked, result.IMKickError = s.kickIMLogin(ctx, command.TargetUserID)
	result.RoomEvicted, result.RoomID, result.RTCKicked, result.RTCKickError, result.RoomEvictError = s.evictCurrentRoom(ctx, command)
	return result
}

// CreateManagerUserBlock 创建经理封禁记录并把目标用户状态置为 banned。
// 目标范围由 App 策略与经理通用跨区开关共同决定，user-service 必须复验，不能只依赖 gateway 早拒绝。
func (s *Service) CreateManagerUserBlock(ctx context.Context, command ManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
	if s.userRepository == nil || s.moderationRepository == nil {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}
	command.CommandID = strings.TrimSpace(command.CommandID)
	command.Reason = strings.TrimSpace(command.Reason)
	if command.CommandID == "" || command.ManagerUserID <= 0 || command.TargetUserID <= 0 || command.BlockedUntilMS <= 0 {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "manager block command is incomplete")
	}
	if command.NowMS <= 0 {
		command.NowMS = s.now().UTC().UnixMilli()
	}
	if command.BlockedUntilMS <= command.NowMS {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "blocked_until_ms must be in the future")
	}
	if command.Reason == "" {
		command.Reason = "manager_center"
	}
	manager, err := s.userRepository.GetUser(ctx, command.ManagerUserID)
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	target, err := s.userRepository.GetUser(ctx, command.TargetUserID)
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	if s.roleScopePolicyResolver == nil {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "role scope policy resolver is not configured")
	}
	policy, err := s.roleScopePolicyResolver.ResolveRoleScopePolicy(ctx, hostservice.RoleScopeSceneManagerOperations)
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	effectiveScope := policy.BaseScope
	if policy.RegionExpansionConfigurable {
		crossRegionEnabled, err := s.moderationRepository.ManagerCrossRegionEnabled(ctx, command.ManagerUserID)
		if err != nil {
			return ManagerUserBlock{}, UserStatusResult{}, err
		}
		effectiveScope = policy.EffectiveScope(crossRegionEnabled)
	}
	if !managerModerationTargetAllowed(effectiveScope, manager, target) {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.PermissionDenied, "target user is outside manager scope")
	}
	if target.Status != userdomain.StatusActive {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "target user is not active")
	}
	block, err := s.moderationRepository.CreateManagerUserBlock(ctx, command, target)
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	status, err := s.SetUserStatus(ctx, UserStatusCommand{
		AppCode:        appcode.FromContext(ctx),
		TargetUserID:   command.TargetUserID,
		Status:         userdomain.StatusBanned,
		OperatorType:   OperatorTypeAppUser,
		OperatorUserID: command.ManagerUserID,
		Reason:         managerUserBlockReasonPrefix + block.BlockID,
		RequestID:      command.RequestID,
		NowMs:          command.NowMS,
	})
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	return block, status, nil
}

func managerModerationTargetAllowed(scope hostservice.RoleScope, manager userdomain.User, target userdomain.User) bool {
	switch scope {
	case hostservice.RoleScopeGlobal:
		return true
	case hostservice.RoleScopeRegion:
		return manager.RegionID > 0 && manager.RegionID == target.RegionID
	case hostservice.RoleScopeCountry:
		return manager.Country != "" && target.Country != "" && strings.EqualFold(manager.Country, target.Country)
	default:
		return false
	}
}

func (s *Service) ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]ManagerUserBlock, error) {
	if s.moderationRepository == nil {
		return nil, xerr.New(xerr.Unavailable, "moderation repository is not configured")
	}
	if managerUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "manager_user_id is required")
	}
	status = normalizeManagerBlockStatus(status)
	if status == "" {
		status = ManagerUserBlockStatusActive
	}
	if pageSize <= 0 || pageSize > 100 {
		pageSize = 50
	}
	return s.moderationRepository.ListManagerUserBlocks(ctx, managerUserID, status, pageSize)
}

func (s *Service) UnblockManagerUser(ctx context.Context, command ReleaseManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
	command.Status = ManagerUserBlockStatusReleased
	return s.releaseManagerUserBlock(ctx, command)
}

func (s *Service) ExpireManagerUserBlocks(ctx context.Context, limit int32) (int, error) {
	if s.moderationRepository == nil {
		return 0, xerr.New(xerr.Unavailable, "moderation repository is not configured")
	}
	if limit <= 0 || limit > 500 {
		limit = 100
	}
	nowMS := s.now().UTC().UnixMilli()
	releases, err := s.moderationRepository.ExpireManagerUserBlocks(ctx, nowMS, limit)
	if err != nil {
		return 0, err
	}
	// repository 已在每条释放事务内同时检查 admin/manager 两类 active 事实并按需恢复 users.status；
	// service 不再二次 SetUserStatus，避免检查与写入之间插入新的封禁事实。
	return len(releases), nil
}

func (s *Service) releaseManagerUserBlock(ctx context.Context, command ReleaseManagerUserBlockCommand) (ManagerUserBlock, UserStatusResult, error) {
	if s.moderationRepository == nil {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.Unavailable, "moderation repository is not configured")
	}
	command.BlockID = strings.TrimSpace(command.BlockID)
	command.Reason = strings.TrimSpace(command.Reason)
	if command.ManagerUserID <= 0 || command.BlockID == "" {
		return ManagerUserBlock{}, UserStatusResult{}, xerr.New(xerr.InvalidArgument, "manager unblock command is incomplete")
	}
	if command.NowMS <= 0 {
		command.NowMS = s.now().UTC().UnixMilli()
	}
	if command.Reason == "" {
		command.Reason = "manager_center_unblock"
	}
	release, err := s.moderationRepository.ReleaseManagerUserBlock(ctx, command)
	if err != nil {
		return ManagerUserBlock{}, UserStatusResult{}, err
	}
	if !release.ShouldRestore {
		// repository 已判断能否安全恢复；这里不能盲目 SetUserStatus(active)，否则会覆盖后台封禁或并发经理封禁。
		return release.Block, UserStatusResult{User: release.User, AccessTokenRevoked: true}, nil
	}
	return release.Block, UserStatusResult{User: release.User, AccessTokenRevoked: true}, nil
}

func (s *Service) writeSessionDenylist(ctx context.Context, command UserStatusCommand, sessionIDs []string, reason string) (bool, string) {
	if len(sessionIDs) == 0 {
		return true, ""
	}
	if s.sessionDenylist == nil {
		return false, "session denylist is not configured"
	}
	ttl := s.sessionDenylistTTL
	if ttl <= 0 {
		ttl = time.Hour
	}
	reason = strings.TrimSpace(reason)
	if reason == "" {
		reason = userStatusRevokeReason
	}
	failed := 0
	var firstError string
	for _, sessionID := range sessionIDs {
		sessionID = strings.TrimSpace(sessionID)
		if sessionID == "" {
			continue
		}
		// 单个 Redis 写入失败不能回滚已经提交的封禁事实；调用方可通过重复封禁或重新修改国家补偿剩余 session。
		if err := s.sessionDenylist.SetRevokedSession(ctx, command.AppCode, sessionID, reason, ttl); err != nil {
			failed++
			if firstError == "" {
				firstError = err.Error()
			}
		}
	}
	if failed > 0 {
		return false, firstError
	}
	return true, ""
}

func (s *Service) kickIMLogin(ctx context.Context, userID int64) (bool, string) {
	if s.imLoginKicker == nil {
		return false, ""
	}
	if err := s.imLoginKicker.KickUser(ctx, userID); err != nil {
		return false, err.Error()
	}
	return true, ""
}

func (s *Service) evictCurrentRoom(ctx context.Context, command UserStatusCommand) (bool, string, bool, string, string) {
	if s.roomEvictor == nil {
		return false, "", false, "", ""
	}
	resp, err := s.roomEvictor.SystemEvictUser(ctx, &roomv1.SystemEvictUserRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   command.RequestID,
			CommandId:   idgen.New("cmd_user_status_evict"),
			ActorUserId: command.TargetUserID,
			SentAtMs:    command.NowMs,
			AppCode:     command.AppCode,
		},
		TargetUserId:   command.TargetUserID,
		OperatorUserId: command.OperatorUserID,
		Reason:         command.Reason,
		BanFromRoom:    true,
	})
	if err != nil {
		return false, "", false, "", err.Error()
	}
	return resp.GetHadCurrentRoom() && resp.GetResult().GetApplied(), resp.GetRoomId(), resp.GetRtcKicked(), resp.GetRtcKickError(), ""
}

func normalizeUserStatus(status userdomain.Status) userdomain.Status {
	return userdomain.Status(strings.ToLower(strings.TrimSpace(string(status))))
}

func validUserStatus(status userdomain.Status) bool {
	return status == userdomain.StatusActive || status == userdomain.StatusDisabled || status == userdomain.StatusBanned
}

func normalizeOperatorType(operatorType string) string {
	switch strings.ToLower(strings.TrimSpace(operatorType)) {
	case OperatorTypeAdmin:
		return OperatorTypeAdmin
	case OperatorTypeAppUser:
		return OperatorTypeAppUser
	default:
		return OperatorTypeSystem
	}
}

func normalizeManagerBlockStatus(status string) string {
	status = strings.ToLower(strings.TrimSpace(status))
	switch status {
	case ManagerUserBlockStatusActive, ManagerUserBlockStatusReleased, ManagerUserBlockStatusExpired:
		return status
	default:
		return ""
	}
}

func normalizeModerationReason(reason string, status userdomain.Status) string {
	reason = strings.TrimSpace(reason)
	if reason != "" {
		return reason
	}
	switch status {
	case userdomain.StatusBanned:
		return "user_banned"
	case userdomain.StatusDisabled:
		return "user_disabled"
	default:
		return "user_status_active"
	}
}
