package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"strings"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/giftlimits"
	"hyapp/pkg/roomid"
	"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"
)

// AuthorizeRoomMediaUpload 在 COS PutObject 之前执行房间 owner/presence、治理态和 VIP 权益门禁。
// 返回的 prefix 是 gateway 生成对象 key 的唯一可信目录，客户端不能自行指定 key。
func (s *Service) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.New(xerr.InvalidArgument, "request is required")
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
	actorUserID := req.GetMeta().GetActorUserId()
	purpose := strings.ToLower(strings.TrimSpace(req.GetPurpose()))
	uploadCommandID := strings.TrimSpace(req.GetMeta().GetCommandId())
	if actorUserID <= 0 || !giftlimits.ValidCommandID(uploadCommandID) ||
		(purpose != roomMediaPurposeBackground && purpose != roomMediaPurposeImage && purpose != roomMediaPurposeCover) ||
		(purpose != roomMediaPurposeCover && !roomid.ValidStringID(roomID)) || (purpose == roomMediaPurposeCover && roomID != "") {
		return nil, xerr.New(xerr.InvalidArgument, "room media upload request is invalid")
	}
	media := roomMediaFromProto(req.GetMedia())
	if err := validateRoomMediaProperties(purpose, media); err != nil {
		return nil, err
	}
	if media.URL != "" || media.ObjectKey != "" || media.Status != roomMediaStatusActive {
		return nil, xerr.New(xerr.InvalidArgument, "room media authorization requires pre-upload metadata")
	}
	guard, ok := s.repository.(RoomMediaUploadGuardStore)
	if !ok {
		return nil, xerr.New(xerr.Unavailable, "room media upload guard is unavailable")
	}
	payloadSHA256, err := roomMediaUploadPayloadSHA256(media)
	if err != nil {
		return nil, err
	}
	appCode := appcode.FromContext(ctx)
	registrationCommandID := uploadCommandID
	if purpose == roomMediaPurposeCover {
		// 客户端 command_id 只控制上传幂等；对外消费凭证由 owner service 派生，避免把任意字符串
		// 当作可绑定封面的授权记录主键。
		registrationCommandID = roomCoverUploadID(appCode, actorUserID, uploadCommandID)
	}
	registration := RoomMediaUploadRegistration{
		AppCode: appCode, RoomID: roomID, CommandID: registrationCommandID,
		ActorUserID: actorUserID, Purpose: purpose, MediaPayloadSHA256: payloadSHA256, Media: media,
	}
	registration.ExpectedObjectKey = roomMediaExpectedObjectKey(
		registration.AppCode, actorUserID, roomID, purpose, registration.CommandID, media,
	)
	var authorizedReplay *RoomMediaUploadRegistration
	if existing, exists, readErr := guard.GetRoomMediaUploadRegistration(ctx, roomID, registration.CommandID); readErr != nil {
		return nil, readErr
	} else if exists {
		if !sameRoomMediaUploadIdentity(existing, registration) {
			return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict")
		}
		switch existing.Status {
		case roomMediaStatusActive:
			// PutObject 已完成的同载荷重试只重放首次结果；后续权限变化不能把已成功请求改成失败。
			return roomMediaUploadAuthorizationResponse(existing, s.clock.Now().UnixMilli()), nil
		case "authorized":
			// authorized 只代表 owner 已占用 command_id，不能代表对象已经上传成功。重试 PutObject
			// 前必须重新检查当前房主/麦位/VIP，避免权限撤销后继续使用旧授权写 COS。
			authorizedReplay = &existing
		default:
			return nil, xerr.New(xerr.Internal, "room media upload registration status is invalid")
		}
	}
	if purpose == roomMediaPurposeCover {
		if consumed, exists, readErr := guard.GetRoomMediaUploadByObjectKey(ctx, registration.ExpectedObjectKey); readErr != nil {
			return nil, readErr
		} else if exists && consumed.Status == roomMediaStatusConsumed {
			if !sameConsumedRoomCoverUpload(consumed, registration) {
				return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict")
			}
			return nil, xerr.New(xerr.Conflict, "room cover upload is already consumed")
		}
	}
	var authorizedVersion int64
	if purpose != roomMediaPurposeCover {
		snapshot, err := s.currentSnapshot(ctx, roomID)
		if err != nil {
			return nil, err
		}
		if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive {
			return nil, xerr.New(xerr.RoomClosed, "room closed")
		}
		authorizedVersion = snapshot.GetVersion()
		switch purpose {
		case roomMediaPurposeBackground:
			if snapshot.GetOwnerUserId() != actorUserID {
				return nil, xerr.New(xerr.PermissionDenied, "permission denied")
			}
			if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, vipBenefitCustomRoomBackground, "upload_room_background"); err != nil {
				return nil, err
			}
		case roomMediaPurposeImage:
			if err := requireSnapshotSpeakPermission(snapshot, actorUserID, s.clock.Now().UnixMilli()); err != nil {
				return nil, err
			}
			if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, vipBenefitRoomImageMessage, "upload_room_image"); err != nil {
				return nil, err
			}
		}
	} else if media.Animated {
		// 静态封面属于基础能力；只有服务端实际解析出多帧 GIF/WebP 时才查询精确权益。
		// 解锁等级完全由 wallet 的 app benefit 配置决定，room-service 不判断 VIP 数字。
		if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, vipBenefitAnimatedRoomCover, "upload_animated_room_cover"); err != nil {
			return nil, err
		}
	}
	nowMS := s.clock.Now().UnixMilli()
	if authorizedReplay != nil {
		return roomMediaUploadAuthorizationResponse(*authorizedReplay, nowMS), nil
	}
	registration.AuthorizedVersion = authorizedVersion
	registration.CreatedAtMS, registration.UpdatedAtMS = nowMS, nowMS
	registered, err := guard.RegisterRoomMediaUpload(ctx, registration)
	if errors.Is(err, ErrRoomMediaUploadCommandConflict) {
		return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict")
	}
	if err != nil {
		return nil, err
	}
	return roomMediaUploadAuthorizationResponse(registered, nowMS), nil
}

