package roomadmin

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/robotclient"
	"hyapp-admin-server/internal/integration/roomclient"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/repository"
)

var (
	ErrInvalidArgument = errors.New("invalid argument")
	ErrNotFound        = errors.New("not found")
)

type Service struct {
	userDB      *sql.DB
	store       *repository.Store
	roomClient  roomclient.Client
	robotClient robotclient.Client
}

type RoomOwner struct {
	Avatar          string `json:"avatar"`
	CountryCode     string `json:"countryCode,omitempty"`
	DisplayUserID   string `json:"displayUserId"`
	UserID          string `json:"userId"`
	Username        string `json:"username"`
	VisibleRegionID int64  `json:"visibleRegionId,omitempty"`
}

type Room struct {
	CoverURL          string    `json:"coverUrl"`
	ClosedAtMs        int64     `json:"closedAtMs,omitempty"`
	ClosedByAdminID   uint      `json:"closedByAdminId,omitempty"`
	ClosedByAdminName string    `json:"closedByAdminName,omitempty"`
	CloseReason       string    `json:"closeReason,omitempty"`
	CreatedAtMs       int64     `json:"createdAtMs"`
	Heat              int64     `json:"heat"`
	Mode              string    `json:"mode"`
	OccupiedSeatCount int32     `json:"occupiedSeatCount"`
	OnlineCount       int32     `json:"onlineCount"`
	Owner             RoomOwner `json:"owner"`
	OwnerUserID       string    `json:"ownerUserId"`
	RegionName        string    `json:"regionName"`
	RoomID            string    `json:"roomId"`
	RoomShortID       string    `json:"roomShortId"`
	RoomContribution  int64     `json:"roomContribution"`
	SeatCount         int32     `json:"seatCount"`
	SortScore         int64     `json:"sortScore"`
	Status            string    `json:"status"`
	Title             string    `json:"title"`
	UpdatedAtMs       int64     `json:"updatedAtMs"`
	VisibleRegionID   int64     `json:"visibleRegionId"`
}

func NewService(userDB *sql.DB, store *repository.Store, roomClient roomclient.Client, robotClient robotclient.Client) *Service {
	return &Service{userDB: userDB, store: store, roomClient: roomClient, robotClient: robotClient}
}

func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64, error) {
	if s.roomClient == nil {
		return nil, 0, fmt.Errorf("room service client is not configured")
	}
	query = normalizeListQuery(query)
	if scope, external := appctx.ExternalScopeFromContext(ctx); external {
		// 外管查询的区域与 owner 集合均由认证链路生成；空团队必须返回空页，
		// 不能把空 IN 条件退化成主后台的全 App 查询。
		if scope.RegionID <= 0 || len(scope.OwnerUserIDs) == 0 {
			return []Room{}, 0, nil
		}
		query.RegionID = scope.RegionID
		query.OwnerUserID = 0
		query.OwnerUserIDs = append([]int64(nil), scope.OwnerUserIDs...)
	}
	ownerUserID, ownerMatched, err := s.resolveListOwnerUserID(ctx, query)
	if err != nil {
		return nil, 0, err
	}
	if (!query.OwnerFilter.IsEmpty() || strings.TrimSpace(query.OwnerUserKeyword) != "") && !ownerMatched {
		return []Room{}, 0, nil
	}
	roomKeyword := query.Keyword
	if ownerUserID == 0 {
		keywordOwnerUserID, keywordOwnerMatched, err := s.resolveOwnerNumberUserID(ctx, query.Keyword)
		if err != nil {
			return nil, 0, err
		}
		if keywordOwnerMatched {
			ownerUserID = keywordOwnerUserID
			roomKeyword = ""
		}
	}
	result, err := s.roomClient.ListRooms(ctx, roomclient.ListRoomsRequest{
		Page:          query.Page,
		PageSize:      query.PageSize,
		Keyword:       roomKeyword,
		Status:        query.Status,
		RegionID:      query.RegionID,
		OwnerUserID:   ownerUserID,
		OwnerUserIDs:  append([]int64(nil), query.OwnerUserIDs...),
		SortBy:        query.SortBy,
		SortDirection: query.SortDirection,
	})
	if err != nil {
		return nil, 0, err
	}
	items := roomsFromClient(result.Rooms)
	if err := s.fillRoomDetails(ctx, items); err != nil {
		return nil, 0, err
	}
	return items, result.Total, nil
}

