package roomapi

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"image/jpeg"
	"image/png"
	"io"
	"net/http"
	"strings"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/giftlimits"
	"hyapp/pkg/roomid"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

const (
	roomBackgroundUploadMaxBytes = int64(12 << 20)
	roomImageUploadMaxBytes      = int64(8 << 20)
	roomCoverUploadMaxBytes      = int64(10 << 20)
	roomUploadEnvelopeOverhead   = int64(1 << 20)
	roomUploadMaxFrames          = int32(120)
	roomUploadMaxDurationMS      = int64(15_000)
)

type roomMediaData struct {
	URL          string `json:"url"`
	ObjectKey    string `json:"object_key"`
	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,omitempty"`
	ThumbnailURL string `json:"thumbnail_url,omitempty"`
	SHA256       string `json:"sha256"`
	Status       string `json:"status"`
}

type roomMediaUploadData struct {
	Purpose      string        `json:"purpose"`
	RoomID       string        `json:"room_id"`
	CommandID    string        `json:"command_id"`
	RoomVersion  int64         `json:"room_version"`
	Media        roomMediaData `json:"media"`
	ServerTimeMS int64         `json:"server_time_ms"`
}

// roomCoverUploadData 只暴露后续房间命令需要的凭证与服务端解析事实。
// URL 只用于客户端预览，创建/更新接口拒绝用它替代 cover_upload_id。
type roomCoverUploadData struct {
	CoverUploadID string `json:"cover_upload_id"`
	URL           string `json:"url"`
	SHA256        string `json:"sha256"`
	FrameCount    int32  `json:"frame_count"`
	Animated      bool   `json:"animated"`
	ContentType   string `json:"content_type"`
	Format        string `json:"format"`
	SizeBytes     int64  `json:"size_bytes"`
	Width         int32  `json:"width"`
	Height        int32  `json:"height"`
	DurationMS    int64  `json:"duration_ms"`
	ServerTimeMS  int64  `json:"server_time_ms"`
}

func (data roomMediaData) proto() *roomv1.RoomMedia {
	return &roomv1.RoomMedia{
		Url: strings.TrimSpace(data.URL), ObjectKey: strings.TrimLeft(strings.TrimSpace(data.ObjectKey), "/"),
		ContentType: strings.ToLower(strings.TrimSpace(data.ContentType)), Format: strings.ToLower(strings.TrimSpace(data.Format)),
		SizeBytes: data.SizeBytes, Width: data.Width, Height: data.Height, Animated: data.Animated,
		FrameCount: data.FrameCount, DurationMs: data.DurationMS, PosterUrl: strings.TrimSpace(data.PosterURL),
		ThumbnailUrl: strings.TrimSpace(data.ThumbnailURL), Sha256: strings.ToLower(strings.TrimSpace(data.SHA256)),
		Status: strings.ToLower(strings.TrimSpace(data.Status)),
	}
}

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

func (h *Handler) uploadRoomBackground(writer http.ResponseWriter, request *http.Request) {
	h.uploadRoomMedia(writer, request, "room_background", roomBackgroundUploadMaxBytes)
}

func (h *Handler) uploadRoomImage(writer http.ResponseWriter, request *http.Request) {
	h.uploadRoomMedia(writer, request, "room_image", roomImageUploadMaxBytes)
}

func (h *Handler) uploadRoomCover(writer http.ResponseWriter, request *http.Request) {
	h.uploadRoomMedia(writer, request, "room_cover", roomCoverUploadMaxBytes)
}

