package backend

import (
	"bytes"
	"context"
	"image"
	"image/color"
	_ "image/gif"
	"image/jpeg"
	_ "image/png"
	"os"
)

const maxImageExtractDecodedPixels = int64(16_000_000)

// Normalization is deliberately serialized so a configurable number of
// semantic workers cannot all hold a large source, decoded raster, resize
// buffer, and encoder buffer at the same time.
var imageExtractNormalizeSlot = make(chan struct{}, 1)

func acquireImageExtractNormalizeSlot(ctx context.Context) (func(), bool) {
	select {
	case imageExtractNormalizeSlot <- struct{}{}:
		return func() { <-imageExtractNormalizeSlot }, true
	case <-ctx.Done():
		return nil, false
	}
}

// normalizeImageForExtract converts an oversized, decodable image into a
// bounded JPEG payload for the provider. It uses nearest-neighbor sampling
// and the first frame returned by the standard-library GIF decoder. The source
// file is never modified, but JPEG metadata such as EXIF orientation is not
// preserved. The process-wide slot bounds peak normalization memory to one
// in-flight image at a time.
func normalizeImageForExtract(ctx context.Context, data []byte, maxBytes int64) ([]byte, string, bool) {
	if maxBytes <= 0 || int64(len(data)) <= maxBytes {
		return data, "", true
	}
	release, ok := acquireImageExtractNormalizeSlot(ctx)
	if !ok {
		return nil, "", false
	}
	defer release()
	return normalizeImageForExtractLocked(ctx, data, maxBytes)
}

func normalizeImageForExtractLocked(ctx context.Context, data []byte, maxBytes int64) ([]byte, string, bool) {
	if err := ctx.Err(); err != nil {
		return nil, "", false
	}

	config, format, err := image.DecodeConfig(bytes.NewReader(data))
	if err != nil || !supportedImageNormalizationFormat(format) || config.Width <= 0 || config.Height <= 0 {
		return nil, "", false
	}
	if int64(config.Width) > maxImageExtractDecodedPixels/int64(config.Height) {
		return nil, "", false
	}

	if err := ctx.Err(); err != nil {
		return nil, "", false
	}
	src, _, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		return nil, "", false
	}

	width, height := config.Width, config.Height
	for scale := 1.0; scale >= 0.0625; scale *= 0.75 {
		if err := ctx.Err(); err != nil {
			return nil, "", false
		}
		targetWidth := max(1, int(float64(width)*scale))
		targetHeight := max(1, int(float64(height)*scale))
		candidate := src
		if targetWidth != width || targetHeight != height {
			candidate = resizeImageNearest(src, targetWidth, targetHeight)
		}

		for quality := 90; quality >= 30; quality -= 15 {
			if err := ctx.Err(); err != nil {
				return nil, "", false
			}
			var encoded bytes.Buffer
			if err := jpeg.Encode(&encoded, opaqueImage{Image: candidate}, &jpeg.Options{Quality: quality}); err != nil {
				// The JPEG encoder has a dimension limit. A smaller scale may
				// still be valid, so continue rather than rejecting the source.
				break
			}
			if int64(encoded.Len()) <= maxBytes {
				return encoded.Bytes(), "image/jpeg", true
			}
		}
	}
	return nil, "", false
}

// normalizeImageFileForExtractLocked decodes a staged source directly from
// disk. This keeps large S3-backed sources out of a second in-memory decode
// buffer while retaining the same pixel-bomb and output-size guards.
func normalizeImageFileForExtractLocked(ctx context.Context, path string, maxBytes int64) ([]byte, string, bool) {
	if err := ctx.Err(); err != nil {
		return nil, "", false
	}
	f, err := os.Open(path)
	if err != nil {
		return nil, "", false
	}
	defer func() { _ = f.Close() }()
	config, format, err := image.DecodeConfig(f)
	if err != nil || !supportedImageNormalizationFormat(format) || config.Width <= 0 || config.Height <= 0 {
		return nil, "", false
	}
	if int64(config.Width) > maxImageExtractDecodedPixels/int64(config.Height) {
		return nil, "", false
	}
	if _, err := f.Seek(0, 0); err != nil {
		return nil, "", false
	}
	src, _, err := image.Decode(f)
	if err != nil {
		return nil, "", false
	}
	width, height := config.Width, config.Height
	for scale := 1.0; scale >= 0.0625; scale *= 0.75 {
		if err := ctx.Err(); err != nil {
			return nil, "", false
		}
		targetWidth, targetHeight := max(1, int(float64(width)*scale)), max(1, int(float64(height)*scale))
		candidate := src
		if targetWidth != width || targetHeight != height {
			candidate = resizeImageNearest(src, targetWidth, targetHeight)
		}
		for quality := 90; quality >= 30; quality -= 15 {
			var encoded bytes.Buffer
			if err := jpeg.Encode(&encoded, opaqueImage{Image: candidate}, &jpeg.Options{Quality: quality}); err != nil {
				break
			}
			if int64(encoded.Len()) <= maxBytes {
				return encoded.Bytes(), "image/jpeg", true
			}
		}
	}
	return nil, "", false
}

// opaqueImage composites transparent pixels onto white without allocating a
// second raster. JPEG has no alpha channel and otherwise turns transparent
// pixels black, which can erase dark text on transparent image backgrounds.
type opaqueImage struct {
	image.Image
}

func (i opaqueImage) ColorModel() color.Model { return color.RGBAModel }

func (i opaqueImage) At(x, y int) color.Color {
	c := color.NRGBAModel.Convert(i.Image.At(x, y)).(color.NRGBA)
	if c.A == 255 {
		return c
	}
	a := int(c.A)
	return color.NRGBA{
		R: uint8((int(c.R)*a + 255*(255-a)) / 255),
		G: uint8((int(c.G)*a + 255*(255-a)) / 255),
		B: uint8((int(c.B)*a + 255*(255-a)) / 255),
		A: 255,
	}
}

func supportedImageNormalizationFormat(format string) bool {
	switch format {
	case "jpeg", "png", "gif":
		return true
	default:
		return false
	}
}

func supportedImageNormalizationContentType(contentType string) bool {
	switch stripMIMEParams(contentType) {
	case "image/jpeg", "image/jpg", "image/png", "image/gif":
		return true
	default:
		return false
	}
}

func resizeImageNearest(src image.Image, width, height int) *image.RGBA {
	dst := image.NewRGBA(image.Rect(0, 0, width, height))
	srcBounds := src.Bounds()
	srcWidth, srcHeight := srcBounds.Dx(), srcBounds.Dy()
	for y := 0; y < height; y++ {
		sy := srcBounds.Min.Y + y*srcHeight/height
		for x := 0; x < width; x++ {
			sx := srcBounds.Min.X + x*srcWidth/width
			dst.Set(x, y, src.At(sx, sy))
		}
	}
	return dst
}