func (s *Service) resolveListOwnerUserID(ctx context.Context, query listQuery) (int64, bool, error) {
	if query.OwnerUserID > 0 {
		return query.OwnerUserID, true, nil
	}
	if !query.OwnerFilter.IsEmpty() {
		return shared.ResolveUserIdentityFilter(ctx, s.userDB, appctx.FromContext(ctx), query.OwnerFilter, time.Now().UnixMilli())
	}
	return s.resolveOwnerUserID(ctx, query.OwnerUserKeyword)
}

func (s *Service) resolveOwnerUserID(ctx context.Context, keyword string) (int64, bool, error) {
	keyword = strings.TrimSpace(keyword)
	if keyword == "" {
		return 0, false, nil
	}
	if s.userDB == nil {
		return shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
	}
	// 房主筛选必须先把短号、当前展示号、active 靓号等运营可见号码解析成稳定 user_id，再交给 room-service 按 owner_user_id 精确过滤。
	return shared.ResolveExactUserID(ctx, s.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
}

func (s *Service) resolveOwnerNumberUserID(ctx context.Context, keyword string) (int64, bool, error) {
	keyword = strings.TrimSpace(keyword)
	if keyword == "" || s.userDB == nil {
		return 0, false, nil
	}
	nowMS := time.Now().UnixMilli()
	displaySQL, displayArgs := shared.UserDisplayIDSQL("u", keyword, nowMS)
	orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMS)
	args := append([]any{appctx.FromContext(ctx), keyword}, displayArgs...)
	args = append(args, orderArgs...)
	row := s.userDB.QueryRowContext(ctx, `
		SELECT u.user_id
		FROM users u
		WHERE u.app_code = ? AND (CAST(u.user_id AS CHAR) = ? OR `+displaySQL+`)
		ORDER BY
		  `+orderSQL+`,
		  u.user_id DESC
		LIMIT 1`,
		args...,
	)
	var userID int64
	if err := row.Scan(&userID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return 0, false, nil
		}
		return 0, false, err
	}
	return userID, true, nil
}

func (s *Service) GetRoom(ctx context.Context, roomID string) (Room, error) {
	item, err := s.roomClient.GetRoom(ctx, roomclient.GetRoomRequest{RoomID: roomID})
	if err != nil {
		return Room{}, mapRoomClientError(err)
	}
	items := []Room{roomFromClient(*item)}
	if scope, external := appctx.ExternalScopeFromContext(ctx); external && !externalRoomAllowed(scope, item.OwnerUserID, item.VisibleRegionID) {
		return Room{}, ErrNotFound
	}
	if err := s.fillRoomDetails(ctx, items); err != nil {
		return Room{}, err
	}
	return items[0], nil
}

func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomRequest, actor shared.Actor, requestID string) (Room, error) {
	if s.roomClient == nil {
		return Room{}, fmt.Errorf("room service client is not configured")
	}
	if _, _, err := buildRoomUpdate(req); err != nil {
		return Room{}, err
	}
	if _, external := appctx.ExternalScopeFromContext(ctx); external {
		if req.VisibleRegionID != nil {
			return Room{}, fmt.Errorf("%w: 外管不能修改房间区域", ErrInvalidArgument)
		}
		if _, err := s.GetRoom(ctx, roomID); err != nil {
			return Room{}, err
		}
	}
	if !roomUpdateHasChange(req) {
		return s.GetRoom(ctx, roomID)
	}
	_, err := s.roomClient.UpdateRoom(ctx, roomclient.UpdateRoomRequest{
		RequestID:       requestID,
		RoomID:          roomID,
		ActorUserID:     int64(actor.UserID),
		Title:           req.Title,
		CoverURL:        req.CoverURL,
		Description:     req.Description,
		Mode:            req.Mode,
		Status:          req.Status,
		VisibleRegionID: req.VisibleRegionID,
		CloseReason:     valueOrEmpty(req.CloseReason),
		AdminID:         uint64(actor.UserID),
		AdminName:       actor.Username,
	})
	if err != nil {
		return Room{}, mapRoomClientError(err)
	}
	return s.GetRoom(ctx, roomID)
}

func (s *Service) DeleteRoom(ctx context.Context, roomID string, actor shared.Actor, requestID string) error {
	if s.roomClient == nil {
		return fmt.Errorf("room service client is not configured")
	}
	_, err := s.roomClient.DeleteRoom(ctx, roomclient.DeleteRoomRequest{
		RequestID:   requestID,
		RoomID:      roomID,
		ActorUserID: int64(actor.UserID),
		AdminID:     uint64(actor.UserID),
		AdminName:   actor.Username,
	})
	return mapRoomClientError(err)
}