func (h *Handler) uploadRoomMedia(writer http.ResponseWriter, request *http.Request, purpose string, maxBytes int64) {
	roomID := strings.TrimSpace(request.PathValue("room_id"))
	if purpose != "room_cover" && !roomid.ValidStringID(roomID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "room_id is invalid")
		return
	}
	if h.objectUploader == nil || h.objectURLVerifier == nil || h.roomMediaGuard == nil {
		httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room media upload is unavailable")
		return
	}
	request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+roomUploadEnvelopeOverhead)
	if err := request.ParseMultipartForm(maxBytes + roomUploadEnvelopeOverhead); err != nil {
		httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit")
		return
	}
	if request.MultipartForm != nil {
		defer request.MultipartForm.RemoveAll()
	}
	commandID := strings.TrimSpace(request.FormValue("command_id"))
	if !giftlimits.ValidCommandID(commandID) {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "command_id is invalid")
		return
	}
	file, header, err := request.FormFile("file")
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is required")
		return
	}
	defer file.Close()
	content, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
	if err != nil || int64(len(content)) == 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is invalid")
		return
	}
	if int64(len(content)) > maxBytes {
		httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit")
		return
	}
	if header.Size >= 0 && header.Size != int64(len(content)) {
		// multipart 声明大小与实际读取字节不一致通常表示截断或拼接；后续 SHA/COS ContentLength
		// 都必须以同一份原始字节为准，因此在媒体解析前直接拒绝。
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file size does not match multipart metadata")
		return
	}
	// 相册缓存文件名和 multipart Content-Type 不可信；格式、后缀和对象 Content-Type
	// 全部从原始字节解析，确保 WebP/GIF 即使带着 JPG 元数据也能按真实类型保存。
	media, _, err := inspectRoomImage(content)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE", err.Error())
		return
	}
	if err := validateRoomUploadDimensions(purpose, media); err != nil {
		httpkit.WriteError(writer, request, http.StatusBadRequest, "INVALID_MEDIA_DIMENSIONS", err.Error())
		return
	}
	authorization, err := h.roomMediaGuard.AuthorizeRoomMediaUpload(request.Context(), &roomv1.AuthorizeRoomMediaUploadRequest{
		Meta: httpkit.RoomMeta(request, roomID, commandID), Purpose: purpose, Media: media.proto(),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	if authorization == nil || !authorization.GetAllowed() || strings.TrimSpace(authorization.GetObjectKeyPrefix()) == "" ||
		strings.TrimSpace(authorization.GetObjectKey()) == "" {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "room media upload denied")
		return
	}
	objectKey := strings.TrimLeft(strings.TrimSpace(authorization.GetObjectKey()), "/")
	completeCommandID := commandID
	if purpose == "room_cover" {
		completeCommandID = strings.TrimSpace(authorization.GetCoverUploadId())
		if !giftlimits.ValidCommandID(completeCommandID) {
			httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room cover upload authorization is incomplete")
			return
		}
	}
	objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, bytes.NewReader(content), int64(len(content)), media.ContentType)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload failed")
		return
	}
	if !h.objectURLVerifier.OwnsObjectURL(objectURL, objectKey) {
		// 上传成功但返回地址不属于配置 access_url 时拒绝下发，避免把不可信 URL 固化进房间状态。
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload url verification failed")
		return
	}
	media.URL, media.ObjectKey, media.Status = objectURL, objectKey, "active"
	completed, err := h.roomMediaGuard.CompleteRoomMediaUpload(request.Context(), &roomv1.CompleteRoomMediaUploadRequest{
		Meta: httpkit.RoomMeta(request, roomID, completeCommandID), Purpose: purpose, Media: media.proto(),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	if completed == nil || !completed.GetActive() {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed")
		return
	}
	completedMedia := roomMediaDataFromProto(completed.GetMedia())
	if completedMedia == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed")
		return
	}
	media = *completedMedia
	if purpose == "room_cover" {
		coverUploadID := strings.TrimSpace(completed.GetCoverUploadId())
		if coverUploadID == "" || coverUploadID != completeCommandID {
			httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room cover upload completion failed")
			return
		}
		httpkit.WriteOK(writer, request, roomCoverUploadData{
			CoverUploadID: coverUploadID,
			URL:           media.URL,
			SHA256:        media.SHA256,
			FrameCount:    media.FrameCount,
			Animated:      media.Animated,
			ContentType:   media.ContentType,
			Format:        media.Format,
			SizeBytes:     media.SizeBytes,
			Width:         media.Width,
			Height:        media.Height,
			DurationMS:    media.DurationMS,
			ServerTimeMS:  completed.GetServerTimeMs(),
		})
		return
	}
	httpkit.WriteOK(writer, request, roomMediaUploadData{
		Purpose: purpose, RoomID: roomID, CommandID: commandID, RoomVersion: authorization.GetRoomVersion(),
		Media: media, ServerTimeMS: completed.GetServerTimeMs(),
	})
}

