package service

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/url"
	"path"
	"strings"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/giftlimits"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/state"
)

const (
	roomMediaPurposeBackground = "room_background"
	roomMediaPurposeImage      = "room_image"
	roomMediaPurposeCover      = "room_cover"
	roomMediaStatusActive      = "active"
	roomMediaStatusConsumed    = "consumed"

	roomBackgroundMaxBytes  = int64(12 << 20)
	roomBackgroundMaxWidth  = int32(2160)
	roomBackgroundMaxHeight = int32(3840)
	roomBackgroundMaxPixels = int64(8_294_400)
	roomImageMaxBytes       = int64(8 << 20)
	roomImageMaxDimension   = int32(4096)
	roomImageMaxPixels      = int64(16_777_216)
	roomCoverMaxBytes       = int64(10 << 20)
	roomCoverMaxDimension   = int32(4096)
	roomCoverMaxPixels      = int64(16_777_216)
	roomMediaMaxFrames      = int32(120)
	roomMediaMaxDurationMS  = int64(15_000)
)

var roomMediaMIMEByFormat = map[string]string{
	"jpeg": "image/jpeg",
	"png":  "image/png",
	"gif":  "image/gif",
	"webp": "image/webp",
}

// validateRoomMedia 校验专用上传入口产生的完整媒体事实。room-service 不下载 URL 重新嗅探，
// 但会约束 COS key 归属、https、格式/MIME、尺寸和哈希，防止客户端绕过 gateway 直调 gRPC。
func validateRoomMedia(ctxAppCode string, roomID string, actorUserID int64, purpose string, media state.RoomMedia) error {
	media.URL = strings.TrimSpace(media.URL)
	media.ObjectKey = strings.TrimLeft(strings.TrimSpace(media.ObjectKey), "/")
	media.ContentType = strings.ToLower(strings.TrimSpace(media.ContentType))
	media.Format = strings.ToLower(strings.TrimSpace(media.Format))
	media.SHA256 = strings.ToLower(strings.TrimSpace(media.SHA256))
	media.Status = strings.ToLower(strings.TrimSpace(media.Status))
	if media.URL == "" || media.ObjectKey == "" || media.Status != roomMediaStatusActive {
		return xerr.New(xerr.InvalidArgument, "room media is incomplete")
	}
	if len(media.URL) > 1024 || len(media.ObjectKey) > 512 || len(media.PosterURL) > 1024 || len(media.ThumbnailURL) > 1024 {
		return xerr.New(xerr.InvalidArgument, "room media url or object_key is too long")
	}
	parsed, err := url.Parse(media.URL)
	if err != nil || parsed.Scheme != "https" || strings.TrimSpace(parsed.Host) == "" {
		return xerr.New(xerr.InvalidArgument, "room media url must be https")
	}
	if expected := roomMediaObjectPrefix(ctxAppCode, actorUserID, roomID, purpose); !strings.HasPrefix(media.ObjectKey, expected) {
		return xerr.New(xerr.PermissionDenied, "room media object does not belong to actor and room")
	}
	if path.Clean(media.ObjectKey) != media.ObjectKey ||
		!(strings.TrimLeft(parsed.Path, "/") == media.ObjectKey || strings.HasSuffix(strings.TrimRight(parsed.Path, "/"), "/"+media.ObjectKey)) {
		// gateway 还会按 COS access_url 校验 host/base path；owner service 至少把 URL path 与同一 object_key 绑定，
		// 防止客户端拿一条合法 key 配合另一张外部图片绕过素材归属。
		return xerr.New(xerr.PermissionDenied, "room media url does not match object_key")
	}
	return validateRoomMediaProperties(purpose, media)
}

