package user

import (
	"context"
	"database/sql"
	"errors"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	userservice "hyapp/services/user-service/internal/service/user"
)

const managerBlockStatusReasonPrefix = "manager_center_block:"

// ManagerCrossRegionEnabled 读取 active 经理的通用数据范围开关；功能权限仍由 gateway/user host service 分别校验。
func (r *Repository) ManagerCrossRegionEnabled(ctx context.Context, managerUserID int64) (bool, error) {
	var enabled bool
	err := r.db.QueryRowContext(ctx, `
		SELECT can_manage_cross_region = 1
		FROM manager_profiles
		WHERE app_code = ? AND user_id = ? AND status = 'active'
		LIMIT 1
	`, appcode.FromContext(ctx), managerUserID).Scan(&enabled)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	return enabled, err
}

// SetUserStatus 原子更新用户主状态，并在非 active 状态下吊销该用户所有 active refresh session。
func (r *Repository) SetUserStatus(ctx context.Context, command userservice.UserStatusCommand) (userservice.UserStatusPersistenceResult, error) {
	if r == nil || r.db == nil {
		return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.Normalize(command.AppCode)
	if appCode == "" {
		appCode = appcode.FromContext(ctx)
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userservice.UserStatusPersistenceResult{}, err
	}
	defer tx.Rollback()

	var oldStatus string
	if err := tx.QueryRowContext(ctx, `
		SELECT status
		FROM users
		WHERE app_code = ? AND user_id = ?
		FOR UPDATE
	`, appCode, command.TargetUserID).Scan(&oldStatus); err != nil {
		if err == sql.ErrNoRows {
			return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.NotFound, "user not found")
		}
		return userservice.UserStatusPersistenceResult{}, err
	}
	if command.Status == userdomain.StatusActive {
		activeBanCount, err := countActiveManagedBans(ctx, tx, command.TargetUserID)
		if err != nil {
			return userservice.UserStatusPersistenceResult{}, err
		}
		if activeBanCount > 0 {
			// 所有恢复入口都必须尊重 admin/manager 独立封禁事实；不能通过旧 SetUserStatus(active) 绕过新状态机。
			return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Conflict, "user still has active ban facts")
		}
	}
	if command.Status == userdomain.StatusBanned && strings.HasPrefix(command.Reason, managerBlockStatusReasonPrefix) {
		blockID := strings.TrimSpace(strings.TrimPrefix(command.Reason, managerBlockStatusReasonPrefix))
		var blockStatus string
		err := tx.QueryRowContext(ctx, `
			SELECT status
			FROM manager_user_blocks
			WHERE app_code = ? AND block_id = ? AND target_user_id = ?
			FOR UPDATE`, appCode, blockID, command.TargetUserID).Scan(&blockStatus)
		if errors.Is(err, sql.ErrNoRows) {
			// 经理封禁先写 block 再切用户状态；后台可能已在两步之间强制释放该 block，此时不能重新制造无 active 事实的 banned 状态。
			return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Conflict, "manager block is no longer active")
		}
		if err != nil {
			return userservice.UserStatusPersistenceResult{}, err
		}
		if blockStatus != userservice.ManagerUserBlockStatusActive {
			// block 已被经理自行解除、自动到期或后台强制释放时，后到达的状态写入必须作为冲突终止。
			return userservice.UserStatusPersistenceResult{}, xerr.New(xerr.Conflict, "manager block is no longer active")
		}
	}

	_, err = tx.ExecContext(ctx, `
		UPDATE users
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?
	`, string(command.Status), command.NowMs, appCode, command.TargetUserID)
	if err != nil {
		return userservice.UserStatusPersistenceResult{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_status_change_logs (
			app_code, target_user_id, old_status, new_status, operator_type, operator_user_id, reason, request_id, created_at_ms
		)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, appCode, command.TargetUserID, oldStatus, string(command.Status), command.OperatorType, command.OperatorUserID, command.Reason, command.RequestID, command.NowMs); err != nil {
		return userservice.UserStatusPersistenceResult{}, err
	}

	sessionIDs := make([]string, 0)
	if command.Status != userdomain.StatusActive {
		rows, err := tx.QueryContext(ctx, `
			SELECT session_id
			FROM auth_sessions
			WHERE app_code = ? AND user_id = ? AND expires_at_ms > ?
			FOR UPDATE
		`, appCode, command.TargetUserID, command.NowMs)
		if err != nil {
			return userservice.UserStatusPersistenceResult{}, err
		}
		for rows.Next() {
			var sessionID string
			if err := rows.Scan(&sessionID); err != nil {
				_ = rows.Close()
				return userservice.UserStatusPersistenceResult{}, err
			}
			sessionIDs = append(sessionIDs, sessionID)
		}
		if err := rows.Close(); err != nil {
			return userservice.UserStatusPersistenceResult{}, err
		}
		if _, err := tx.ExecContext(ctx, `
			UPDATE auth_sessions
			SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
			WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL
		`, command.NowMs, userStatusRevokeReason(command.Status), command.RequestID, revokedBy(command.OperatorUserID), command.NowMs, appCode, command.TargetUserID); err != nil {
			return userservice.UserStatusPersistenceResult{}, err
		}
	}

	if err := tx.Commit(); err != nil {
		return userservice.UserStatusPersistenceResult{}, err
	}

	user, err := r.GetUser(appcode.WithContext(ctx, appCode), command.TargetUserID)
	if err != nil {
		return userservice.UserStatusPersistenceResult{}, err
	}
	return userservice.UserStatusPersistenceResult{User: user, RevokedSessionIDs: sessionIDs}, nil
}

