package s3

import (
	"fmt"
	"io"
	"net/url"
	"path/filepath"
	"strings"
	"time"

	"source.offmarket.win/aleagle/eaglecast/internal/media"
	"github.com/rhnvrm/simples3"
)

// Opt represents AWS S3 specific params
type Opt struct {
	URL        string        `koanf:"url"`
	PublicURL  string        `koanf:"public_url"`
	AccessKey  string        `koanf:"aws_access_key_id"`
	SecretKey  string        `koanf:"aws_secret_access_key"`
	Region     string        `koanf:"aws_default_region"`
	Bucket     string        `koanf:"bucket"`
	BucketPath string        `koanf:"bucket_path"`
	BucketType string        `koanf:"bucket_type"`
	Expiry     time.Duration `koanf:"expiry"`
	RootURL    string        `koanf:"root_url"`
}

// Client implements `media.Store` for S3 provider
type Client struct {
	s3   *simples3.S3
	opts Opt
}

// NewS3Store initialises store for S3 provider. It takes in the AWS configuration
// and sets up the `simples3` client to interact with AWS APIs for all bucket operations.
func NewS3Store(opt Opt) (media.Store, error) {
	var cl *simples3.S3
	if opt.URL == "" {
		opt.URL = fmt.Sprintf("https://s3.%s.amazonaws.com", opt.Region)
	}
	opt.URL = strings.TrimRight(opt.URL, "/")

	// Default (and max S3 expiry) is 7 days.
	if opt.Expiry.Seconds() < 1 {
		opt.Expiry = time.Duration(167) * time.Hour
	}

	if opt.AccessKey == "" && opt.SecretKey == "" {
		// fallback to IAM role if no access key/secret key is provided.
		cl, _ = simples3.NewUsingIAM(opt.Region)
	}

	if cl == nil {
		cl = simples3.New(opt.Region, opt.AccessKey, opt.SecretKey)
	}

	cl.SetEndpoint(opt.URL)

	return &Client{
		s3:   cl,
		opts: opt,
	}, nil
}

// Put takes in the filename, the content type and file object itself and uploads to S3.
func (c *Client) Put(name string, cType string, file io.ReadSeeker) (string, error) {
	// Upload input parameters
	p := simples3.UploadInput{
		Bucket:      c.opts.Bucket,
		ContentType: cType,
		FileName:    name,
		Body:        file,

		// Paths inside the bucket should not start with /.
		ObjectKey: c.makeBucketPath(name),
	}

	if c.opts.BucketType == "public" {
		p.ACL = "public-read"
	}

	// Upload.
	if _, err := c.s3.FilePut(p); err != nil {
		return "", err
	}
	return name, nil
}

// Get accepts the filename of the object stored and retrieves from S3.
func (c *Client) GetURL(name string) string {
	// Generate a private S3 pre-signed URL if it's a private bucket, and there
	// is no public URL provided.
	if c.opts.BucketType == "private" && c.opts.PublicURL == "" {
		u := c.s3.GeneratePresignedURL(simples3.PresignedInput{
			Bucket:        c.opts.Bucket,
			ObjectKey:     c.makeBucketPath(name),
			Method:        "GET",
			Timestamp:     time.Now(),
			ExpirySeconds: int(c.opts.Expiry.Seconds()),
		})

		return u
	}

	// Generate a public S3 URL if it's a public bucket or a public URL is
	// provided.
	return c.makeFileURL(name)
}

// GetBlob reads a file from S3 and returns the raw bytes.
func (c *Client) GetBlob(uurl string) ([]byte, error) {
	if p, err := url.Parse(uurl); err != nil {
		uurl = filepath.Base(uurl)
	} else {
		uurl = filepath.Base(p.Path)
	}

	// Download the file from S3.
	file, err := c.s3.FileDownload(simples3.DownloadInput{
		Bucket:    c.opts.Bucket,
		ObjectKey: c.makeBucketPath(filepath.Base(uurl)),
	})
	if err != nil {
		return nil, err
	}

	// Read it into a byte blob.
	b, err := io.ReadAll(file)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	return b, nil
}

// Delete accepts the filename of the object and deletes from S3.
func (c *Client) Delete(name string) error {
	err := c.s3.FileDelete(simples3.DeleteInput{
		Bucket:    c.opts.Bucket,
		ObjectKey: c.makeBucketPath(name),
	})

	return err
}

// makeBucketPath returns the file path inside the bucket. The path should not
// start with a /.
func (c *Client) makeBucketPath(name string) string {
	// If the path is root (/), return the filename without the preceding slash.
	p := strings.TrimPrefix(strings.TrimSuffix(c.opts.BucketPath, "/"), "/")
	if p == "" {
		return name
	}

	// whatever/bucket/path/filename.jpg: No preceding slash.
	return p + "/" + name
}

func (c *Client) makeFileURL(name string) string {
	if c.opts.PublicURL != "" {
		prefix := c.opts.PublicURL
		if strings.HasPrefix(prefix, "/") {
			prefix = c.opts.RootURL + prefix
		}
		return prefix + "/" + c.makeBucketPath(name)
	}

	return c.opts.URL + "/" + c.opts.Bucket + "/" + c.makeBucketPath(name)
}