// validateRoomMediaProperties 供上传授权阶段在对象尚未写入 COS、因而还没有 URL/key 时复核解析结果。
func validateRoomMediaProperties(purpose string, media state.RoomMedia) error {
	media.ContentType = strings.ToLower(strings.TrimSpace(media.ContentType))
	media.Format = strings.ToLower(strings.TrimSpace(media.Format))
	media.SHA256 = strings.ToLower(strings.TrimSpace(media.SHA256))
	if strings.TrimSpace(media.PosterURL) != "" || strings.TrimSpace(media.ThumbnailURL) != "" {
		// 当前上传链路没有生成或校验派生图，客户端不能借 poster/thumbnail 字段挂入任意外部 URL。
		// 等后续服务端真正生成首帧和缩略图后，再由 owner service 填充这两个只读事实字段。
		return xerr.New(xerr.InvalidArgument, "poster_url and thumbnail_url are not supported")
	}
	expectedMIME, supported := roomMediaMIMEByFormat[media.Format]
	if !supported || expectedMIME != media.ContentType {
		return xerr.New(xerr.InvalidArgument, "room media format does not match content_type")
	}
	if media.Width <= 0 || media.Height <= 0 || media.FrameCount <= 0 || media.SizeBytes <= 0 {
		return xerr.New(xerr.InvalidArgument, "room media dimensions are invalid")
	}
	decodedHash, err := hex.DecodeString(media.SHA256)
	if err != nil || len(decodedHash) != sha256.Size {
		return xerr.New(xerr.InvalidArgument, "room media sha256 is invalid")
	}
	if media.Animated {
		if media.Format != "gif" && media.Format != "webp" {
			return xerr.New(xerr.InvalidArgument, "animated room media must be gif or webp")
		}
		if media.FrameCount < 2 || media.FrameCount > roomMediaMaxFrames || media.DurationMS <= 0 || media.DurationMS > roomMediaMaxDurationMS {
			return xerr.New(xerr.InvalidArgument, "animated room media exceeds frame or duration limit")
		}
	} else if media.FrameCount != 1 || media.DurationMS != 0 {
		return xerr.New(xerr.InvalidArgument, "static room media metadata is invalid")
	}
	pixels := int64(media.Width) * int64(media.Height)
	switch purpose {
	case roomMediaPurposeBackground:
		if media.SizeBytes > roomBackgroundMaxBytes || media.Width > roomBackgroundMaxWidth || media.Height > roomBackgroundMaxHeight || pixels > roomBackgroundMaxPixels {
			return xerr.New(xerr.InvalidArgument, "room background exceeds media limits")
		}
	case roomMediaPurposeImage:
		if media.SizeBytes > roomImageMaxBytes || media.Width > roomImageMaxDimension || media.Height > roomImageMaxDimension || pixels > roomImageMaxPixels {
			return xerr.New(xerr.InvalidArgument, "room image exceeds media limits")
		}
	case roomMediaPurposeCover:
		if media.SizeBytes > roomCoverMaxBytes || media.Width > roomCoverMaxDimension || media.Height > roomCoverMaxDimension || pixels > roomCoverMaxPixels {
			return xerr.New(xerr.InvalidArgument, "room cover exceeds media limits")
		}
	default:
		return xerr.New(xerr.InvalidArgument, "room media purpose is invalid")
	}
	return nil
}

// roomMediaObjectPrefix 是上传与提交共同使用的租户/用户/房间归属边界。
// path.Join 同时消除可疑的 ../ 片段，gateway 生成对象 key 时必须使用同一规则。
func roomMediaObjectPrefix(rawAppCode string, actorUserID int64, roomID string, purpose string) string {
	directory := "images"
	if purpose == roomMediaPurposeBackground {
		directory = "backgrounds"
	}
	if purpose == roomMediaPurposeCover {
		// 封面上传发生在房间创建之前，因此对象归属只能绑定租户和上传用户；真正绑定 room_id
		// 由创建/资料命令消费 cover_upload_id 时在 MySQL 事务内完成。
		return path.Join("room-media", appcode.Normalize(rawAppCode), "covers", fmt.Sprintf("%d", actorUserID)) + "/"
	}
	return path.Join("room-media", appcode.Normalize(rawAppCode), directory, fmt.Sprintf("%d", actorUserID), strings.TrimSpace(roomID)) + "/"
}

