package handlers

import (
	"bytes"
	"image"
	"image/color"
	"image/draw"
	_ "image/gif"
	"image/jpeg"
	_ "image/png"
	"net/http"
	"os"
	"path/filepath"
	"time"

	xdraw "golang.org/x/image/draw"
	_ "golang.org/x/image/webp"
)

// Open Graph image dimensions recommended for large summary cards
// (Discord, Twitter/X, Slack, etc.).
const (
	ogImageWidth  = 1200
	ogImageHeight = 630
	ogMaxTiles    = 4
	ogTileGap     = 8
)

var ogBackground = color.RGBA{R: 0x0b, G: 0x0b, B: 0x16, A: 0xff}

// BoxOGImage renders the social-preview image for a box: a collage of up to
// four file thumbnails, or a branded placeholder when none are available yet.
func (a *App) BoxOGImage(w http.ResponseWriter, r *http.Request) {
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	if err := a.uploadService.CanDownload(box); err != nil {
		a.serveOGImage(w, r, a.ogPlaceholder())
		return
	}

	// Never leak thumbnails of a locked, obfuscated box. (Protected-but-not-
	// obfuscated boxes already show their thumbnails on the download page, so
	// they may appear here too.)
	hideContents := a.uploadService.IsProtected(box) && box.Obfuscate

	thumbs := make([]image.Image, 0, ogMaxTiles)
	if !hideContents {
		for _, file := range box.Files {
			if len(thumbs) >= ogMaxTiles {
				break
			}
			if file.Thumbnail == "" && file.ThumbnailObjectKey == "" {
				continue
			}
			object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file)
			if err != nil {
				continue
			}
			img, _, decodeErr := image.Decode(object.Body)
			object.Body.Close()
			if decodeErr == nil {
				thumbs = append(thumbs, img)
			}
		}
	}

	if len(thumbs) == 0 {
		a.serveOGImage(w, r, a.ogPlaceholder())
		return
	}
	a.serveOGImage(w, r, renderCollage(thumbs))
}

func (a *App) serveOGImage(w http.ResponseWriter, r *http.Request, img image.Image) {
	var buf bytes.Buffer
	if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 85}); err != nil {
		http.Error(w, "could not render preview image", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "image/jpeg")
	// Social scrapers fetch this rarely and cache on their side; a modest cache
	// keeps it fresh as thumbnails finish generating.
	w.Header().Set("Cache-Control", "public, max-age=3600")
	http.ServeContent(w, r, "og-image.jpg", time.Time{}, bytes.NewReader(buf.Bytes()))
}

// ogPlaceholder builds the branded fallback image: the file placeholder icon
// centered on the brand background.
func (a *App) ogPlaceholder() image.Image {
	canvas := image.NewRGBA(image.Rect(0, 0, ogImageWidth, ogImageHeight))
	draw.Draw(canvas, canvas.Bounds(), &image.Uniform{ogBackground}, image.Point{}, draw.Src)

	file, err := os.Open(filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp"))
	if err != nil {
		return canvas
	}
	defer file.Close()
	icon, _, err := image.Decode(file)
	if err != nil {
		return canvas
	}

	// Scale the icon to ~40% of the canvas height and centre it.
	target := ogImageHeight * 2 / 5
	b := icon.Bounds()
	scale := float64(target) / float64(b.Dy())
	dw := int(float64(b.Dx()) * scale)
	dh := target
	x0 := (ogImageWidth - dw) / 2
	y0 := (ogImageHeight - dh) / 2
	xdraw.CatmullRom.Scale(canvas, image.Rect(x0, y0, x0+dw, y0+dh), icon, b, xdraw.Over, nil)
	return canvas
}

// renderCollage tiles up to four thumbnails into the OG canvas with a small gap.
func renderCollage(thumbs []image.Image) image.Image {
	canvas := image.NewRGBA(image.Rect(0, 0, ogImageWidth, ogImageHeight))
	draw.Draw(canvas, canvas.Bounds(), &image.Uniform{ogBackground}, image.Point{}, draw.Src)

	cols, rows := collageGrid(len(thumbs))
	cellW := (ogImageWidth - ogTileGap*(cols+1)) / cols
	cellH := (ogImageHeight - ogTileGap*(rows+1)) / rows

	i := 0
	for ry := 0; ry < rows && i < len(thumbs); ry++ {
		for cx := 0; cx < cols && i < len(thumbs); cx++ {
			x0 := ogTileGap + cx*(cellW+ogTileGap)
			y0 := ogTileGap + ry*(cellH+ogTileGap)
			drawCover(canvas, image.Rect(x0, y0, x0+cellW, y0+cellH), thumbs[i])
			i++
		}
	}
	return canvas
}

func collageGrid(n int) (cols, rows int) {
	switch {
	case n <= 1:
		return 1, 1
	case n == 2:
		return 2, 1
	case n == 3:
		return 3, 1
	default:
		return 2, 2
	}
}

// drawCover scales src to completely fill dst, cropping the overflow (centred),
// preserving aspect ratio — the CSS object-fit: cover equivalent.
func drawCover(dst *image.RGBA, cell image.Rectangle, src image.Image) {
	b := src.Bounds()
	iw, ih := b.Dx(), b.Dy()
	if iw <= 0 || ih <= 0 {
		return
	}
	cellAR := float64(cell.Dx()) / float64(cell.Dy())
	imgAR := float64(iw) / float64(ih)

	var sw, sh int
	if imgAR > cellAR {
		// Source is wider than the cell: crop the sides.
		sh = ih
		sw = int(float64(ih) * cellAR)
	} else {
		// Source is taller: crop top/bottom.
		sw = iw
		sh = int(float64(iw) / cellAR)
	}
	sx := b.Min.X + (iw-sw)/2
	sy := b.Min.Y + (ih-sh)/2
	xdraw.CatmullRom.Scale(dst, cell, src, image.Rect(sx, sy, sx+sw, sy+sh), xdraw.Over, nil)
}