func (s *Service) BanRoom(ctx context.Context, roomID string, reason string, actor shared.Actor, requestID string) (Room, error) {
	reason = strings.TrimSpace(reason)
	if roomID == "" || reason == "" {
		return Room{}, fmt.Errorf("%w: room and reason are required", ErrInvalidArgument)
	}
	if _, external := appctx.ExternalScopeFromContext(ctx); external {
		if _, err := s.GetRoom(ctx, roomID); err != nil {
			return Room{}, err
		}
	}
	if _, err := s.roomClient.BanRoom(ctx, roomclient.ModerateRoomRequest{
		RequestID: requestID, RoomID: roomID, ActorUserID: int64(actor.UserID), Reason: reason,
		AdminID: uint64(actor.UserID), AdminName: actor.Username,
	}); err != nil {
		return Room{}, mapRoomClientError(err)
	}
	return s.GetRoom(ctx, roomID)
}

func (s *Service) UnbanRoom(ctx context.Context, roomID string, reason string, actor shared.Actor, requestID string) (Room, error) {
	reason = strings.TrimSpace(reason)
	if roomID == "" || reason == "" {
		return Room{}, fmt.Errorf("%w: room and reason are required", ErrInvalidArgument)
	}
	if _, external := appctx.ExternalScopeFromContext(ctx); external {
		if _, err := s.GetRoom(ctx, roomID); err != nil {
			return Room{}, err
		}
	}
	if _, err := s.roomClient.UnbanRoom(ctx, roomclient.ModerateRoomRequest{
		RequestID: requestID, RoomID: roomID, ActorUserID: int64(actor.UserID), Reason: reason,
		AdminID: uint64(actor.UserID), AdminName: actor.Username,
	}); err != nil {
		return Room{}, mapRoomClientError(err)
	}
	return s.GetRoom(ctx, roomID)
}

func externalRoomAllowed(scope appctx.ExternalScope, ownerUserID int64, regionID int64) bool {
	if scope.RegionID <= 0 || regionID != scope.RegionID || ownerUserID <= 0 {
		return false
	}
	for _, allowedOwnerUserID := range scope.OwnerUserIDs {
		if ownerUserID == allowedOwnerUserID {
			return true
		}
	}
	return false
}

func roomUpdateHasChange(req updateRoomRequest) bool {
	return req.Title != nil || req.CoverURL != nil || req.Description != nil || req.Mode != nil || req.Status != nil || req.VisibleRegionID != nil
}

func roomsFromClient(items []roomclient.Room) []Room {
	rooms := make([]Room, 0, len(items))
	for _, item := range items {
		rooms = append(rooms, roomFromClient(item))
	}
	return rooms
}

func roomFromClient(item roomclient.Room) Room {
	status := item.Status
	if status != "active" && status != "banned" {
		status = "closed"
	}
	ownerUserID := strconv.FormatInt(item.OwnerUserID, 10)
	return Room{
		RoomID:            item.RoomID,
		RoomShortID:       item.RoomShortID,
		VisibleRegionID:   item.VisibleRegionID,
		OwnerUserID:       ownerUserID,
		Owner:             RoomOwner{UserID: ownerUserID},
		Title:             item.Title,
		CoverURL:          item.CoverURL,
		Mode:              item.Mode,
		Status:            status,
		CloseReason:       item.CloseReason,
		ClosedByAdminID:   uint(item.ClosedByAdminID),
		ClosedByAdminName: item.ClosedByAdminName,
		ClosedAtMs:        item.ClosedAtMS,
		Heat:              item.Heat,
		RoomContribution:  item.Heat,
		OnlineCount:       item.OnlineCount,
		SeatCount:         item.SeatCount,
		OccupiedSeatCount: item.OccupiedSeatCount,
		SortScore:         item.SortScore,
		CreatedAtMs:       item.CreatedAtMS,
		UpdatedAtMs:       item.UpdatedAtMS,
	}
}

func mapRoomClientError(err error) error {
	if err == nil {
		return nil
	}
	if st, ok := status.FromError(err); ok {
		switch st.Code() {
		case codes.NotFound:
			return ErrNotFound
		case codes.InvalidArgument, codes.FailedPrecondition:
			return fmt.Errorf("%w: %s", ErrInvalidArgument, st.Message())
		}
	}
	return err
}

type roomUpdate struct {
	listColumn string
	roomColumn string
	value      any
}

type snapshotPatch struct {
	coverURLSet     bool
	coverURL        string
	descriptionSet  bool
	description     string
	modeSet         bool
	mode            string
	statusSet       bool
	status          string
	titleSet        bool
	title           string
	visibleRegionID int64
	regionSet       bool
}

