package roomapi

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/roomid"
	"hyapp/pkg/xerr"
	"hyapp/services/gateway-service/internal/appconfig"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	// Flutter 在 15 秒终止请求；gateway 留出 5 秒给 DNS/TLS/传输和客户端解码，必须先返回带 request_id 的稳定错误。
	roomGiftPanelTotalBudget = 10 * time.Second
	// 用户主资料是收礼人头像昵称的来源，单独给足正常批量查询时间，但不允许占满整个面板预算。
	roomGiftPanelRecipientCoreBudget = 1500 * time.Millisecond
)

type roomGiftPanelCatalogState struct {
	version   int64
	cacheable bool
}

type roomGiftPanelStageResult[T any] struct {
	value    T
	degraded bool
	err      error
}

// startRoomGiftPanelStage 只负责请求内并发和观测；业务 owner 仍由各下游服务保持。
// 结果通道有缓冲，handler 因总预算先返回时，已响应的 gRPC goroutine 不会卡在投递结果上。
func startRoomGiftPanelStage[T any](group *sync.WaitGroup, ctx context.Context, requestID string, roomID string, stage string, fn func() (T, bool, error)) <-chan roomGiftPanelStageResult[T] {
	resultCh := make(chan roomGiftPanelStageResult[T], 1)
	group.Add(1)
	go func() {
		defer group.Done()
		startedAt := time.Now()
		value, degraded, err := fn()
		logRoomGiftPanelStage(ctx, requestID, roomID, stage, startedAt, err, degraded)
		resultCh <- roomGiftPanelStageResult[T]{value: value, degraded: degraded, err: err}
	}()
	return resultCh
}

func waitRoomGiftPanelStage[T any](ctx context.Context, resultCh <-chan roomGiftPanelStageResult[T]) (T, bool, error) {
	select {
	case result := <-resultCh:
		return result.value, result.degraded, result.err
	case <-ctx.Done():
		var zero T
		return zero, false, ctx.Err()
	}
}

func logRoomGiftPanelStage(ctx context.Context, requestID string, roomID string, stage string, startedAt time.Time, err error, degraded bool) {
	logCtx := logx.With(ctx,
		slog.String("request_id", requestID),
		slog.String("room_id", roomID),
		slog.String("app_code", appcode.FromContext(ctx)),
	)
	attrs := []slog.Attr{
		slog.String("stage", stage),
		slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()),
		slog.Bool("success", err == nil),
		slog.Bool("degraded", degraded),
	}
	switch {
	case roomGiftPanelStageCanceled(err):
		// 首个必要阶段失败后 handler 会取消仍在运行的 sibling RPC；这些 Canceled
		// 只是请求内并发已被收敛，不是新的下游故障。保留 INFO 轨迹用于 request_id
		// 关联，但不能为同一个主错误再制造多条 ERROR 告警。
		attrs = append(attrs, slog.String("error", err.Error()), slog.Bool("canceled", true))
		logx.Info(logCtx, "gateway_room_gift_panel_stage", attrs...)
	case err != nil && degraded:
		attrs = append(attrs, slog.String("error", err.Error()))
		logx.Warn(logCtx, "gateway_room_gift_panel_stage", attrs...)
	case err != nil:
		logx.Error(logCtx, "gateway_room_gift_panel_stage", err, attrs...)
	case degraded:
		logx.Warn(logCtx, "gateway_room_gift_panel_stage", attrs...)
	default:
		logx.Info(logCtx, "gateway_room_gift_panel_stage", attrs...)
	}
}

func roomGiftPanelStageCanceled(err error) bool {
	return err != nil && (errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled)
}