// roomCoverUploadID 把客户端上传 command_id 收敛为不泄漏原值的稳定凭证。相同用户、租户和上传动作
// 永远得到同一 ID，gateway 重试不会创建第二条授权记录；跨用户或跨 App 则无法碰撞或转用。
func roomCoverUploadID(rawAppCode string, actorUserID int64, commandID string) string {
	sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%d\x00%s", appcode.Normalize(rawAppCode), actorUserID, strings.TrimSpace(commandID))))
	return "cover_" + hex.EncodeToString(sum[:16])
}

// resolveRoomCoverForMutation 把一次性上传记录解析成可信 URL，并返回必须随房间命令提交的消费条件。
// active 记录尚未使用；consumed 记录仅允许同房间的原命令重放，后续 SaveMutation 会用 active 条件阻止新命令复用。
func (s *Service) resolveRoomCoverForMutation(ctx context.Context, requestID string, roomID string, actorUserID int64, coverUploadID string) (string, *RoomCoverUploadConsumption, error) {
	coverUploadID = strings.TrimSpace(coverUploadID)
	if !strings.HasPrefix(coverUploadID, "cover_") || !giftlimits.ValidCommandID(coverUploadID) {
		return "", nil, xerr.New(xerr.InvalidArgument, "cover_upload_id is invalid")
	}
	guard, ok := s.repository.(RoomMediaUploadGuardStore)
	if !ok {
		return "", nil, xerr.New(xerr.Unavailable, "room media upload guard is unavailable")
	}
	registration, exists, err := guard.GetRoomMediaUploadRegistration(ctx, "", coverUploadID)
	if err != nil {
		return "", nil, err
	}
	if !exists {
		// 成功提交后主键中的 room_id 已被改成目标房间。读取该行只用于相同 command_id 的幂等重放；
		// 新命令即使拿到同一 URL，也会在事务消费条件上因 status!=active 失败并整体回滚。
		registration, exists, err = guard.GetRoomMediaUploadRegistration(ctx, roomID, coverUploadID)
		if err != nil {
			return "", nil, err
		}
	}
	if !exists {
		return "", nil, xerr.New(xerr.NotFound, "room cover upload not found")
	}
	if registration.ActorUserID != actorUserID || registration.Purpose != roomMediaPurposeCover ||
		(registration.Status != roomMediaStatusActive && !(registration.Status == roomMediaStatusConsumed && registration.RoomID == roomID)) {
		return "", nil, xerr.New(xerr.PermissionDenied, "room cover upload does not belong to actor and room")
	}
	if err := validateRoomMedia(appcode.FromContext(ctx), "", actorUserID, roomMediaPurposeCover, registration.Media); err != nil {
		return "", nil, err
	}
	if registration.Media.Animated && registration.Status == roomMediaStatusActive {
		// 上传与绑定都检查精确 benefit，避免 VIP 到期后继续把尚未消费的动态封面写进房间状态。
		if err := s.requireVIPBenefit(ctx, requestID, actorUserID, vipBenefitAnimatedRoomCover, "set_animated_room_cover"); err != nil {
			return "", nil, err
		}
	}
	return strings.TrimSpace(registration.ObjectURL), &RoomCoverUploadConsumption{
		CoverUploadID: coverUploadID,
		ActorUserID:   actorUserID,
	}, nil
}