// CompleteRoomMediaUpload 只接受 gateway 已成功 PutObject 后回传的 URL/key。
// owner 以首次注册的完整解析事实为准推进 active，Save/Send 随后只认 active registration。
func (s *Service) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.New(xerr.InvalidArgument, "request is required")
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
	commandID := strings.TrimSpace(req.GetMeta().GetCommandId())
	actorUserID := req.GetMeta().GetActorUserId()
	purpose := strings.ToLower(strings.TrimSpace(req.GetPurpose()))
	if actorUserID <= 0 || !giftlimits.ValidCommandID(commandID) ||
		(purpose != roomMediaPurposeBackground && purpose != roomMediaPurposeImage && purpose != roomMediaPurposeCover) ||
		(purpose != roomMediaPurposeCover && !roomid.ValidStringID(roomID)) || (purpose == roomMediaPurposeCover && roomID != "") {
		return nil, xerr.New(xerr.InvalidArgument, "room media completion request is invalid")
	}
	media := roomMediaFromProto(req.GetMedia())
	if err := validateRoomMedia(appcode.FromContext(ctx), roomID, actorUserID, purpose, media); err != nil {
		return nil, err
	}
	guard, ok := s.repository.(RoomMediaUploadGuardStore)
	if !ok {
		return nil, xerr.New(xerr.Unavailable, "room media upload guard is unavailable")
	}
	existing, exists, err := guard.GetRoomMediaUploadRegistration(ctx, roomID, commandID)
	if err != nil {
		return nil, err
	}
	if !exists {
		return nil, xerr.New(xerr.NotFound, "room media upload authorization not found")
	}
	payloadSHA256, err := roomMediaUploadPayloadSHA256(media)
	if err != nil {
		return nil, err
	}
	candidate := RoomMediaUploadRegistration{
		AppCode: appcode.FromContext(ctx), RoomID: roomID, CommandID: commandID, ActorUserID: actorUserID,
		Purpose: purpose, MediaPayloadSHA256: payloadSHA256, ExpectedObjectKey: media.ObjectKey, ObjectURL: media.URL, Media: media,
	}
	if !sameRoomMediaUploadIdentity(existing, candidate) || !sameRoomMediaProperties(existing.Media, media) {
		return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict")
	}
	if existing.Status == roomMediaStatusActive {
		if strings.TrimSpace(existing.ObjectURL) != strings.TrimSpace(media.URL) {
			return nil, xerr.New(xerr.IdempotencyConflict, "room media upload completion conflict")
		}
		return &roomv1.CompleteRoomMediaUploadResponse{
			Active: true, Media: roomMediaToProto(existing.Media), ServerTimeMs: s.clock.Now().UnixMilli(),
			CoverUploadId: roomMediaCoverUploadID(existing),
		}, nil
	}
	existing.ObjectURL, existing.Status, existing.Media = media.URL, roomMediaStatusActive, media
	existing.UpdatedAtMS = s.clock.Now().UnixMilli()
	completed, err := guard.CompleteRoomMediaUpload(ctx, existing)
	if errors.Is(err, ErrRoomMediaUploadCommandConflict) {
		return nil, xerr.New(xerr.IdempotencyConflict, "room media upload completion conflict")
	}
	if err != nil {
		return nil, err
	}
	return &roomv1.CompleteRoomMediaUploadResponse{
		Active: true, Media: roomMediaToProto(completed.Media), ServerTimeMs: s.clock.Now().UnixMilli(),
		CoverUploadId: roomMediaCoverUploadID(completed),
	}, nil
}

