package service

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"log/slog"
	"math"
	"strings"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
)

const (
	roomListTabHot        = "hot"
	roomListTabNew        = "new"
	roomListTabVisited    = "visited"
	roomListTabFriend     = "friend"
	roomListTabFollowing  = "following"
	roomListTabFollowed   = "followed"
	defaultRoomListLimit  = 20
	maxRoomListLimit      = 50
	maxRoomListQueryRunes = 64
	roomHotHeatWeight     = 650000
	roomHotOnlineWeight   = 300000
	roomHotMicWeight      = 50000
	roomHotOnlineCap      = 50
	roomHotMicCap         = 10
)

// roomListCursor 是服务端生成的不透明翻页游标。
// 客户端只能原样传回，不能依赖内部字段；tab 写入游标用于拒绝跨 tab 复用。
type roomListCursor struct {
	Tab           string `json:"tab"`
	Query         string `json:"query,omitempty"`
	CountryCode   string `json:"country_code,omitempty"`
	SortScore     int64  `json:"sort_score,omitempty"`
	CreatedAtMS   int64  `json:"created_at_ms,omitempty"`
	UpdatedAtMS   int64  `json:"updated_at_ms,omitempty"`
	SubjectUserID int64  `json:"subject_user_id,omitempty"`
	PinListRank   int64  `json:"pin_list_rank,omitempty"`
	Pinned        bool   `json:"pinned,omitempty"`
	PinWeight     int64  `json:"pin_weight,omitempty"`
	PinnedUntilMS int64  `json:"pinned_until_ms,omitempty"`
	RoomID        string `json:"room_id"`
}

// ListRooms 查询公共房间发现列表读模型，并用服务端解析出的用户区域决定置顶和普通房排序桶。
func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	viewerUserID := req.GetViewerUserId()
	if viewerUserID <= 0 {
		// viewer_user_id 必须由 gateway 鉴权后填充，room-service 不接受匿名列表查询。
		return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
	}

	tab := normalizeRoomListTab(req.GetTab())
	if tab == "" {
		return nil, xerr.New(xerr.InvalidArgument, "tab is invalid")
	}

	limit := normalizeRoomListLimit(req.GetLimit())
	query, err := normalizeRoomListQuery(req.GetQuery())
	if err != nil {
		return nil, err
	}
	countryCode := normalizeRoomCountryCode(req.GetCountryCode())
	cursor, err := decodeRoomListCursor(tab, query, countryCode, req.GetCursor())
	if err != nil {
		return nil, err
	}

	entries, err := s.repository.ListRoomListEntries(ctx, RoomListQuery{
		AppCode:             appcode.FromContext(ctx),
		VisibleRegionID:     normalizeVisibleRegionID(req.GetVisibleRegionId()),
		Tab:                 tab,
		Query:               query,
		CountryCode:         countryCode,
		Limit:               limit + 1,
		CursorSortScore:     cursor.SortScore,
		CursorCreatedAtMS:   cursor.CreatedAtMS,
		CursorRoomID:        cursor.RoomID,
		CursorPinListRank:   cursor.PinListRank,
		CursorPinned:        cursor.Pinned,
		CursorPinWeight:     cursor.PinWeight,
		CursorPinnedUntilMS: cursor.PinnedUntilMS,
		NowMS:               s.clock.Now().UnixMilli(),
	})
	if err != nil {
		return nil, err
	}

	return roomListResponseFromEntries(tab, query, countryCode, entries, limit), nil
}

