package services

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand/v2"
	nurl "net/url"
	"path/filepath"
	"time"

	localCache "git.solsynth.dev/hypernet/paperclip/pkg/internal/cache"
	"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
	"git.solsynth.dev/hypernet/paperclip/pkg/internal/models"
	"github.com/eko/gocache/lib/v4/cache"
	"github.com/eko/gocache/lib/v4/marshaler"
	"github.com/eko/gocache/lib/v4/store"
	jsoniter "github.com/json-iterator/go"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
	"github.com/samber/lo"
)

type openAttachmentResult struct {
	Attachment models.Attachment        `json:"attachment"`
	Boosts     []models.AttachmentBoost `json:"boost"`
}

func GetAttachmentOpenCacheKey(rid string) any {
	return fmt.Sprintf("attachment-open#%s", rid)
}

func OpenAttachmentByRID(rid string, region ...string) (url string, mimetype string, err error) {
	cacheManager := cache.New[any](localCache.S)
	marshal := marshaler.New(cacheManager)
	contx := context.Background()

	var result *openAttachmentResult
	if val, err := marshal.Get(
		contx,
		GetAttachmentOpenCacheKey(rid),
		new(openAttachmentResult),
	); err == nil {
		result = val.(*openAttachmentResult)
	}

	if result == nil {
		var attachment models.Attachment
		if err = database.C.Where(models.Attachment{
			Rid: rid,
		}).
			Preload("Pool").
			Preload("Thumbnail").
			Preload("Compressed").
			First(&attachment).Error; err != nil {
			return
		}

		var boosts []models.AttachmentBoost
		boosts, err = ListBoostByAttachmentWithStatus(attachment.ID, models.BoostStatusActive)
		if err != nil {
			return
		}

		result = &openAttachmentResult{
			Attachment: attachment,
			Boosts:     boosts,
		}
	}

	if len(result.Attachment.MimeType) > 0 {
		mimetype = result.Attachment.MimeType
	}

	var dest models.BaseDestination
	var rawDest []byte

	if len(region) > 0 {
		if des, ok := DestinationsByRegion[region[0]]; ok {
			for _, boost := range result.Boosts {
				if boost.Destination == des.Index {
					rawDest = des.Raw
					json.Unmarshal(rawDest, &dest)
				}
			}
		}
	}
	if rawDest == nil {
		if len(result.Boosts) > 0 {
			randomIdx := rand.IntN(len(result.Boosts))
			boost := result.Boosts[randomIdx]
			if des, ok := DestinationsByIndex[boost.Destination]; ok {
				rawDest = des.Raw
				json.Unmarshal(rawDest, &dest)
			}
		} else {
			if des, ok := DestinationsByIndex[result.Attachment.Destination]; ok {
				rawDest = des.Raw
				json.Unmarshal(rawDest, &dest)
			}
		}
	}

	if rawDest == nil {
		err = fmt.Errorf("no destination found")
		return
	}

	switch dest.Type {
	case models.DestinationTypeLocal:
		var destConfigured models.LocalDestination
		_ = jsoniter.Unmarshal(rawDest, &destConfigured)
		url = "file://" + filepath.Join(destConfigured.Path, result.Attachment.Uuid)
		return
	case models.DestinationTypeS3:
		var destConfigured models.S3Destination
		_ = jsoniter.Unmarshal(rawDest, &destConfigured)
		if destConfigured.EnableSigned {
			var client *minio.Client
			client, err = minio.New(destConfigured.Endpoint, &minio.Options{
				Creds:  credentials.NewStaticV4(destConfigured.SecretID, destConfigured.SecretKey, ""),
				Secure: destConfigured.EnableSSL,
			})
			if err != nil {
				return
			}

			var uri *nurl.URL
			uri, err = client.PresignedGetObject(context.Background(), destConfigured.Bucket, result.Attachment.Uuid, 60*time.Minute, nil)
			if err != nil {
				return
			}

			url = uri.String()
			return
		}
		if len(destConfigured.AccessBaseURL) > 0 {
			url = fmt.Sprintf(
				"%s/%s",
				destConfigured.AccessBaseURL,
				nurl.QueryEscape(filepath.Join(destConfigured.Path, result.Attachment.Uuid)),
			)
		} else {
			protocol := lo.Ternary(destConfigured.EnableSSL, "https", "http")
			url = fmt.Sprintf(
				"%s://%s.%s/%s",
				protocol,
				destConfigured.Bucket,
				destConfigured.Endpoint,
				nurl.QueryEscape(filepath.Join(destConfigured.Path, result.Attachment.Uuid)),
			)
		}
		return
	default:
		err = fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
		return
	}
}

func CacheOpenAttachment(item *openAttachmentResult) {
	if item == nil {
		return
	}

	cacheManager := cache.New[any](localCache.S)
	marshal := marshaler.New(cacheManager)
	contx := context.Background()

	_ = marshal.Set(
		contx,
		GetAttachmentCacheKey(item.Attachment.Rid),
		*item,
		store.WithExpiration(60*time.Minute),
		store.WithTags([]string{"attachment-open", fmt.Sprintf("user#%s", item.Attachment.Rid)}),
	)
}