// serveRoomGiftPanel 在单个明确预算内聚合 owner service 读模型。
// 第一阶段只并发互不依赖的 RPC；依赖 visible_region_id 的礼物配置在快照返回后启动，避免猜测区域。
func (h *Handler) serveRoomGiftPanel(writer http.ResponseWriter, request *http.Request) {
	if h.roomQueryClient == nil || h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	roomID := strings.TrimSpace(request.PathValue("room_id"))
	if !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	totalStartedAt := time.Now()
	requestID := httpkit.RequestIDFromContext(request.Context())
	totalCtx, cancel := context.WithTimeout(request.Context(), h.roomGiftPanelTotalTimeout())
	var stageGroup sync.WaitGroup
	defer func() {
		// 先取消所有尚未完成的 RPC，再等待 request-scoped worker 退出；HTTP handler 返回时不遗留后台访问。
		cancel()
		stageGroup.Wait()
	}()
	request = request.WithContext(totalCtx)
	viewerUserID := auth.UserIDFromContext(totalCtx)
	app := appcode.FromContext(totalCtx)

	snapshotCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "snapshot", func() (*roomv1.GetRoomSnapshotResponse, bool, error) {
		resp, err := h.roomQueryClient.GetRoomSnapshot(totalCtx, &roomv1.GetRoomSnapshotRequest{
			Meta:         httpkit.RoomMeta(request, roomID, ""),
			RoomId:       roomID,
			ViewerUserId: viewerUserID,
		})
		return resp, false, err
	})
	balanceCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "balance", func() (*walletv1.GetBalancesResponse, bool, error) {
		resp, err := h.walletClient.GetBalances(totalCtx, &walletv1.GetBalancesRequest{
			RequestId:  requestID,
			UserId:     viewerUserID,
			AssetTypes: []string{"COIN"},
			AppCode:    app,
		})
		return resp, false, err
	})
	catalogCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "catalog", func() (roomGiftPanelCatalogState, bool, error) {
		version, cacheable, err := h.roomGiftCatalogVersion(request, app)
		return roomGiftPanelCatalogState{version: version, cacheable: cacheable}, false, err
	})
	bagCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "bag", func() (*walletv1.ListUserResourcesResponse, bool, error) {
		resp, err := h.roomGiftBagResources(request, viewerUserID)
		// 只有 Wallet 明确标记的资源读瞬时冲突可以降级为空背包；鉴权、参数、
		// 网络和未知内部错误仍让面板失败，避免把真实依赖故障静默伪装成“用户没有背包礼物”。
		return resp, xerr.IsCode(err, xerr.WalletResourceReadTransient), err
	})
	comboConfigCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "combo_config", func() (appconfig.GiftComboConfig, bool, error) {
		config, err := h.roomGiftComboConfig(totalCtx, app, viewerUserID)
		// Remote policy failure disables micro-batching without blocking the gift panel. The stage still
		// returns the error so observability distinguishes an explicit kill switch from a database outage;
		// 当前 Flutter 无论是否聚合都继续走 V2，不能因配置故障切回旧财务入口。
		return config, err != nil, err
	})
	quantityPresetsCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "quantity_presets", func() ([]int32, bool, error) {
		presets, err := h.roomGiftQuantityPresets(totalCtx, app)
		// 配置库故障不能阻断礼物面板；降级到旧档位并保留阶段错误，便于区分未配置和运行时依赖异常。
		return presets, err != nil, err
	})

	snapshotResp, _, err := waitRoomGiftPanelStage(totalCtx, snapshotCh)
	if err != nil {
		h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
		return
	}
	snapshot := snapshotResp.GetRoom()
	recipientUserIDs := roomGiftRecipientUserIDs(snapshot)
	profileCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "recipient_profile", func() (map[int64]roomDisplayProfileData, bool, error) {
		profiles, degraded := h.roomGiftRecipientProfileMap(request, requestID, roomID, recipientUserIDs)
		return profiles, degraded, nil
	})

	catalog, _, err := waitRoomGiftPanelStage(totalCtx, catalogCh)
	if err != nil {
		h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
		return
	}
	configCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "config", func() (roomGiftPanelConfig, bool, error) {
		config, err := h.roomGiftPanelBaseConfigForVersion(request, app, snapshot.GetVisibleRegionId(), catalog.version, catalog.cacheable)
		return config, false, err
	})

	balances, _, err := waitRoomGiftPanelStage(totalCtx, balanceCh)
	if err != nil {
		h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
		return
	}
	panelConfig, _, err := waitRoomGiftPanelStage(totalCtx, configCh)
	if err != nil {
		h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
		return
	}
	bagResources, bagDegraded, err := waitRoomGiftPanelStage(totalCtx, bagCh)
	if err != nil {
		if !bagDegraded || totalCtx.Err() != nil {
			h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
			return
		}
		// 背包只影响用户持有礼物的附加列表，目录、余额和送礼主链路仍可安全展示；
		// 当前请求返回空背包，下一次打开会重新读取，不缓存这次瞬时降级。
		bagResources = &walletv1.ListUserResourcesResponse{}
	}
	recipientProfiles, _, err := waitRoomGiftPanelStage(totalCtx, profileCh)
	if err != nil {
		// 核心资料的 1.5 秒子预算会在内部降级；这里出现 error 只可能是整个 gift-panel 总预算已耗尽。
		h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, err)
		return
	}
	comboConfig, _, comboErr := waitRoomGiftPanelStage(totalCtx, comboConfigCh)
	if comboErr != nil {
		if totalCtx.Err() != nil {
			h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, comboErr)
			return
		}
		comboConfig = disabledGiftComboFallback()
	}
	quantityPresets, _, quantityPresetsErr := waitRoomGiftPanelStage(totalCtx, quantityPresetsCh)
	if quantityPresetsErr != nil {
		if totalCtx.Err() != nil {
			h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, quantityPresetsErr)
			return
		}
		quantityPresets = appconfig.DefaultGiftQuantityPresets()
	}

	panelGifts := append([]giftConfigData{}, panelConfig.Gifts...)
	panelGifts = append(panelGifts, roomGiftBagGiftsFromResources(bagResources, panelConfig.Gifts, snapshot.GetVisibleRegionId())...)
	data := roomGiftPanelData{
		CoinBalance:     coinBalanceFromProto(balances),
		Recipients:      roomGiftRecipients(snapshot, recipientProfiles),
		Tabs:            roomGiftTabs(panelGifts, panelConfig.GiftTypes),
		Gifts:           panelGifts,
		QuantityPresets: quantityPresets,
		ComboConfig:     comboConfig,
	}
	logRoomGiftPanelStage(totalCtx, requestID, roomID, "total", totalStartedAt, nil, false)
	httpkit.WriteOK(writer, request, data)
}