// ListRoomFeeds 查询 Mine 页 visited/friend/following/followed 房间流。
func (s *Service) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	viewerUserID := req.GetViewerUserId()
	if viewerUserID <= 0 {
		// feed 一定绑定当前登录用户，不能允许匿名或客户端自选用户。
		return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
	}

	tab := normalizeRoomFeedTab(req.GetTab())
	if tab == "" {
		return nil, xerr.New(xerr.InvalidArgument, "tab is invalid")
	}

	limit := normalizeRoomListLimit(req.GetLimit())
	query, err := normalizeRoomListQuery(req.GetQuery())
	if err != nil {
		return nil, err
	}
	cursor, err := decodeRoomListCursor(tab, query, "", req.GetCursor())
	if err != nil {
		return nil, err
	}
	if roomFeedTabRequiresRelationCursor(tab) && strings.TrimSpace(req.GetCursor()) != "" && cursor.SubjectUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "cursor is invalid")
	}

	var entries []RoomListEntry
	switch tab {
	case roomListTabVisited:
		entries, err = s.repository.ListRoomUserFeedEntries(ctx, RoomUserFeedQuery{
			AppCode:           appcode.FromContext(ctx),
			UserID:            viewerUserID,
			FeedType:          tab,
			VisibleRegionID:   normalizeVisibleRegionID(req.GetVisibleRegionId()),
			Query:             query,
			Limit:             limit + 1,
			CursorUpdatedAtMS: cursor.UpdatedAtMS,
			CursorRoomID:      cursor.RoomID,
		})
	case roomListTabFollowed:
		entries, err = s.repository.ListRoomFollowEntries(ctx, RoomFollowQuery{
			AppCode:            appcode.FromContext(ctx),
			UserID:             viewerUserID,
			VisibleRegionID:    normalizeVisibleRegionID(req.GetVisibleRegionId()),
			Query:              query,
			Limit:              limit + 1,
			CursorFollowedAtMS: cursor.UpdatedAtMS,
			CursorRoomID:       cursor.RoomID,
		})
	default:
		// friend/following 的关系事实属于 user-service；room-service 只根据 gateway 传入的一页关系用户查询 active 房间卡片。
		entries, err = s.repository.ListRoomRelatedFeedEntries(ctx, RoomRelatedFeedQuery{
			AppCode:             appcode.FromContext(ctx),
			VisibleRegionID:     normalizeVisibleRegionID(req.GetVisibleRegionId()),
			RelatedUsers:        roomFeedRelatedUsersFromProto(req.GetRelatedUsers()),
			Query:               query,
			Limit:               limit + 1,
			CursorUpdatedAtMS:   cursor.UpdatedAtMS,
			CursorSubjectUserID: cursor.SubjectUserID,
			CursorRoomID:        cursor.RoomID,
		})
	}
	if err != nil {
		return nil, err
	}

	return roomListResponseFromEntries(tab, query, "", entries, limit), nil
}

// GetMyRoom 查询当前用户自己创建的房间，不依赖 room_list_entries 投影命中。
func (s *Service) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	ownerUserID := req.GetOwnerUserId()
	if ownerUserID <= 0 {
		// owner_user_id 必须由 gateway 鉴权后填充，Mine 卡片不能接受客户端任意查人。
		return nil, xerr.New(xerr.InvalidArgument, "owner_user_id is required")
	}

	serverTimeMS := s.clock.Now().UnixMilli()
	meta, exists, err := s.repository.GetRoomMetaByOwner(ctx, ownerUserID)
	if err != nil {
		return nil, err
	}
	if !exists {
		return &roomv1.GetMyRoomResponse{ServerTimeMs: serverTimeMS}, nil
	}

	snapshot, err := s.currentSnapshot(ctx, meta.RoomID)
	if err != nil {
		return nil, err
	}

	return &roomv1.GetMyRoomResponse{
		HasRoom:      true,
		Room:         roomListItemToProto(roomListEntryFromMetaAndSnapshot(meta, snapshot, serverTimeMS)),
		ServerTimeMs: serverTimeMS,
	}, nil
}

func roomListResponseFromEntries(tab string, query string, countryCode string, entries []RoomListEntry, limit int) *roomv1.ListRoomsResponse {
	nextCursor := ""
	if len(entries) > limit {
		// 多查一条用于判断是否存在下一页；返回数据只包含请求的 limit 条。
		last := entries[limit-1]
		nextCursor = encodeRoomListCursor(tab, query, countryCode, last)
		entries = entries[:limit]
	}

	items := make([]*roomv1.RoomListItem, 0, len(entries))
	for _, entry := range entries {
		// response 只暴露列表卡片字段，不把 repository 的 cursor 辅助字段泄露给客户端。
		items = append(items, roomListItemToProto(entry))
	}

	return &roomv1.ListRoomsResponse{
		Rooms:      items,
		NextCursor: nextCursor,
	}
}