// resolveSubmittedRoomCover 统一新旧协议边界。权益制 App 的普通客户端只能提交 cover_upload_id；
// legacy_timed 继续接受历史 URL，机器人内部编排也保留已受信任的资源 URL，避免跨 App 发布互相阻塞。
func (s *Service) resolveSubmittedRoomCover(ctx context.Context, requestID string, roomID string, actorUserID int64, coverUploadID string, legacyURL string, robotRoom bool) (string, *RoomCoverUploadConsumption, error) {
	coverUploadID = strings.TrimSpace(coverUploadID)
	legacyURL = strings.TrimSpace(legacyURL)
	if coverUploadID != "" {
		if legacyURL != "" {
			return "", nil, xerr.New(xerr.InvalidArgument, "cover_upload_id and room_avatar are mutually exclusive")
		}
		return s.resolveRoomCoverForMutation(ctx, requestID, roomID, actorUserID, coverUploadID)
	}
	if legacyURL == "" {
		return "", nil, xerr.New(xerr.InvalidArgument, "cover_upload_id is required")
	}
	if robotRoom || legacyURL == defaultRoomAvatar {
		return legacyURL, nil, nil
	}
	access, err := s.loadEffectiveVIPAccess(ctx, requestID, actorUserID)
	if err != nil {
		return "", nil, err
	}
	if access.programType == tieredPrivilegeVIPProgram {
		// Fami 当前由 wallet 配置为权益制；这里不识别 app_code，也不读取 VIP 等级数字。
		return "", nil, xerr.New(xerr.InvalidArgument, "cover_upload_id is required")
	}
	return legacyURL, nil, nil
}

func roomMediaExpectedObjectKey(rawAppCode string, actorUserID int64, roomID string, purpose string, commandID string, media state.RoomMedia) string {
	commandHash := sha256.Sum256([]byte(strings.TrimSpace(commandID)))
	extension := media.Format
	if extension == "jpeg" {
		extension = "jpg"
	}
	return path.Join(
		roomMediaObjectPrefix(rawAppCode, actorUserID, roomID, purpose),
		hex.EncodeToString(commandHash[:8])+"-"+strings.ToLower(strings.TrimSpace(media.SHA256))[:16]+"."+extension,
	)
}

func roomMediaUploadPayloadSHA256(media state.RoomMedia) (string, error) {
	// URL/object_key/status 是上传生命周期字段，不属于首次解析的文件事实；其余字段使用结构体固定顺序编码。
	payload, err := json.Marshal(struct {
		ContentType  string `json:"content_type"`
		Format       string `json:"format"`
		SizeBytes    int64  `json:"size_bytes"`
		Width        int32  `json:"width"`
		Height       int32  `json:"height"`
		Animated     bool   `json:"animated"`
		FrameCount   int32  `json:"frame_count"`
		DurationMS   int64  `json:"duration_ms"`
		PosterURL    string `json:"poster_url"`
		ThumbnailURL string `json:"thumbnail_url"`
		SHA256       string `json:"sha256"`
	}{
		ContentType: media.ContentType, Format: media.Format, SizeBytes: media.SizeBytes,
		Width: media.Width, Height: media.Height, Animated: media.Animated, FrameCount: media.FrameCount,
		DurationMS: media.DurationMS, PosterURL: media.PosterURL, ThumbnailURL: media.ThumbnailURL, SHA256: media.SHA256,
	})
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:]), nil
}

func sameRoomMediaProperties(left state.RoomMedia, right state.RoomMedia) bool {
	left.URL, left.ObjectKey, left.Status = "", "", ""
	right.URL, right.ObjectKey, right.Status = "", "", ""
	return left == right
}

