package service

import (
	"context"
	"errors"
	"strings"
	"time"
	"unicode/utf8"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/giftlimits"
	"hyapp/pkg/roomid"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/rank"
	"hyapp/services/room-service/internal/room/state"
)

// SaveRoomBackground 把房主上传后的背景图 URL 保存到房间维度素材列表。
// 该动作只维护低频素材表，不改变 Room Cell 当前背景；真正生效必须调用 SetRoomBackground。
func (s *Service) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	roomID, actorUserID, commandID, media, err := normalizeSaveRoomBackground(req)
	if err != nil {
		return nil, err
	}
	if commandID != "" && !giftlimits.ValidCommandID(commandID) {
		return nil, xerr.New(xerr.InvalidArgument, "command_id is invalid")
	}

	imageURL := media.URL
	replayCommandID := commandID
	if replayCommandID == "" {
		// legacy_timed 没有客户端 command_id；这里先算出与首次提交一致的稳定键，确保旧成功也能先于可变权限重放。
		replayCommandID = legacyRoomBackgroundCommandID(req.GetMeta().GetAppCode(), roomID, actorUserID, imageURL)
	}
	if reader, ok := s.repository.(RoomBackgroundCommandReader); ok {
		existing, exists, readErr := reader.GetRoomBackgroundByCommand(ctx, roomID, replayCommandID)
		if readErr != nil {
			return nil, readErr
		}
		if exists {
			if !sameRoomBackgroundSaveRequest(existing, actorUserID, imageURL, media) {
				return nil, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict")
			}
			// 已成功保存是不可变事实。房间随后关闭、owner 元数据变化或 VIP 到期都不能把同一重试改成失败。
			return &roomv1.SaveRoomBackgroundResponse{
				Background:   roomBackgroundToProto(existing),
				ServerTimeMs: s.clock.Now().UnixMilli(),
			}, nil
		}
	}
	if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil {
		return nil, err
	}
	access, err := s.loadEffectiveVIPAccess(ctx, req.GetMeta().GetRequestId(), actorUserID)
	if err != nil {
		return nil, err
	}
	if access.programType == tieredPrivilegeVIPProgram {
		if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, "save_room_background"); err != nil {
			// Fami P1 的素材保存会形成持久数据，必须在写库前校验当前精确 benefit。
			return nil, err
		}
		if !giftlimits.ValidCommandID(commandID) {
			return nil, xerr.New(xerr.InvalidArgument, "command_id is invalid")
		}
		if err := validateRoomMedia(req.GetMeta().GetAppCode(), roomID, actorUserID, roomMediaPurposeBackground, media); err != nil {
			return nil, err
		}
		if err := s.requireActiveRoomMediaUpload(ctx, roomID, actorUserID, roomMediaPurposeBackground, commandID, media); err != nil {
			// URL/key 前缀只能证明路径归属；这里还要用 active registration 锁定首次嗅探的哈希、尺寸和格式。
			return nil, err
		}
	} else {
		// Lalu legacy_timed 保留历史 image_url-only 协议；服务端按 URL 生成稳定幂等键，空 Media 表示未经过新版专用上传。
		if media.URL == "" || utf8.RuneCountInString(media.URL) > maxRoomBackgroundRunes {
			return nil, xerr.New(xerr.InvalidArgument, "image_url is invalid")
		}
		commandID = replayCommandID
		media = state.RoomMedia{}
	}

	nowMS := s.clock.Now().UnixMilli()
	background, err := s.repository.SaveRoomBackground(ctx, RoomBackgroundImage{
		AppCode:         req.GetMeta().GetAppCode(),
		RoomID:          roomID,
		ImageURL:        imageURL,
		CommandID:       commandID,
		Media:           media,
		CreatedByUserID: actorUserID,
		CreatedAtMS:     nowMS,
		UpdatedAtMS:     nowMS,
	})
	if err != nil {
		if errors.Is(err, ErrRoomBackgroundCommandConflict) {
			return nil, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict")
		}
		return nil, err
	}

	return &roomv1.SaveRoomBackgroundResponse{
		Background:   roomBackgroundToProto(background),
		ServerTimeMs: s.clock.Now().UnixMilli(),
	}, nil
}