func inspectRoomImage(content []byte) (roomMediaData, string, error) {
	var data roomMediaData
	var extension string
	switch {
	case len(content) >= 3 && content[0] == 0xff && content[1] == 0xd8 && content[2] == 0xff:
		cfg, err := jpeg.DecodeConfig(bytes.NewReader(content))
		if err != nil {
			return data, "", fmt.Errorf("invalid jpeg")
		}
		data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "jpeg", "image/jpeg", int32(cfg.Width), int32(cfg.Height), 1
		extension = "jpg"
	case len(content) >= 8 && bytes.Equal(content[:8], []byte{137, 80, 78, 71, 13, 10, 26, 10}):
		if pngHasAnimationChunk(content) {
			return data, "", fmt.Errorf("animated png is not supported")
		}
		cfg, err := png.DecodeConfig(bytes.NewReader(content))
		if err != nil {
			return data, "", fmt.Errorf("invalid png")
		}
		data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "png", "image/png", int32(cfg.Width), int32(cfg.Height), 1
		extension = "png"
	case len(content) >= 6 && (string(content[:6]) == "GIF87a" || string(content[:6]) == "GIF89a"):
		width, height, frames, durationMS, err := inspectGIF(content)
		if err != nil {
			return data, "", err
		}
		data.Format, data.ContentType, data.Width, data.Height = "gif", "image/gif", width, height
		data.FrameCount, data.Animated, data.DurationMS = frames, frames > 1, durationMS
		if !data.Animated {
			data.DurationMS = 0
		}
		extension = "gif"
	case len(content) >= 12 && string(content[:4]) == "RIFF" && string(content[8:12]) == "WEBP":
		width, height, frames, durationMS, animated, err := inspectWebP(content)
		if err != nil {
			return data, "", err
		}
		data.Format, data.ContentType, data.Width, data.Height = "webp", "image/webp", width, height
		data.FrameCount, data.DurationMS, data.Animated = frames, durationMS, animated
		extension = "webp"
	default:
		return data, "", fmt.Errorf("only jpeg, png, gif and webp are supported")
	}
	sum := sha256.Sum256(content)
	data.SizeBytes, data.SHA256, data.Status = int64(len(content)), hex.EncodeToString(sum[:]), "active"
	return data, extension, nil
}

func validateRoomUploadDimensions(purpose string, media roomMediaData) error {
	if media.Width <= 0 || media.Height <= 0 || media.FrameCount <= 0 {
		return fmt.Errorf("image dimensions are invalid")
	}
	if media.Animated && (media.FrameCount > roomUploadMaxFrames || media.DurationMS <= 0 || media.DurationMS > roomUploadMaxDurationMS) {
		return fmt.Errorf("animation exceeds frame or duration limit")
	}
	pixels := int64(media.Width) * int64(media.Height)
	if purpose == "room_background" {
		if media.Width > 2160 || media.Height > 3840 || pixels > 8_294_400 {
			return fmt.Errorf("room background dimensions exceed limit")
		}
		return nil
	}
	if media.Width > 4096 || media.Height > 4096 || pixels > 16_777_216 {
		if purpose == "room_cover" {
			return fmt.Errorf("room cover dimensions exceed limit")
		}
		return fmt.Errorf("room image dimensions exceed limit")
	}
	return nil
}

func pngHasAnimationChunk(content []byte) bool {
	for offset := 8; offset+12 <= len(content); {
		size := int(binary.BigEndian.Uint32(content[offset : offset+4]))
		if size < 0 || offset+12+size > len(content) {
			return false
		}
		if string(content[offset+4:offset+8]) == "acTL" {
			return true
		}
		offset += 12 + size
	}
	return false
}