func buildRoomUpdate(req updateRoomRequest) ([]roomUpdate, snapshotPatch, error) {
	updates := make([]roomUpdate, 0, 6)
	var patch snapshotPatch
	if req.Title != nil {
		value := strings.TrimSpace(*req.Title)
		if value == "" {
			return nil, patch, fmt.Errorf("%w: 请输入房间名称", ErrInvalidArgument)
		}
		if len(value) > 128 {
			return nil, patch, fmt.Errorf("%w: 房间名称不能超过 128 个字符", ErrInvalidArgument)
		}
		updates = append(updates, roomUpdate{listColumn: "title", value: value})
		patch.titleSet = true
		patch.title = value
	}
	if req.CoverURL != nil {
		value := strings.TrimSpace(*req.CoverURL)
		if len(value) > 512 {
			return nil, patch, fmt.Errorf("%w: 房间头像不能超过 512 个字符", ErrInvalidArgument)
		}
		updates = append(updates, roomUpdate{listColumn: "cover_url", value: value})
		patch.coverURLSet = true
		patch.coverURL = value
	}
	if req.Description != nil {
		value := strings.TrimSpace(*req.Description)
		if len(value) > 512 {
			return nil, patch, fmt.Errorf("%w: 房间简介不能超过 512 个字符", ErrInvalidArgument)
		}
		patch.descriptionSet = true
		patch.description = value
	}
	if req.Mode != nil {
		value := strings.TrimSpace(*req.Mode)
		if value == "" {
			return nil, patch, fmt.Errorf("%w: 请输入房间模式", ErrInvalidArgument)
		}
		if len(value) > 64 {
			return nil, patch, fmt.Errorf("%w: 房间模式不能超过 64 个字符", ErrInvalidArgument)
		}
		updates = append(updates, roomUpdate{listColumn: "mode", roomColumn: "mode", value: value})
		patch.modeSet = true
		patch.mode = value
	}
	if req.Status != nil {
		value, ok := normalizeRoomStatus(*req.Status)
		if !ok {
			return nil, patch, fmt.Errorf("%w: 房间状态不正确", ErrInvalidArgument)
		}
		if value == "closed" {
			reason := strings.TrimSpace(valueOrEmpty(req.CloseReason))
			if reason == "" {
				return nil, patch, fmt.Errorf("%w: 请输入关闭原因", ErrInvalidArgument)
			}
			if len(reason) > 512 {
				return nil, patch, fmt.Errorf("%w: 关闭原因不能超过 512 个字符", ErrInvalidArgument)
			}
		}
		updates = append(updates, roomUpdate{listColumn: "status", roomColumn: "status", value: value})
		patch.statusSet = true
		patch.status = value
	}
	if req.VisibleRegionID != nil {
		value := *req.VisibleRegionID
		if value < 0 {
			return nil, patch, fmt.Errorf("%w: 区域 ID 不正确", ErrInvalidArgument)
		}
		updates = append(updates, roomUpdate{listColumn: "visible_region_id", roomColumn: "visible_region_id", value: value})
		patch.regionSet = true
		patch.visibleRegionID = value
	}
	return updates, patch, nil
}

func normalizeListQuery(query listQuery) listQuery {
	if query.Page < 1 {
		query.Page = 1
	}
	if query.PageSize < 1 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	query.Keyword = strings.TrimSpace(query.Keyword)
	query.OwnerUserKeyword = strings.TrimSpace(query.OwnerUserKeyword)
	query.OwnerFilter.UserID = strings.TrimSpace(query.OwnerFilter.UserID)
	query.OwnerFilter.DisplayUserID = strings.TrimSpace(query.OwnerFilter.DisplayUserID)
	query.OwnerFilter.Username = strings.TrimSpace(query.OwnerFilter.Username)
	query.Status = strings.ToLower(strings.TrimSpace(query.Status))
	if status, ok := normalizeRoomStatus(query.Status); ok {
		query.Status = status
	}
	query.SortBy = normalizeRoomListSortBy(query.SortBy)
	query.SortDirection = normalizeRoomListSortDirection(query.SortDirection)
	return query
}

func roomListOrderBy(query listQuery) string {
	// ORDER BY 字段只能来自 normalizeRoomListSortBy 的白名单，避免把 HTTP 查询值拼进 SQL。
	if query.SortBy == "room_contribution" {
		if query.SortDirection == "asc" {
			return "rle.heat ASC, rle.created_at_ms DESC, rle.room_id DESC"
		}
		return "rle.heat DESC, rle.created_at_ms DESC, rle.room_id DESC"
	}
	return "rle.created_at_ms DESC, rle.room_id DESC"
}

