package images

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"hash/fnv"
	"image"
	"image/jpeg"
	_ "image/png"
	"io"
	"strings"

	_ "golang.org/x/image/webp"

	"github.com/valkey-io/valkey-go"
)

var ErrNotAnImage = errors.New("not an image")

func (s *Service) GetMime(path string) (mimeType string, err error) {
	info, err := s.fs.Stat(path)
	if err != nil {
		err = fmt.Errorf("image file info could not be fetched: %w", err)
	}

	if mimer, ok := info.(interface{ ContentType() string }); ok {
		mimeType = mimer.ContentType()
		return
	}

	return
}

func (s *Service) getImage(path string) (file []byte, mimeType string, err error) {
	if err != nil {
		err = fmt.Errorf("could not get path: %w", err)
		return
	}
	mimeType, err = s.GetMime(path)
	if err != nil {
		return
	}

	if !strings.HasPrefix(mimeType, "image") {
		err = ErrNotAnImage
		return
	}

	file, err = s.fs.Read(path)
	if err != nil {
		err = fmt.Errorf("image file could not be fetched")
	}

	return
}

func (s *Service) GetImage(path string) (img Image, err error) {
	rawImage, _, err := s.getImage(path)
	if err != nil {
		return
	}

	img.Path = path
	img.Image, _, err = image.Decode(bytes.NewBuffer(rawImage))

	return
}

func (s *Service) GetScaledImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
	if err = s.seph.Acquire(ctx, 1); err != nil {
		err = fmt.Errorf("could not Acquire semaphore: %w", err)
		return

	}
	defer s.seph.Release(1)

	path, err := PathFromUID(uid)
	if err != nil {
		return
	}

	rawImage, mimeType, err := s.getImage(path)
	if err != nil {
		return
	}

	hash, err := hash(rawImage, options)
	if err != nil {
		return
	}

	if imgBytes, err := s.cache.Do(ctx, s.cache.B().Get().Key("img:"+hash).Build()).AsBytes(); err == nil {
		return bytes.NewBuffer(imgBytes), mimeType, err
	}

	decImage, _, err := image.Decode(bytes.NewBuffer(rawImage))
	if err != nil {
		return
	}

	decImage = scale(decImage, options)
	outBuff := bytes.NewBuffer(make([]byte, 0, 1024))

	if options.Quality == 0 {
		options.Quality = s.cfg.Quality
	}

	err = jpeg.Encode(outBuff, decImage, &jpeg.Options{Quality: options.Quality})
	if err != nil {
		return
	}
	if err = s.cache.Do(ctx, s.cache.B().Set().Key("img:"+hash).Value(valkey.BinaryString(outBuff.Bytes())).Ex(s.cfg.CacheLifetime).Build()).Error(); err == nil {
		return outBuff, "image/jpeg", err
	}

	return outBuff, "image/jpeg", err
}

func hash(bytes []byte, options Options) (hash string, err error) {
	h := fnv.New128()

	if _, err = h.Write(bytes); err != nil {
		return
	}
	if err = binary.Write(h, binary.LittleEndian, int64(options.Height)); err != nil {
		return
	}
	if err = binary.Write(h, binary.LittleEndian, int64(options.Width)); err != nil {
		return
	}
	if err = binary.Write(h, binary.LittleEndian, int64(options.Quality)); err != nil {
		return
	}

	res := h.Sum(make([]byte, 0, 128/8))

	return base64.RawStdEncoding.EncodeToString(res), nil
}