func inspectGIF(content []byte) (int32, int32, int32, int64, error) {
	if len(content) < 13 || (string(content[:6]) != "GIF87a" && string(content[:6]) != "GIF89a") {
		return 0, 0, 0, 0, fmt.Errorf("invalid gif")
	}
	width := int32(binary.LittleEndian.Uint16(content[6:8]))
	height := int32(binary.LittleEndian.Uint16(content[8:10]))
	if width <= 0 || height <= 0 || width > 4096 || height > 4096 || int64(width)*int64(height) > 16_777_216 {
		// 先检查逻辑画布，后续只扫描块边界，不按攻击者声明的尺寸分配像素缓冲区。
		return 0, 0, 0, 0, fmt.Errorf("gif dimensions exceed limit")
	}

	offset := 13
	if content[10]&0x80 != 0 {
		colorTableBytes := 3 * (1 << (int(content[10]&0x07) + 1))
		if offset+colorTableBytes > len(content) {
			return 0, 0, 0, 0, fmt.Errorf("invalid gif color table")
		}
		offset += colorTableBytes
	}

	var frames int32
	var durationMS int64
	var pendingDelayMS int64
	for offset < len(content) {
		marker := content[offset]
		offset++
		switch marker {
		case 0x3b: // trailer
			if frames <= 0 {
				return 0, 0, 0, 0, fmt.Errorf("gif has no image frame")
			}
			if frames == 1 {
				durationMS = 0
			}
			return width, height, frames, durationMS, nil
		case 0x21: // extension
			if offset >= len(content) {
				return 0, 0, 0, 0, fmt.Errorf("invalid gif extension")
			}
			label := content[offset]
			offset++
			if label == 0xf9 {
				// Graphic Control Extension 固定为 4 字节数据和 0 终止符；delay 单位是 1/100 秒。
				if offset+6 > len(content) || content[offset] != 4 || content[offset+5] != 0 {
					return 0, 0, 0, 0, fmt.Errorf("invalid gif graphic control extension")
				}
				pendingDelayMS = int64(binary.LittleEndian.Uint16(content[offset+2:offset+4])) * 10
				offset += 6
				continue
			}
			var skipErr error
			offset, _, skipErr = skipGIFSubBlocks(content, offset)
			if skipErr != nil {
				return 0, 0, 0, 0, skipErr
			}
		case 0x2c: // image descriptor
			if offset+9 > len(content) {
				return 0, 0, 0, 0, fmt.Errorf("invalid gif image descriptor")
			}
			left := int(binary.LittleEndian.Uint16(content[offset : offset+2]))
			top := int(binary.LittleEndian.Uint16(content[offset+2 : offset+4]))
			frameWidth := int(binary.LittleEndian.Uint16(content[offset+4 : offset+6]))
			frameHeight := int(binary.LittleEndian.Uint16(content[offset+6 : offset+8]))
			packed := content[offset+8]
			if frameWidth <= 0 || frameHeight <= 0 || left+frameWidth > int(width) || top+frameHeight > int(height) {
				return 0, 0, 0, 0, fmt.Errorf("invalid gif frame dimensions")
			}
			offset += 9
			if packed&0x80 != 0 {
				colorTableBytes := 3 * (1 << (int(packed&0x07) + 1))
				if offset+colorTableBytes > len(content) {
					return 0, 0, 0, 0, fmt.Errorf("invalid gif local color table")
				}
				offset += colorTableBytes
			}
			if offset >= len(content) || content[offset] < 2 || content[offset] > 8 {
				return 0, 0, 0, 0, fmt.Errorf("invalid gif lzw code size")
			}
			offset++
			var hasImageData bool
			var skipErr error
			offset, hasImageData, skipErr = skipGIFSubBlocks(content, offset)
			if skipErr != nil || !hasImageData {
				return 0, 0, 0, 0, fmt.Errorf("invalid gif image data")
			}
			frames++
			durationMS += pendingDelayMS
			pendingDelayMS = 0
			if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS {
				return 0, 0, 0, 0, fmt.Errorf("gif animation exceeds frame or duration limit")
			}
		default:
			return 0, 0, 0, 0, fmt.Errorf("invalid gif block marker")
		}
	}
	return 0, 0, 0, 0, fmt.Errorf("gif trailer is missing")
}