func roomFeedRelatedUsersFromProto(items []*roomv1.RoomFeedRelatedUser) []RoomFeedRelatedUser {
	// gateway 已做关系授权，room-service 这里只过滤无效项并保持输入顺序供 repository 排序使用。
	relatedUsers := make([]RoomFeedRelatedUser, 0, len(items))
	for _, item := range items {
		if item == nil || item.GetUserId() <= 0 || item.GetRelationUpdatedAtMs() <= 0 {
			continue
		}
		relatedUsers = append(relatedUsers, RoomFeedRelatedUser{
			UserID:              item.GetUserId(),
			RelationUpdatedAtMS: item.GetRelationUpdatedAtMs(),
		})
	}
	return relatedUsers
}

// projectRoomListBestEffort 根据最新快照刷新房间列表投影。
func (s *Service) projectRoomListBestEffort(ctx context.Context, snapshot *roomv1.RoomSnapshot) {
	if snapshot == nil || snapshot.GetRoomId() == "" {
		return
	}

	meta, exists, err := s.repository.GetRoomMeta(ctx, snapshot.GetRoomId())
	if err != nil {
		// 投影不能回滚已提交 Room Cell 命令，先记录日志；后续可用 projection task 补偿。
		logx.Error(ctx, "room_list_load_meta_failed", err, slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId()))
		return
	}
	if !exists {
		logx.Warn(ctx, "room_list_meta_missing", slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId()))
		return
	}

	nowMS := s.clock.Now().UnixMilli()
	entry := roomListEntryFromSnapshot(snapshot, meta.VisibleRegionID, nowMS)
	if err := s.repository.UpsertRoomListEntry(ctx, entry); err != nil {
		// 列表读模型是最终一致；失败不改变命令成功语义。
		logx.Error(ctx, "room_list_upsert_failed", err, slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId()), slog.Int64("region_id", meta.VisibleRegionID))
	}
}

// roomListEntryFromSnapshot 把完整房间快照压缩成列表卡片读模型。
// 这里刻意只提取列表需要的字段，避免列表表结构反向绑定 RoomState 全量细节。
func roomListEntryFromSnapshot(snapshot *roomv1.RoomSnapshot, visibleRegionID int64, nowMS int64) RoomListEntry {
	onlineCount := int32(len(snapshot.GetOnlineUsers()))
	seatCount := int32(len(snapshot.GetMicSeats()))
	occupiedSeatCount := int32(0)
	for _, seat := range snapshot.GetMicSeats() {
		if seat.GetUserId() != 0 {
			// 列表只需要占麦数量，不暴露具体麦位用户。
			occupiedSeatCount++
		}
	}

	// 展示资料仍放在 RoomExt，列表投影只在刷新时复制需要的字段。
	title := snapshot.GetRoomExt()["title"]
	coverURL := snapshot.GetRoomExt()["cover_url"]
	roomShortID := snapshot.GetRoomShortId()
	if roomShortID == "" {
		roomShortID = snapshot.GetRoomExt()["room_short_id"]
	}

	return RoomListEntry{
		AppCode:           snapshot.GetAppCode(),
		RoomID:            snapshot.GetRoomId(),
		RoomShortID:       roomShortID,
		VisibleRegionID:   normalizeVisibleRegionID(visibleRegionID),
		OwnerCountryCode:  normalizeRoomCountryCode(snapshot.GetRoomExt()[roomExtOwnerCountryKey]),
		OwnerUserID:       snapshot.GetOwnerUserId(),
		Title:             title,
		CoverURL:          coverURL,
		Mode:              snapshot.GetMode(),
		Status:            snapshot.GetStatus(),
		Locked:            snapshot.GetLocked(),
		Heat:              snapshot.GetHeat(),
		OnlineCount:       onlineCount,
		SeatCount:         seatCount,
		OccupiedSeatCount: occupiedSeatCount,
		SortScore:         roomListSortScore(snapshot.GetHeat(), onlineCount, occupiedSeatCount),
		CreatedAtMS:       nowMS,
		UpdatedAtMS:       nowMS,
	}
}