func roomMediaFromProto(media *roomv1.RoomMedia) state.RoomMedia {
	if media == nil {
		return state.RoomMedia{}
	}
	return state.RoomMedia{
		URL:          strings.TrimSpace(media.GetUrl()),
		ObjectKey:    strings.TrimLeft(strings.TrimSpace(media.GetObjectKey()), "/"),
		ContentType:  strings.ToLower(strings.TrimSpace(media.GetContentType())),
		Format:       strings.ToLower(strings.TrimSpace(media.GetFormat())),
		SizeBytes:    media.GetSizeBytes(),
		Width:        media.GetWidth(),
		Height:       media.GetHeight(),
		Animated:     media.GetAnimated(),
		FrameCount:   media.GetFrameCount(),
		DurationMS:   media.GetDurationMs(),
		PosterURL:    strings.TrimSpace(media.GetPosterUrl()),
		ThumbnailURL: strings.TrimSpace(media.GetThumbnailUrl()),
		SHA256:       strings.ToLower(strings.TrimSpace(media.GetSha256())),
		Status:       strings.ToLower(strings.TrimSpace(media.GetStatus())),
	}
}

func roomMediaToProto(media state.RoomMedia) *roomv1.RoomMedia {
	if strings.TrimSpace(media.URL) == "" && strings.TrimSpace(media.ObjectKey) == "" {
		return nil
	}
	return &roomv1.RoomMedia{
		Url: media.URL, ObjectKey: media.ObjectKey, ContentType: media.ContentType, Format: media.Format,
		SizeBytes: media.SizeBytes, Width: media.Width, Height: media.Height, Animated: media.Animated,
		FrameCount: media.FrameCount, DurationMs: media.DurationMS, PosterUrl: media.PosterURL,
		ThumbnailUrl: media.ThumbnailURL, Sha256: media.SHA256, Status: media.Status,
	}
}

func roomMediaEventSnapshot(media state.RoomMedia) *roomeventsv1.RoomMediaSnapshot {
	return &roomeventsv1.RoomMediaSnapshot{
		Url: media.URL, ObjectKey: media.ObjectKey, ContentType: media.ContentType, Format: media.Format,
		SizeBytes: media.SizeBytes, Width: media.Width, Height: media.Height, Animated: media.Animated,
		FrameCount: media.FrameCount, DurationMs: media.DurationMS, PosterUrl: media.PosterURL,
		ThumbnailUrl: media.ThumbnailURL, Sha256: media.SHA256,
	}
}

func roomBackgroundState(background RoomBackgroundImage) state.RoomBackground {
	return state.RoomBackground{
		BackgroundID: background.BackgroundID, RoomID: background.RoomID, CreatedByUserID: background.CreatedByUserID,
		CreatedAtMS: background.CreatedAtMS, UpdatedAtMS: background.UpdatedAtMS, CommandID: background.CommandID,
		Media: background.Media,
	}
}

func roomBackgroundToProto(background RoomBackgroundImage) *roomv1.RoomBackgroundImage {
	return &roomv1.RoomBackgroundImage{
		BackgroundId: background.BackgroundID, RoomId: background.RoomID, ImageUrl: background.ImageURL,
		CreatedByUserId: background.CreatedByUserID, CreatedAtMs: background.CreatedAtMS, UpdatedAtMs: background.UpdatedAtMS,
		Media: roomMediaToProto(background.Media), CommandId: background.CommandID,
	}
}

func roomBackgroundsToProto(backgrounds []RoomBackgroundImage) []*roomv1.RoomBackgroundImage {
	items := make([]*roomv1.RoomBackgroundImage, 0, len(backgrounds))
	for _, background := range backgrounds {
		items = append(items, roomBackgroundToProto(background))
	}
	return items
}

func legacyRoomBackgroundCommandID(rawAppCode string, roomID string, actorUserID int64, imageURL string) string {
	// legacy 请求没有 command_id；用租户、房间、房主和 URL 生成稳定短键，重试不会重复建素材，
	// 同时不把 URL 明文放进 VARCHAR(128) 或日志索引。
	sum := sha256.Sum256([]byte(strings.Join([]string{
		appcode.Normalize(rawAppCode), strings.TrimSpace(roomID), fmt.Sprintf("%d", actorUserID), strings.TrimSpace(imageURL),
	}, "\x00")))
	return "legacy_bg_" + hex.EncodeToString(sum[:16])
}