func skipGIFSubBlocks(content []byte, offset int) (int, bool, error) {
	hasData := false
	for {
		if offset >= len(content) {
			return 0, false, fmt.Errorf("invalid gif sub-block")
		}
		blockSize := int(content[offset])
		offset++
		if blockSize == 0 {
			return offset, hasData, nil
		}
		if offset+blockSize > len(content) {
			return 0, false, fmt.Errorf("invalid gif sub-block")
		}
		hasData = true
		offset += blockSize
	}
}

func inspectWebP(content []byte) (int32, int32, int32, int64, bool, error) {
	if len(content) < 20 || string(content[:4]) != "RIFF" || string(content[8:12]) != "WEBP" {
		return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header")
	}
	declaredSize := uint64(binary.LittleEndian.Uint32(content[4:8])) + 8
	if declaredSize != uint64(len(content)) {
		// RIFF size 不匹配通常表示截断或尾部拼接；禁止只扫到某个看似合法的前缀就接受整个对象。
		return 0, 0, 0, 0, false, fmt.Errorf("invalid webp riff size")
	}

	var width, height int32
	var frames int32
	var durationMS int64
	var animatedFlag, hasVP8X, hasANIM, hasStaticImage bool
	for offset := 12; offset < len(content); {
		if offset+8 > len(content) {
			return 0, 0, 0, 0, false, fmt.Errorf("invalid webp trailing chunk")
		}
		chunkType := string(content[offset : offset+4])
		size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8]))
		start := uint64(offset + 8)
		end := start + size
		paddedEnd := end + size%2
		if end < start || paddedEnd > uint64(len(content)) {
			return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk")
		}
		if size%2 == 1 && content[end] != 0 {
			return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk padding")
		}
		chunk := content[int(start):int(end)]
		switch chunkType {
		case "VP8X":
			if hasVP8X || offset != 12 || len(chunk) != 10 || chunk[0]&0xc1 != 0 || chunk[1] != 0 || chunk[2] != 0 || chunk[3] != 0 {
				return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header")
			}
			hasVP8X = true
			animatedFlag = chunk[0]&0x02 != 0
			width = int32(readUint24LE(chunk[4:7])) + 1
			height = int32(readUint24LE(chunk[7:10])) + 1
		case "VP8 ", "VP8L":
			if hasStaticImage {
				return 0, 0, 0, 0, false, fmt.Errorf("webp has multiple top-level image payloads")
			}
			if !hasVP8X && offset != 12 {
				return 0, 0, 0, 0, false, fmt.Errorf("invalid simple webp chunk order")
			}
			imageWidth, imageHeight, err := inspectWebPImagePayload(chunkType, chunk)
			if err != nil {
				return 0, 0, 0, 0, false, err
			}
			hasStaticImage = true
			if width == 0 {
				width, height = imageWidth, imageHeight
			} else if imageWidth > width || imageHeight > height {
				return 0, 0, 0, 0, false, fmt.Errorf("webp image exceeds canvas")
			}
		case "ANIM":
			if !hasVP8X || !animatedFlag || hasANIM || len(chunk) != 6 {
				return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation header")
			}
			hasANIM = true
		case "ANMF":
			if !hasVP8X || !animatedFlag || !hasANIM || width <= 0 || height <= 0 || len(chunk) < 16 || chunk[15]&0xfc != 0 {
				return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame")
			}
			frameX := int64(readUint24LE(chunk[0:3])) * 2
			frameY := int64(readUint24LE(chunk[3:6])) * 2
			frameWidth := int32(readUint24LE(chunk[6:9])) + 1
			frameHeight := int32(readUint24LE(chunk[9:12])) + 1
			if frameWidth <= 0 || frameHeight <= 0 || frameX+int64(frameWidth) > int64(width) || frameY+int64(frameHeight) > int64(height) {
				return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame geometry")
			}
			if err := inspectWebPFramePayload(chunk[16:], frameWidth, frameHeight); err != nil {
				return 0, 0, 0, 0, false, err
			}
			frames++
			durationMS += int64(readUint24LE(chunk[12:15]))
			if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS {
				return 0, 0, 0, 0, false, fmt.Errorf("webp animation exceeds frame or duration limit")
			}
		}
		offset = int(paddedEnd)
	}
	if width <= 0 || height <= 0 {
		return 0, 0, 0, 0, false, fmt.Errorf("invalid webp dimensions")
	}
	if animatedFlag {
		if !hasANIM || hasStaticImage || frames < 2 || durationMS <= 0 {
			return 0, 0, 0, 0, false, fmt.Errorf("invalid animated webp")
		}
		return width, height, frames, durationMS, true, nil
	}
	if hasANIM || frames != 0 || !hasStaticImage {
		return 0, 0, 0, 0, false, fmt.Errorf("invalid static webp")
	}
	return width, height, 1, 0, false, nil
}