func normalizeRoomListSortBy(sortBy string) string {
	switch strings.ToLower(strings.TrimSpace(sortBy)) {
	case "", "created_at", "createdat":
		return ""
	case "room_contribution", "roomcontribution", "contribution", "heat":
		return "room_contribution"
	default:
		return ""
	}
}

func normalizeRoomListSortDirection(direction string) string {
	switch strings.ToLower(strings.TrimSpace(direction)) {
	case "asc", "ascending":
		return "asc"
	default:
		return "desc"
	}
}

func normalizeRoomStatus(status string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "":
		return "", true
	case "active", "normal":
		return "active", true
	case "closed":
		return "closed", true
	default:
		return "", false
	}
}

func valueOrEmpty(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func (s *Service) fillRoomDetails(ctx context.Context, items []Room) error {
	if s.userDB == nil || len(items) == 0 {
		return nil
	}

	ownerIDs := make([]int64, 0, len(items))
	regionIDs := make([]int64, 0, len(items))
	for _, item := range items {
		if ownerID, err := strconv.ParseInt(item.OwnerUserID, 10, 64); err == nil && ownerID > 0 {
			ownerIDs = append(ownerIDs, ownerID)
		}
		if item.VisibleRegionID > 0 {
			regionIDs = append(regionIDs, item.VisibleRegionID)
		}
	}

	owners, err := s.queryRoomOwners(ctx, uniqueInt64s(ownerIDs))
	if err != nil {
		return err
	}
	regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs))
	if err != nil {
		return err
	}

	for index := range items {
		if ownerID, err := strconv.ParseInt(items[index].OwnerUserID, 10, 64); err == nil {
			if owner, ok := owners[ownerID]; ok {
				items[index].Owner = owner
			}
		}
		if name, ok := regions[items[index].VisibleRegionID]; ok {
			items[index].RegionName = name
		}
	}
	return nil
}

func (s *Service) queryRoomOwners(ctx context.Context, ownerIDs []int64) (map[int64]RoomOwner, error) {
	owners := make(map[int64]RoomOwner, len(ownerIDs))
	if len(ownerIDs) == 0 {
		return owners, nil
	}
	args := append([]any{appctx.FromContext(ctx)}, int64Args(ownerIDs)...)
	rows, err := s.userDB.QueryContext(ctx, `
		SELECT user_id,
		       current_display_user_id,
		       COALESCE(username, ''),
		       COALESCE(avatar, ''),
		       COALESCE(country, ''),
		       COALESCE(region_id, 0)
		FROM users
		WHERE app_code = ? AND user_id IN (`+placeholders(len(ownerIDs))+`)
	`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var userID int64
		var owner RoomOwner
		if err := rows.Scan(&userID, &owner.DisplayUserID, &owner.Username, &owner.Avatar, &owner.CountryCode, &owner.VisibleRegionID); err != nil {
			return nil, err
		}
		owner.UserID = strconv.FormatInt(userID, 10)
		owners[userID] = owner
	}
	return owners, rows.Err()
}

func (s *Service) queryRegionNames(ctx context.Context, regionIDs []int64) (map[int64]string, error) {
	regions := make(map[int64]string, len(regionIDs))
	if len(regionIDs) == 0 {
		return regions, nil
	}
	args := append([]any{appctx.FromContext(ctx)}, int64Args(regionIDs)...)
	rows, err := s.userDB.QueryContext(ctx, `
		SELECT region_id, name
		FROM regions
		WHERE app_code = ? AND region_id IN (`+placeholders(len(regionIDs))+`)
	`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var regionID int64
		var name string
		if err := rows.Scan(&regionID, &name); err != nil {
			return nil, err
		}
		regions[regionID] = name
	}
	return regions, rows.Err()
}

func uniqueInt64s(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	unique := make([]int64, 0, len(values))
	for _, value := range values {
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		unique = append(unique, value)
	}
	return unique
}

func placeholders(count int) string {
	if count <= 0 {
		return ""
	}
	items := make([]string, count)
	for index := range items {
		items[index] = "?"
	}
	return strings.Join(items, ",")
}

func int64Args(values []int64) []any {
	args := make([]any, 0, len(values))
	for _, value := range values {
		args = append(args, value)
	}
	return args
}

func offset(page int, pageSize int) int {
	return (page - 1) * pageSize
}