func roomListSortScore(heat int64, onlineCount int32, occupiedSeatCount int32) int64 {
	// 热门分只属于列表读模型，不进入 RoomState；贡献值先做 log 压缩，避免历史大房间永久碾压当前有人房间。
	if heat < 0 {
		heat = 0
	}
	online := int64(onlineCount)
	if online < 0 {
		online = 0
	}
	if online > roomHotOnlineCap {
		online = roomHotOnlineCap
	}
	occupied := int64(occupiedSeatCount)
	if occupied < 0 {
		occupied = 0
	}
	if occupied > roomHotMicCap {
		occupied = roomHotMicCap
	}

	heatScore := math.Log2(float64(heat) + 1)
	onlineScore := math.Log2(float64(online) + 1)
	micScore := float64(occupied)
	return int64(heatScore*roomHotHeatWeight + onlineScore*roomHotOnlineWeight + micScore*roomHotMicWeight)
}

// normalizeRoomListTab 只支持公共发现列表 tab，Mine 关系流走 ListRoomFeeds。
func normalizeRoomListTab(tab string) string {
	switch strings.ToLower(strings.TrimSpace(tab)) {
	case "", roomListTabHot:
		return roomListTabHot
	case roomListTabNew:
		return roomListTabNew
	default:
		return ""
	}
}

func normalizeRoomFeedTab(tab string) string {
	// Mine 页房间流只允许明确 tab，避免空 tab 被误解成公共 hot 列表。
	switch strings.ToLower(strings.TrimSpace(tab)) {
	case roomListTabVisited:
		return roomListTabVisited
	case roomListTabFriend:
		return roomListTabFriend
	case roomListTabFollowing:
		return roomListTabFollowing
	case roomListTabFollowed:
		return roomListTabFollowed
	default:
		return ""
	}
}

func roomFeedTabRequiresRelationCursor(tab string) bool {
	// friend/following 的排序边界包含关系用户 ID，visited/followed 只需要房间 ID 和时间。
	return tab == roomListTabFriend || tab == roomListTabFollowing
}

func normalizeRoomListQuery(query string) (string, error) {
	query = strings.TrimSpace(query)
	if query == "" {
		return "", nil
	}
	if len([]rune(query)) > maxRoomListQueryRunes {
		// 搜索词进入 LIKE 查询，长度必须在入口收敛，避免大字符串放大数据库扫描成本。
		return "", xerr.New(xerr.InvalidArgument, "query is too long")
	}

	return query, nil
}

// normalizeRoomListLimit 统一列表分页上限，防止客户端用大 limit 打穿 MySQL 读模型。
func normalizeRoomListLimit(limit int32) int {
	if limit <= 0 {
		return defaultRoomListLimit
	}
	if limit > maxRoomListLimit {
		return maxRoomListLimit
	}
	return int(limit)
}

// normalizeVisibleRegionID 把负数区域收敛到 GLOBAL 桶，避免非法区域影响 SQL 查询边界。
func normalizeVisibleRegionID(regionID int64) int64 {
	if regionID < 0 {
		return 0
	}
	return regionID
}

// normalizeRoomCountryCode 把国家码收敛为大写字母；非法值按空值处理，避免污染 SQL 精确筛选。
func normalizeRoomCountryCode(countryCode string) string {
	countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
	if len(countryCode) < 2 || len(countryCode) > 3 {
		return ""
	}
	for _, ch := range countryCode {
		if ch < 'A' || ch > 'Z' {
			return ""
		}
	}
	return countryCode
}

// decodeRoomListCursor 校验不透明 cursor 和当前 tab 一致，禁止客户端跨排序维度复用游标。
func decodeRoomListCursor(tab string, query string, countryCode string, encoded string) (roomListCursor, error) {
	encoded = strings.TrimSpace(encoded)
	if encoded == "" {
		return roomListCursor{Tab: tab, Query: query, CountryCode: countryCode}, nil
	}

	payload, err := base64.RawURLEncoding.DecodeString(encoded)
	if err != nil {
		return roomListCursor{}, xerr.New(xerr.InvalidArgument, "cursor is invalid")
	}

	var cursor roomListCursor
	if err := json.Unmarshal(payload, &cursor); err != nil {
		return roomListCursor{}, xerr.New(xerr.InvalidArgument, "cursor is invalid")
	}
	if cursor.Tab != tab || cursor.Query != query || cursor.CountryCode != countryCode || cursor.RoomID == "" {
		return roomListCursor{}, xerr.New(xerr.InvalidArgument, "cursor is invalid")
	}

	return cursor, nil
}