// ListRoomBackgrounds 返回房主保存过的背景图列表和当前生效背景。
// 列表属于房间编辑面板数据，因此只允许房主读取。
func (s *Service) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	roomID, viewerUserID, err := normalizeListRoomBackgrounds(req)
	if err != nil {
		return nil, err
	}
	if err := s.requireRoomOwnerMeta(ctx, roomID, viewerUserID); err != nil {
		return nil, err
	}

	backgrounds, err := s.repository.ListRoomBackgrounds(ctx, roomID)
	if err != nil {
		return nil, err
	}
	snapshot, err := s.currentSnapshot(ctx, roomID)
	if err != nil {
		return nil, err
	}

	return &roomv1.ListRoomBackgroundsResponse{
		Backgrounds:           roomBackgroundsToProto(backgrounds),
		SelectedBackgroundUrl: snapshot.GetRoomExt()[roomExtBackgroundKey],
		SelectedBackground:    snapshot.GetActiveBackground(),
		ServerTimeMs:          s.clock.Now().UnixMilli(),
	}, nil
}

// SetRoomBackground 把已保存的背景图设置为当前房间背景。
// 当前背景属于房间展示状态，必须经 Room Cell 命令链路、command log 和快照恢复。
func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	roomID, actorUserID, backgroundID, err := normalizeSetRoomBackground(req)
	if err != nil {
		return nil, err
	}
	cmd := command.SetRoomBackground{
		Base:         baseFromMeta(req.GetMeta()),
		BackgroundID: backgroundID,
	}
	_, record, seen, err := s.commandRecordForIdempotency(ctx, cmd)
	if err != nil {
		return nil, err
	}

	var background RoomBackgroundImage
	if seen {
		stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload)
		if deserializeErr != nil {
			return nil, deserializeErr
		}
		storedCommand, ok := stored.(*command.SetRoomBackground)
		if !ok {
			return nil, xerr.New(xerr.Internal, "stored room background command is invalid")
		}
		cmd = *storedCommand
		background = roomBackgroundFromSetCommand(cmd)
	} else {
		if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil {
			return nil, err
		}
		var exists bool
		background, exists, err = s.repository.GetRoomBackground(ctx, roomID, backgroundID)
		if err != nil {
			return nil, err
		}
		if !exists {
			return nil, xerr.New(xerr.NotFound, "room background not found")
		}
		cmd.BackgroundID = background.BackgroundID
		cmd.BackgroundURL = background.ImageURL
		cmd.Background = roomBackgroundState(background)
	}
	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if current.OwnerUserID != cmd.ActorUserID() {
			return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied")
		}
		if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), "set_room_background"); err != nil {
			// 放在 mutate 回调内可让同一 command_id 的已提交重试先命中幂等，不会因 VIP 后续到期改写旧命令结果。
			return mutationResult{}, nil, err
		}
		if current.RoomExt == nil {
			current.RoomExt = make(map[string]string)
		}
		if (current.ActiveBackground != nil && current.ActiveBackground.BackgroundID == cmd.BackgroundID) ||
			(current.ActiveBackground == nil && current.RoomExt[roomExtBackgroundKey] == cmd.BackgroundURL) {
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}

		current.RoomExt[roomExtBackgroundKey] = cmd.BackgroundURL
		if cmd.Background.Media.URL != "" {
			backgroundSnapshot := cmd.Background
			current.ActiveBackground = &backgroundSnapshot
		} else {
			current.ActiveBackground = nil
		}
		current.Version++
		backgroundEvent, err := outbox.Build(current.RoomID, "RoomBackgroundChanged", current.Version, now, &roomeventsv1.RoomBackgroundChanged{
			ActorUserId:       cmd.ActorUserID(),
			BackgroundId:      cmd.BackgroundID,
			RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey],
			Media:             roomMediaEventSnapshot(cmd.Background.Media),
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:     backgroundEvent.EventID,
				RoomID:      current.RoomID,
				EventType:   "room_background_changed",
				ActorUserID: tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				RoomVersion: current.Version,
				Attributes: map[string]string{
					"background_id":       int64String(cmd.BackgroundID),
					"room_background_url": current.RoomExt[roomExtBackgroundKey],
				},
			},
		}, []outbox.Record{backgroundEvent}, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.SetRoomBackgroundResponse{
		Result:     commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:       result.snapshot,
		Background: roomBackgroundToProto(background),
	}, nil
}

func sameRoomBackgroundSaveRequest(existing RoomBackgroundImage, actorUserID int64, imageURL string, media state.RoomMedia) bool {
	if existing.CreatedByUserID != actorUserID || strings.TrimSpace(existing.ImageURL) != strings.TrimSpace(imageURL) {
		return false
	}
	if strings.TrimSpace(media.ObjectKey) != "" {
		// 新版媒体事实全部由 gateway 解析并由 room-service 校验；任一字段变化都代表 command_id 被复用。
		return existing.Media == media
	}
	legacyCandidate := media
	legacyCandidate.URL = ""
	// legacy_timed 只接受 image_url，其余媒体字段必须同时为空，不能借旧成功绕过新版专用上传校验。
	return existing.Media == (state.RoomMedia{}) && legacyCandidate == (state.RoomMedia{})
}