func userStatusRevokeReason(status userdomain.Status) string {
	switch status {
	case userdomain.StatusBanned:
		return "USER_BANNED"
	case userdomain.StatusDisabled:
		return "USER_DISABLED"
	default:
		return "USER_STATUS_CHANGED"
	}
}

func revokedBy(operatorUserID int64) string {
	if operatorUserID > 0 {
		return "user_status_operator"
	}
	return "user_status"
}

// CreateManagerUserBlock 写入经理封禁记录；用户主状态修改由 service 层随后调用 SetUserStatus 完成。
func (r *Repository) CreateManagerUserBlock(ctx context.Context, command userservice.ManagerUserBlockCommand, target userdomain.User) (userservice.ManagerUserBlock, error) {
	if r == nil || r.db == nil {
		return userservice.ManagerUserBlock{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	blockID := idgen.New("mblock")
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO manager_user_blocks (
			app_code, block_id, command_id, manager_user_id, target_user_id,
			target_display_user_id, target_username, target_avatar, country, target_old_status,
			blocked_until_ms, status, reason, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)`,
		appCode, blockID, command.CommandID, command.ManagerUserID, command.TargetUserID,
		target.CurrentDisplayUserID, target.Username, target.Avatar, target.Country, string(target.Status),
		command.BlockedUntilMS, command.Reason, command.NowMS, command.NowMS,
	)
	if err != nil {
		if managerBlockDuplicate(err) {
			return r.getManagerUserBlockByCommand(ctx, command.CommandID)
		}
		return userservice.ManagerUserBlock{}, err
	}
	return r.getManagerUserBlock(ctx, blockID)
}

func (r *Repository) ListManagerUserBlocks(ctx context.Context, managerUserID int64, status string, pageSize int32) ([]userservice.ManagerUserBlock, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, managerBlockSelectSQL()+`
		WHERE app_code = ? AND manager_user_id = ? AND status = ?
		ORDER BY created_at_ms DESC, block_id DESC
		LIMIT ?`,
		appcode.FromContext(ctx), managerUserID, status, pageSize,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanManagerUserBlocks(rows)
}

func (r *Repository) ReleaseManagerUserBlock(ctx context.Context, command userservice.ReleaseManagerUserBlockCommand) (userservice.ManagerUserBlockRelease, error) {
	return r.releaseManagerUserBlock(ctx, command.BlockID, command.ManagerUserID, command.Status, command.Reason, command.RequestID, command.NowMS)
}

func (r *Repository) ExpireManagerUserBlocks(ctx context.Context, nowMS int64, limit int32) ([]userservice.ManagerUserBlockRelease, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT block_id, manager_user_id
		FROM manager_user_blocks
		WHERE app_code = ? AND status = 'active' AND blocked_until_ms <= ?
		ORDER BY blocked_until_ms ASC, block_id ASC
		LIMIT ?`,
		appcode.FromContext(ctx), nowMS, limit,
	)
	if err != nil {
		return nil, err
	}
	pending := make([]struct {
		blockID       string
		managerUserID int64
	}, 0)
	for rows.Next() {
		var item struct {
			blockID       string
			managerUserID int64
		}
		if err := rows.Scan(&item.blockID, &item.managerUserID); err != nil {
			_ = rows.Close()
			return nil, err
		}
		pending = append(pending, item)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	releases := make([]userservice.ManagerUserBlockRelease, 0, len(pending))
	for _, item := range pending {
		// 逐条释放而不是批量 UPDATE，是为了给每个目标用户单独计算是否还存在其他 active 经理封禁。
		release, err := r.releaseManagerUserBlock(ctx, item.blockID, item.managerUserID, userservice.ManagerUserBlockStatusExpired, "manager_center_block_expired", "manager_block_expire:"+item.blockID, nowMS)
		if err != nil {
			return releases, err
		}
		releases = append(releases, release)
	}
	return releases, nil
}

func (r *Repository) releaseManagerUserBlock(ctx context.Context, blockID string, managerUserID int64, status string, reason string, requestID string, nowMS int64) (userservice.ManagerUserBlockRelease, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	defer func() { _ = tx.Rollback() }()
	block, err := r.getManagerUserBlockForUpdate(ctx, tx, blockID, managerUserID)
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	if block.Status != userservice.ManagerUserBlockStatusActive {
		if err := tx.Commit(); err != nil {
			return userservice.ManagerUserBlockRelease{}, err
		}
		user, err := r.GetUser(ctx, block.TargetUserID)
		return userservice.ManagerUserBlockRelease{Block: block, User: user}, err
	}
	currentStatus, err := lockUserStatus(ctx, tx, appcode.FromContext(ctx), block.TargetUserID)
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	_, err = tx.ExecContext(ctx, `
		UPDATE manager_user_blocks
		SET status = ?, released_reason = ?, released_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND block_id = ?`,
		status, reason, nowMS, nowMS, appcode.FromContext(ctx), blockID,
	)
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	block.Status = status
	block.UpdatedAtMS = nowMS
	restored, err := restoreUserAfterManagedBanRelease(ctx, tx, managedBanRestoreCommand{
		TargetUserID:  block.TargetUserID,
		CurrentStatus: currentStatus,
		OperatorType:  managerReleaseOperatorType(status),
		OperatorID:    managerUserID,
		Reason:        managerReleaseStatusReason(status, block.BlockID),
		RequestID:     requestID,
		NowMs:         nowMS,
	})
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	if err := tx.Commit(); err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	user, err := r.GetUser(ctx, block.TargetUserID)
	if err != nil {
		return userservice.ManagerUserBlockRelease{}, err
	}
	return userservice.ManagerUserBlockRelease{
		Block:         block,
		User:          user,
		ShouldRestore: restored,
	}, nil
}

func (r *Repository) getManagerUserBlock(ctx context.Context, blockID string) (userservice.ManagerUserBlock, error) {
	row := r.db.QueryRowContext(ctx, managerBlockSelectSQL()+`WHERE app_code = ? AND block_id = ?`, appcode.FromContext(ctx), blockID)
	return scanManagerUserBlock(row)
}

func (r *Repository) getManagerUserBlockByCommand(ctx context.Context, commandID string) (userservice.ManagerUserBlock, error) {
	row := r.db.QueryRowContext(ctx, managerBlockSelectSQL()+`WHERE app_code = ? AND command_id = ?`, appcode.FromContext(ctx), commandID)
	return scanManagerUserBlock(row)
}

func (r *Repository) getManagerUserBlockForUpdate(ctx context.Context, tx *sql.Tx, blockID string, managerUserID int64) (userservice.ManagerUserBlock, error) {
	row := tx.QueryRowContext(ctx, managerBlockSelectSQL()+`
		WHERE app_code = ? AND block_id = ? AND manager_user_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), blockID, managerUserID,
	)
	return scanManagerUserBlock(row)
}

func managerReleaseOperatorType(status string) string {
	if status == userservice.ManagerUserBlockStatusExpired {
		return userservice.OperatorTypeSystem
	}
	return userservice.OperatorTypeAppUser
}

func managerReleaseStatusReason(status string, blockID string) string {
	if status == userservice.ManagerUserBlockStatusExpired {
		return "manager_center_block_expired:" + blockID
	}
	return "manager_center_unblock:" + blockID
}

func managerBlockSelectSQL() string {
	return `
		SELECT block_id, manager_user_id, target_user_id, target_display_user_id,
		       target_username, target_avatar, country, target_old_status, blocked_until_ms,
		       status, reason, created_at_ms, updated_at_ms
		FROM manager_user_blocks
		`
}

func scanManagerUserBlocks(rows *sql.Rows) ([]userservice.ManagerUserBlock, error) {
	items := make([]userservice.ManagerUserBlock, 0)
	for rows.Next() {
		item, err := scanManagerUserBlockRows(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

type managerBlockScanner interface {
	Scan(dest ...any) error
}

func scanManagerUserBlock(row managerBlockScanner) (userservice.ManagerUserBlock, error) {
	item, err := scanManagerUserBlockRows(row)
	if errors.Is(err, sql.ErrNoRows) {
		return userservice.ManagerUserBlock{}, xerr.New(xerr.NotFound, "manager block not found")
	}
	return item, err
}

func scanManagerUserBlockRows(row managerBlockScanner) (userservice.ManagerUserBlock, error) {
	var item userservice.ManagerUserBlock
	var oldStatus string
	if err := row.Scan(
		&item.BlockID,
		&item.ManagerUserID,
		&item.TargetUserID,
		&item.TargetDisplayUserID,
		&item.TargetUsername,
		&item.TargetAvatar,
		&item.Country,
		&oldStatus,
		&item.BlockedUntilMS,
		&item.Status,
		&item.Reason,
		&item.CreatedAtMS,
		&item.UpdatedAtMS,
	); err != nil {
		return userservice.ManagerUserBlock{}, err
	}
	item.TargetOldStatus = userdomain.Status(oldStatus)
	return item, nil
}

func managerBlockDuplicate(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "Error 1062")
}