func roomMediaUploadAuthorizationResponse(registration RoomMediaUploadRegistration, serverTimeMS int64) *roomv1.AuthorizeRoomMediaUploadResponse {
	return &roomv1.AuthorizeRoomMediaUploadResponse{
		Allowed: true, ObjectKeyPrefix: roomMediaObjectPrefix(registration.AppCode, registration.ActorUserID, registration.RoomID, registration.Purpose),
		ObjectKey: registration.ExpectedObjectKey, RoomVersion: registration.AuthorizedVersion, ServerTimeMs: serverTimeMS,
		CoverUploadId: roomMediaCoverUploadID(registration),
	}
}

func roomMediaCoverUploadID(registration RoomMediaUploadRegistration) string {
	if registration.Purpose == roomMediaPurposeCover {
		return strings.TrimSpace(registration.CommandID)
	}
	return ""
}

func sameRoomMediaUploadIdentity(left RoomMediaUploadRegistration, right RoomMediaUploadRegistration) bool {
	return appcode.Normalize(left.AppCode) == appcode.Normalize(right.AppCode) &&
		strings.TrimSpace(left.RoomID) == strings.TrimSpace(right.RoomID) &&
		strings.TrimSpace(left.CommandID) == strings.TrimSpace(right.CommandID) &&
		left.ActorUserID == right.ActorUserID && strings.TrimSpace(left.Purpose) == strings.TrimSpace(right.Purpose) &&
		strings.EqualFold(strings.TrimSpace(left.MediaPayloadSHA256), strings.TrimSpace(right.MediaPayloadSHA256)) &&
		strings.TrimLeft(strings.TrimSpace(left.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(right.ExpectedObjectKey), "/")
}

func sameConsumedRoomCoverUpload(consumed RoomMediaUploadRegistration, candidate RoomMediaUploadRegistration) bool {
	return appcode.Normalize(consumed.AppCode) == appcode.Normalize(candidate.AppCode) &&
		strings.TrimSpace(consumed.CommandID) == strings.TrimSpace(candidate.CommandID) &&
		consumed.ActorUserID == candidate.ActorUserID && consumed.Purpose == roomMediaPurposeCover && candidate.Purpose == roomMediaPurposeCover &&
		strings.EqualFold(strings.TrimSpace(consumed.MediaPayloadSHA256), strings.TrimSpace(candidate.MediaPayloadSHA256)) &&
		strings.TrimLeft(strings.TrimSpace(consumed.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(candidate.ExpectedObjectKey), "/")
}

func (s *Service) requireActiveRoomMediaUpload(ctx context.Context, roomID string, actorUserID int64, purpose string, commandID string, media state.RoomMedia) error {
	guard, ok := s.repository.(RoomMediaUploadGuardStore)
	if !ok {
		return xerr.New(xerr.Unavailable, "room media upload guard is unavailable")
	}
	registration, exists, err := guard.GetActiveRoomMediaUploadByObjectKey(ctx, media.ObjectKey)
	if err != nil {
		return err
	}
	if !exists {
		return xerr.New(xerr.NotFound, "active room media upload not found")
	}
	payloadSHA256, err := roomMediaUploadPayloadSHA256(media)
	if err != nil {
		return err
	}
	candidate := RoomMediaUploadRegistration{
		AppCode: appcode.FromContext(ctx), RoomID: roomID, CommandID: strings.TrimSpace(commandID), ActorUserID: actorUserID,
		Purpose: purpose, MediaPayloadSHA256: payloadSHA256, ExpectedObjectKey: media.ObjectKey, ObjectURL: media.URL, Media: media,
	}
	if !sameRoomMediaUploadIdentity(registration, candidate) ||
		strings.TrimSpace(registration.ObjectURL) != strings.TrimSpace(media.URL) || !sameRoomMediaProperties(registration.Media, media) {
		return xerr.New(xerr.IdempotencyConflict, "room media does not match completed upload")
	}
	return nil
}

// SendRoomImage 把服务端认可的图片消息写入 Room Cell command log + durable outbox。
// outbox 的 at-least-once 重试使用稳定 message_id，Flutter 不能以到达次数作为消息条数。
func (s *Service) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) {
	if req == nil || req.GetMeta() == nil {
		return nil, xerr.New(xerr.InvalidArgument, "request is required")
	}
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.SendRoomImage{
		Base: baseFromMeta(req.GetMeta()), MessageID: roomImageMessageID(req.GetMeta()), Image: roomMediaFromProto(req.GetImage()),
	}
	if !roomid.ValidStringID(cmd.RoomID()) || cmd.ActorUserID() <= 0 || strings.TrimSpace(cmd.ID()) == "" {
		return nil, xerr.New(xerr.InvalidArgument, "room image message meta is invalid")
	}
	// 与装扮相同，已提交 command_id 必须在当前 VIP 到期/禁言之前直接重放。
	if _, record, seen, err := s.commandRecordForIdempotency(ctx, cmd); err != nil {
		return nil, err
	} else if seen {
		stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload)
		if deserializeErr != nil {
			return nil, deserializeErr
		}
		storedCommand, ok := stored.(*command.SendRoomImage)
		if !ok {
			return nil, xerr.New(xerr.Internal, "stored room image command is invalid")
		}
		// 返回第一次提交固化的 message_id 和媒体事实，不能用当前重试请求拼出看似成功的另一条消息。
		cmd = *storedCommand
	} else {
		if err := validateRoomMedia(appcode.FromContext(ctx), cmd.RoomID(), cmd.ActorUserID(), roomMediaPurposeImage, cmd.Image); err != nil {
			return nil, err
		}
		if err := s.requireActiveRoomMediaUpload(ctx, cmd.RoomID(), cmd.ActorUserID(), roomMediaPurposeImage, cmd.ID(), cmd.Image); err != nil {
			return nil, err
		}
		if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), vipBenefitRoomImageMessage, "send_room_image"); err != nil {
			return nil, err
		}
	}

	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 err := requireStateSpeakPermission(current, cmd.ActorUserID(), now.UnixMilli()); err != nil {
			return mutationResult{}, nil, err
		}
		// 图片消息不成为房间核心状态字段，但版本推进把 command log、恢复谱系和 outbox 放进同一原子提交。
		current.Version++
		event, err := outbox.Build(current.RoomID, "RoomImageMessageSent", current.Version, now, &roomeventsv1.RoomImageMessageSent{
			MessageId: cmd.MessageID, CommandId: cmd.ID(), SenderUserId: cmd.ActorUserID(), Image: roomMediaEventSnapshot(cmd.Image),
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli())}, []outbox.Record{event}, nil
	})
	if err != nil {
		return nil, err
	}
	now := s.clock.Now()
	return &roomv1.SendRoomImageResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), now), Accepted: true,
		MessageId: cmd.MessageID, RoomId: cmd.RoomID(), SenderUserId: cmd.ActorUserID(), ServerTimeMs: now.UnixMilli(),
		Image: roomMediaToProto(cmd.Image),
	}, nil
}