// encodeRoomListCursor 把最后一条列表记录编码成下一页边界。
// cursor 不暴露契约语义，后续调整排序字段时可以只兼容服务端解析逻辑。
func encodeRoomListCursor(tab string, query string, countryCode string, entry RoomListEntry) string {
	cursor := roomListCursor{
		Tab:           tab,
		Query:         query,
		CountryCode:   countryCode,
		SortScore:     entry.SortScore,
		CreatedAtMS:   entry.CreatedAtMS,
		UpdatedAtMS:   entry.UpdatedAtMS,
		SubjectUserID: entry.FeedSubjectUserID,
		PinListRank:   entry.PinListRank,
		Pinned:        entry.IsPinned,
		PinWeight:     entry.PinWeight,
		PinnedUntilMS: entry.PinnedUntilMS,
		RoomID:        entry.RoomID,
	}

	payload, err := json.Marshal(cursor)
	if err != nil {
		// 固定结构理论上不会 marshal 失败；失败时返回空游标让客户端停止翻页。
		return ""
	}

	return base64.RawURLEncoding.EncodeToString(payload)
}

// roomListItemToProto 将 repository 投影转换为跨服务响应对象。
func roomListItemToProto(entry RoomListEntry) *roomv1.RoomListItem {
	return &roomv1.RoomListItem{
		AppCode:           entry.AppCode,
		RoomId:            entry.RoomID,
		RoomShortId:       entry.RoomShortID,
		OwnerUserId:       entry.OwnerUserID,
		Title:             entry.Title,
		CoverUrl:          entry.CoverURL,
		Mode:              entry.Mode,
		Status:            entry.Status,
		Locked:            entry.Locked,
		Heat:              entry.Heat,
		OnlineCount:       entry.OnlineCount,
		SeatCount:         entry.SeatCount,
		OccupiedSeatCount: entry.OccupiedSeatCount,
		VisibleRegionId:   entry.VisibleRegionID,
		CountryCode:       normalizeRoomCountryCode(entry.OwnerCountryCode),
	}
}

func roomListEntryFromMetaAndSnapshot(meta RoomMeta, snapshot *roomv1.RoomSnapshot, nowMS int64) RoomListEntry {
	if snapshot == nil || snapshot.GetRoomId() == "" {
		// GetMyRoom 兜底路径：即使快照暂时不可用，也能用 rooms 元数据返回“有房间”的最小卡片。
		return RoomListEntry{
			AppCode:          meta.AppCode,
			RoomID:           meta.RoomID,
			RoomShortID:      meta.RoomShortID,
			VisibleRegionID:  normalizeVisibleRegionID(meta.VisibleRegionID),
			OwnerCountryCode: "",
			OwnerUserID:      meta.OwnerUserID,
			Mode:             meta.Mode,
			Status:           meta.Status,
			Locked:           meta.RoomPasswordHash != "",
			SeatCount:        meta.SeatCount,
			CreatedAtMS:      nowMS,
			UpdatedAtMS:      nowMS,
		}
	}

	entry := roomListEntryFromSnapshot(snapshot, meta.VisibleRegionID, nowMS)
	if entry.RoomShortID == "" {
		// 老快照可能没有 room_short_id，rooms meta 是短号的持久兜底来源。
		entry.RoomShortID = meta.RoomShortID
	}
	if entry.Mode == "" {
		// mode/status/owner 都从 meta 兜底，避免快照扩展字段缺失导致 Mine 卡片不可用。
		entry.Mode = meta.Mode
	}
	if entry.Status == "" {
		entry.Status = meta.Status
	}
	if entry.OwnerUserID <= 0 {
		entry.OwnerUserID = meta.OwnerUserID
	}
	return entry
}
