package attachment

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	"image/jpeg"

	// Register decoders for the formats we thumbnail.
	_ "image/gif"
	_ "image/png"

	"golang.org/x/image/draw"
)

// ThumbnailVariant is the derivative name stored in attachment_variants and
// surfaced as ?v= on the thumbnail URL. v2 bakes EXIF orientation into the
// pixels, so v1 objects holding sideways renders are not reused.
const ThumbnailVariant = "thumb-v2"

// thumbnailMaxEdge is the longest-edge cap (pixels) for generated thumbnails.
const thumbnailMaxEdge = 480

// thumbnailJPEGQuality is the re-encode quality for thumbnail output.
const thumbnailJPEGQuality = 82

// thumbnailMaxPixels guards against decompression bombs: a small file can
// decode into a huge pixel array. Reject anything above this before the full
// decode.
const thumbnailMaxPixels = 40_000_000

// ErrThumbnailNotSupported signals the source bytes are not a safely-decodable
// raster image for thumbnailing (e.g. SVG/AVIF/WebP/HEIC). Calllers should
// leave the placeholder and let the user open the original on tap.
var ErrThumbnailNotSupported = fmt.Errorf("thumbnail not supported for this image")

// GeneratedThumbnail is the bytes + dimensions produced by GenerateThumbnail.
type GeneratedThumbnail struct {
	Blob        []byte
	ContentType string
	Width       int
	Height      int
}

// GenerateThumbnail decodes source image bytes (JPEG/PNG/GIF first frame),
// resizes so the longest edge is at most thumbnailMaxEdge (never upscales),
// and re-encodes as JPEG — which drops EXIF/metadata and produces a compact,
// universally-renderable derivative. Undecodable formats return
// ErrThumbnailNotSupported so the caller skips generation rather than failing
// the message list.
func GenerateThumbnail(source []byte, contentType string) (GeneratedThumbnail, error) {
	if len(source) == 0 {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}
	// Guard against decompression bombs: check dimensions before full decode.
	config, _, err := image.DecodeConfig(bytes.NewReader(source))
	if err != nil {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}
	if config.Width <= 0 || config.Height <= 0 {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}
	if config.Width*config.Height > thumbnailMaxPixels {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}
	src, _, err := image.Decode(bytes.NewReader(source))
	if err != nil {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}
	// Bake EXIF orientation into the pixels before resizing: re-encoding
	// drops the tag, and Go's JPEG decoder ignores it, so phone photos would
	// otherwise render sideways.
	src = applyExifOrientation(src, readExifOrientation(source))
	bounds := src.Bounds()
	srcW := bounds.Dx()
	srcH := bounds.Dy()
	if srcW <= 0 || srcH <= 0 {
		return GeneratedThumbnail{}, ErrThumbnailNotSupported
	}

	dstW, dstH := srcW, srcH
	longest := srcW
	if srcH > longest {
		longest = srcH
	}
	if longest > thumbnailMaxEdge {
		scale := float64(thumbnailMaxEdge) / float64(longest)
		dstW = max(1, int(float64(srcW)*scale))
		dstH = max(1, int(float64(srcH)*scale))
	}

	// Fill with white so transparent PNG/GIF regions don't turn black in JPEG.
	dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
	draw.Draw(dst, dst.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src)
	draw.CatmullRom.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil)

	var buf bytes.Buffer
	if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: thumbnailJPEGQuality}); err != nil {
		return GeneratedThumbnail{}, fmt.Errorf("encode thumbnail: %w", err)
	}
	return GeneratedThumbnail{
		Blob:        buf.Bytes(),
		ContentType: "image/jpeg",
		Width:       dstW,
		Height:      dstH,
	}, nil
}