func requireSnapshotSpeakPermission(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) error {
	if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive {
		return xerr.New(xerr.RoomClosed, "room closed")
	}
	switch {
	case snapshotUserBanned(snapshot, userID, nowMS):
		return xerr.New(xerr.PermissionDenied, "user is banned")
	case containsID(snapshot.GetMuteUserIds(), userID):
		return xerr.New(xerr.PermissionDenied, "user is muted")
	case !snapshot.GetChatEnabled():
		return xerr.New(xerr.Conflict, "room chat is disabled")
	case findProtoUser(snapshot, userID) == nil:
		return xerr.New(xerr.PermissionDenied, "user is not in room")
	default:
		return nil
	}
}

func requireStateSpeakPermission(current *state.RoomState, userID int64, nowMS int64) error {
	if moderation, banned := current.BanUsers[userID]; banned && moderation.ActiveAt(nowMS) {
		return xerr.New(xerr.PermissionDenied, "user is banned")
	}
	if current.MuteUsers[userID] {
		return xerr.New(xerr.PermissionDenied, "user is muted")
	}
	if !current.ChatEnabled {
		return xerr.New(xerr.Conflict, "room chat is disabled")
	}
	if current.OnlineUsers[userID] == nil {
		return xerr.New(xerr.PermissionDenied, "user is not in room")
	}
	return nil
}

func roomImageMessageID(meta *roomv1.RequestMeta) string {
	if meta == nil {
		return ""
	}
	sum := sha256.Sum256([]byte(strings.Join([]string{
		appcode.Normalize(meta.GetAppCode()), strings.TrimSpace(meta.GetRoomId()), strings.TrimSpace(meta.GetCommandId()),
	}, "\x00")))
	return "room_msg_" + hex.EncodeToString(sum[:16])
}