func (h *Handler) writeRoomGiftPanelFailure(writer http.ResponseWriter, request *http.Request, requestID string, roomID string, startedAt time.Time, err error) {
	logRoomGiftPanelStage(request.Context(), requestID, roomID, "total", startedAt, err, false)
	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) || status.Code(err) == codes.DeadlineExceeded || status.Code(err) == codes.Canceled || request.Context().Err() != nil {
		// 明确在 Flutter 15 秒前结束 HTTP 响应；envelope 使用原请求上下文中的 request_id，便于从客户端反查阶段日志。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	httpkit.WriteRPCError(writer, request, err)
}

func (h *Handler) roomGiftPanelTotalTimeout() time.Duration {
	if h.giftPanelTotalBudget > 0 {
		return h.giftPanelTotalBudget
	}
	return roomGiftPanelTotalBudget
}

func (h *Handler) roomGiftPanelRecipientCoreTimeout() time.Duration {
	if h.giftPanelRecipientCoreBudget > 0 {
		return h.giftPanelRecipientCoreBudget
	}
	return roomGiftPanelRecipientCoreBudget
}

// roomGiftRecipientProfileMap 只读取 Flutter gift-panel 实际消费的头像昵称核心资料。
// 装扮、徽章和等级字段维持空对象兼容旧 JSON 形状，但不再发起 activity/wallet enrich RPC；
// 这些资料属于用户卡片而非送礼决策，删除整条 N+1 链路比等待后再超时降级更可靠。
func (h *Handler) roomGiftRecipientProfileMap(request *http.Request, requestID string, roomID string, userIDs []int64) (map[int64]roomDisplayProfileData, bool) {
	userIDs = uniquePositiveUserIDs(userIDs)
	if len(userIDs) == 0 {
		return nil, false
	}
	if h.userProfileClient == nil {
		return nil, true
	}

	coreCtx, cancelCore := context.WithTimeout(request.Context(), h.roomGiftPanelRecipientCoreTimeout())
	defer cancelCore()
	coreRequest := request.WithContext(coreCtx)
	startedAt := time.Now()
	resp, err := h.userProfileClient.BatchGetUsers(coreCtx, &userv1.BatchGetUsersRequest{
		Meta:    httpkit.UserMeta(coreRequest, ""),
		UserIds: userIDs,
	})
	logRoomGiftPanelStage(coreCtx, requestID, roomID, "recipient_profile_core", startedAt, err, err != nil)
	if err != nil {
		// 房间目标 user_id 仍来自 Room Cell 快照；资料服务抖动时保留可送礼目标，只降级头像昵称。
		return nil, true
	}
	return roomGiftCoreProfilesFromUsers(userIDs, resp.GetUsers()), false
}

func roomGiftCoreProfilesFromUsers(userIDs []int64, users map[int64]*userv1.User) map[int64]roomDisplayProfileData {
	profiles := make(map[int64]roomDisplayProfileData, len(users))
	for _, userID := range userIDs {
		user := users[userID]
		if user == nil {
			continue
		}
		// 核心层保留历史完整资料字段；可选资源统一初始化为空对象，超时降级不会改变 JSON 类型。
		profiles[userID] = roomDisplayProfileData{
			UserID:              formatOptionalUserID(user.GetUserId()),
			Username:            user.GetUsername(),
			Avatar:              user.GetAvatar(),
			DisplayUserID:       user.GetDisplayUserId(),
			PrettyID:            user.GetPrettyId(),
			PrettyDisplayUserID: user.GetPrettyDisplayUserId(),
			Gender:              strings.TrimSpace(user.GetGender()),
			Age:                 roomProfileAgeFromBirth(user.GetBirth(), time.Now().UTC()),
			Country:             strings.ToUpper(strings.TrimSpace(user.GetCountry())),
			CountryName:         strings.TrimSpace(user.GetCountryName()),
			CountryDisplayName:  strings.TrimSpace(user.GetCountryDisplayName()),
			CountryFlag:         roomProfileCountryFlag(user.GetCountry()),
			VIP:                 map[string]any{},
			Level:               roomDefaultLevelDisplay(),
			LevelBadges:         map[string]any{},
			Badges:              []map[string]any{},
			AvatarFrame:         map[string]any{},
			ProfileCard:         map[string]any{},
			Vehicle:             map[string]any{},
			MicSeatIcon:         map[string]any{},
			MicSeatAnimation:    map[string]any{},
			Charm:               0,
		}
	}
	return profiles
}