func inspectWebPImagePayload(chunkType string, chunk []byte) (int32, int32, error) {
	switch chunkType {
	case "VP8 ":
		// 10 字节只覆盖 lossy key-frame header；至少再有一个压缩 payload 字节，防止伪造尺寸头通过。
		if len(chunk) <= 10 || chunk[0]&0x01 != 0 || !bytes.Equal(chunk[3:6], []byte{0x9d, 0x01, 0x2a}) {
			return 0, 0, fmt.Errorf("invalid webp vp8 payload")
		}
		width := int32(binary.LittleEndian.Uint16(chunk[6:8]) & 0x3fff)
		height := int32(binary.LittleEndian.Uint16(chunk[8:10]) & 0x3fff)
		if width <= 0 || height <= 0 {
			return 0, 0, fmt.Errorf("invalid webp vp8 dimensions")
		}
		return width, height, nil
	case "VP8L":
		// VP8L signature+尺寸头为 5 字节；version 位必须为 0，并要求存在实际压缩 payload。
		if len(chunk) <= 5 || chunk[0] != 0x2f {
			return 0, 0, fmt.Errorf("invalid webp vp8l payload")
		}
		bits := binary.LittleEndian.Uint32(chunk[1:5])
		if bits>>29 != 0 {
			return 0, 0, fmt.Errorf("invalid webp vp8l version")
		}
		return int32(bits&0x3fff) + 1, int32((bits>>14)&0x3fff) + 1, nil
	default:
		return 0, 0, fmt.Errorf("unsupported webp image payload")
	}
}

func inspectWebPFramePayload(content []byte, expectedWidth int32, expectedHeight int32) error {
	if len(content) < 8 {
		return fmt.Errorf("webp animation frame has no image payload")
	}
	foundImage := false
	for offset := 0; offset < len(content); {
		if offset+8 > len(content) {
			return fmt.Errorf("invalid webp animation frame chunk")
		}
		chunkType := string(content[offset : offset+4])
		size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8]))
		start := uint64(offset + 8)
		end := start + size
		paddedEnd := end + size%2
		if end < start || paddedEnd > uint64(len(content)) {
			return fmt.Errorf("invalid webp animation frame chunk")
		}
		if size%2 == 1 && content[end] != 0 {
			return fmt.Errorf("invalid webp animation frame padding")
		}
		chunk := content[int(start):int(end)]
		switch chunkType {
		case "ALPH":
			if foundImage || len(chunk) == 0 {
				return fmt.Errorf("invalid webp animation alpha payload")
			}
		case "VP8 ", "VP8L":
			if foundImage {
				return fmt.Errorf("webp animation frame has multiple image payloads")
			}
			width, height, err := inspectWebPImagePayload(chunkType, chunk)
			if err != nil || width != expectedWidth || height != expectedHeight {
				return fmt.Errorf("webp animation image dimensions mismatch")
			}
			foundImage = true
		default:
			return fmt.Errorf("unsupported webp animation frame chunk")
		}
		offset = int(paddedEnd)
	}
	if !foundImage {
		return fmt.Errorf("webp animation frame has no image payload")
	}
	return nil
}

func readUint24LE(value []byte) uint32 {
	return uint32(value[0]) | uint32(value[1])<<8 | uint32(value[2])<<16
}