func roomBackgroundFromSetCommand(cmd command.SetRoomBackground) RoomBackgroundImage {
	backgroundID := cmd.Background.BackgroundID
	if backgroundID <= 0 {
		backgroundID = cmd.BackgroundID
	}
	roomID := strings.TrimSpace(cmd.Background.RoomID)
	if roomID == "" {
		roomID = cmd.RoomID()
	}
	createdByUserID := cmd.Background.CreatedByUserID
	if createdByUserID <= 0 {
		createdByUserID = cmd.ActorUserID()
	}
	// command log 固化第一次应用时的素材快照；旧记录没有 Background 时继续用 BackgroundURL 返回历史结果。
	return RoomBackgroundImage{
		AppCode: cmd.AppCode, BackgroundID: backgroundID, RoomID: roomID,
		ImageURL: strings.TrimSpace(cmd.BackgroundURL), CommandID: cmd.Background.CommandID, Media: cmd.Background.Media,
		CreatedByUserID: createdByUserID, CreatedAtMS: cmd.Background.CreatedAtMS, UpdatedAtMS: cmd.Background.UpdatedAtMS,
	}
}

func normalizeSaveRoomBackground(req *roomv1.SaveRoomBackgroundRequest) (string, int64, string, state.RoomMedia, error) {
	if req == nil || req.GetMeta() == nil {
		return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "request is required")
	}
	roomID := strings.TrimSpace(req.GetRoomId())
	if roomID == "" {
		roomID = strings.TrimSpace(req.GetMeta().GetRoomId())
	}
	if !roomid.ValidStringID(roomID) {
		return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "room_id is invalid")
	}
	actorUserID := req.GetMeta().GetActorUserId()
	if actorUserID <= 0 {
		return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "actor_user_id is required")
	}
	commandID := strings.TrimSpace(req.GetMeta().GetCommandId())
	media := roomMediaFromProto(req.GetMedia())
	requestImageURL := strings.TrimSpace(req.GetImageUrl())
	if media.URL != "" && requestImageURL != "" && media.URL != requestImageURL {
		return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "image_url does not match media.url")
	}
	if media.URL == "" {
		media.URL = requestImageURL
	}
	return roomID, actorUserID, commandID, media, nil
}

func normalizeListRoomBackgrounds(req *roomv1.ListRoomBackgroundsRequest) (string, int64, error) {
	if req == nil || req.GetMeta() == nil {
		return "", 0, xerr.New(xerr.InvalidArgument, "request is required")
	}
	roomID := strings.TrimSpace(req.GetRoomId())
	if !roomid.ValidStringID(roomID) {
		return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
	}
	viewerUserID := req.GetViewerUserId()
	if viewerUserID <= 0 {
		return "", 0, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
	}
	return roomID, viewerUserID, nil
}

func normalizeSetRoomBackground(req *roomv1.SetRoomBackgroundRequest) (string, int64, int64, error) {
	if req == nil || req.GetMeta() == nil {
		return "", 0, 0, xerr.New(xerr.InvalidArgument, "request is required")
	}
	roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
	if !roomid.ValidStringID(roomID) {
		return "", 0, 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
	}
	actorUserID := req.GetMeta().GetActorUserId()
	if actorUserID <= 0 {
		return "", 0, 0, xerr.New(xerr.InvalidArgument, "actor_user_id is required")
	}
	if strings.TrimSpace(req.GetMeta().GetCommandId()) == "" {
		return "", 0, 0, xerr.New(xerr.InvalidArgument, "command_id is required")
	}
	if req.GetBackgroundId() <= 0 {
		return "", 0, 0, xerr.New(xerr.InvalidArgument, "background_id is required")
	}
	return roomID, actorUserID, req.GetBackgroundId(), nil
}

func (s *Service) requireRoomOwnerMeta(ctx context.Context, roomID string, actorUserID int64) error {
	meta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
	if err != nil {
		return err
	}
	if !exists {
		return xerr.New(xerr.NotFound, "room not found")
	}
	if meta.OwnerUserID != actorUserID {
		return xerr.New(xerr.PermissionDenied, "permission denied")
	}
	if meta.Status != state.RoomStatusActive {
		return xerr.New(xerr.Conflict, "room is not active")
	}
	return nil
}
