package boxstore

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

	"warpbox/lib/helpers"
	"warpbox/lib/models"
)

const (
	thumbnailDir     = ".thumbnails"
	thumbnailMaxSize = 160
)

type thumbnailTask struct {
	BoxID  string
	FileID string
	Name   string
}

func StartThumbnailWorker(batchSize int, interval time.Duration) {
	if batchSize < 1 {
		batchSize = 10
	}

	if interval <= 0 {
		interval = 30 * time.Second
	}

	go func() {
		for {
			ProcessThumbnailBatch(batchSize)
			time.Sleep(interval)
		}
	}()
}

func ProcessThumbnailBatch(batchSize int) int {
	tasks := collectThumbnailTasks(batchSize)
	for _, task := range tasks {
		if err := generateThumbnail(task); err != nil {
			markThumbnailFailed(task.BoxID, task.FileID)
			continue
		}
	}

	return len(tasks)
}

func ThumbnailFilePath(boxID string, fileID string) (string, bool) {
	if !helpers.ValidLowerHexID(fileID, 16) {
		return "", false
	}

	return helpers.SafeChildPath(filepath.Join(BoxPath(boxID), thumbnailDir), fileID+".jpg")
}

func collectThumbnailTasks(batchSize int) []thumbnailTask {
	entries, err := os.ReadDir(uploadRoot)
	if err != nil {
		return nil
	}

	tasks := make([]thumbnailTask, 0, batchSize)
	for _, entry := range entries {
		if !entry.IsDir() || !ValidBoxID(entry.Name()) {
			continue
		}

		tasks = append(tasks, collectBoxThumbnailTasks(entry.Name(), batchSize-len(tasks))...)
		if len(tasks) >= batchSize {
			return tasks
		}
	}

	return tasks
}

func collectBoxThumbnailTasks(boxID string, remaining int) []thumbnailTask {
	if remaining <= 0 {
		return nil
	}

	manifestMu.Lock()
	defer manifestMu.Unlock()

	manifest, err := readManifestUnlocked(boxID)
	if err != nil || IsExpired(manifest) {
		return nil
	}

	tasks := make([]thumbnailTask, 0, remaining)
	changed := false
	for index, file := range manifest.Files {
		if len(tasks) >= remaining {
			break
		}

		if file.Status != models.FileStatusReady || file.ThumbnailPath != nil || file.ThumbnailStatus != "" {
			continue
		}

		if !canGenerateThumbnail(file) {
			manifest.Files[index].ThumbnailStatus = models.ThumbnailStatusUnsupported
			changed = true
			continue
		}

		tasks = append(tasks, thumbnailTask{
			BoxID:  boxID,
			FileID: file.ID,
			Name:   file.Name,
		})
	}

	if changed {
		writeManifestUnlocked(boxID, manifest)
	}

	return tasks
}

func canGenerateThumbnail(file models.BoxFile) bool {
	if strings.HasPrefix(file.MimeType, "image/") {
		return true
	}

	extension := strings.ToLower(filepath.Ext(file.Name))
	return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif"
}

func generateThumbnail(task thumbnailTask) error {
	sourcePath, ok := SafeBoxFilePath(task.BoxID, task.Name)
	if !ok {
		return os.ErrInvalid
	}
	if err := ensureRegularFile(sourcePath); err != nil {
		return err
	}

	source, err := os.Open(sourcePath)
	if err != nil {
		return err
	}
	defer source.Close()

	src, _, err := image.Decode(source)
	if err != nil {
		return err
	}

	thumb := resizeImage(src, thumbnailMaxSize)
	if err := os.MkdirAll(filepath.Join(BoxPath(task.BoxID), thumbnailDir), 0755); err != nil {
		return err
	}

	path, ok := ThumbnailFilePath(task.BoxID, task.FileID)
	if !ok {
		return os.ErrInvalid
	}

	target, tempPath, err := createTempSibling(path)
	if err != nil {
		return err
	}
	committed := false
	defer func() {
		target.Close()
		if !committed {
			os.Remove(tempPath)
		}
	}()

	if err := jpeg.Encode(target, thumb, &jpeg.Options{Quality: 82}); err != nil {
		return err
	}
	if err := target.Close(); err != nil {
		return err
	}
	if err := os.Rename(tempPath, path); err != nil {
		return err
	}
	committed = true

	return markThumbnailReady(task.BoxID, task.FileID)
}

func resizeImage(src image.Image, maxSize int) image.Image {
	bounds := src.Bounds()
	width := bounds.Dx()
	height := bounds.Dy()
	if width <= 0 || height <= 0 {
		return src
	}

	targetWidth := width
	targetHeight := height
	if width > maxSize || height > maxSize {
		if width >= height {
			targetWidth = maxSize
			targetHeight = height * maxSize / width
		} else {
			targetHeight = maxSize
			targetWidth = width * maxSize / height
		}
	}

	if targetWidth < 1 {
		targetWidth = 1
	}
	if targetHeight < 1 {
		targetHeight = 1
	}

	dst := image.NewRGBA(image.Rect(0, 0, targetWidth, targetHeight))
	draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
	for y := 0; y < targetHeight; y++ {
		for x := 0; x < targetWidth; x++ {
			srcX := bounds.Min.X + x*width/targetWidth
			srcY := bounds.Min.Y + y*height/targetHeight
			dst.Set(x, y, src.At(srcX, srcY))
		}
	}

	return dst
}

func markThumbnailReady(boxID string, fileID string) error {
	path := "/box/" + boxID + "/thumbnails/" + url.PathEscape(fileID)
	return updateThumbnailState(boxID, fileID, &path, models.ThumbnailStatusReady)
}

func markThumbnailFailed(boxID string, fileID string) {
	updateThumbnailState(boxID, fileID, nil, models.ThumbnailStatusFailed)
}

func updateThumbnailState(boxID string, fileID string, thumbnailPath *string, status string) error {
	manifestMu.Lock()
	defer manifestMu.Unlock()

	manifest, err := readManifestUnlocked(boxID)
	if err != nil {
		return err
	}

	for index, file := range manifest.Files {
		if file.ID != fileID {
			continue
		}

		manifest.Files[index].ThumbnailPath = thumbnailPath
		manifest.Files[index].ThumbnailStatus = status
		return writeManifestUnlocked(boxID, manifest)
	}

	return os.ErrNotExist
}
