//go:build !noaws && !awsmock

package aws

import (
	"context"
	"errors"
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/forceu/gokapi/internal/models"
	"github.com/forceu/gokapi/internal/webserver/headers"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

var awsConfig models.AwsConfig

var isCorrectLogin bool

// IsIncludedInBuild is true if Gokapi has been compiled with AWS support or the API is being mocked
const IsIncludedInBuild = true

// IsMockApi is true if the API is being mocked and therefore can only be used for testing purposes
const IsMockApi = false

// Init reads the credentials for AWS. Returns true if valid
func Init(config models.AwsConfig) bool {
	awsConfig = config
	ok, err := IsValidLogin(config)
	if err != nil {
		fmt.Println("WARNING: AWS login not successful")
		fmt.Println(err.Error())
		isCorrectLogin = false
		return false
	}
	if ok {
		fmt.Println("AWS login successful")
		isCorrectLogin = true
	}
	return ok
}

// AddBucketName adds the bucket name to the file to be stored
func AddBucketName(file *models.File) {
	file.AwsBucket = awsConfig.Bucket
}

// IsAvailable returns true if valid credentials have been passed
func IsAvailable() bool {
	return isCorrectLogin
}

// LogOut resets the credentials
func LogOut() {
	awsConfig = models.AwsConfig{}
	isCorrectLogin = false
}

// IsValidLogin checks if a valid login was provided
func IsValidLogin(config models.AwsConfig) (bool, error) {
	if !config.IsAllProvided() {
		return false, nil
	}
	tempConfig := awsConfig
	awsConfig = config
	_, _, err := FileExists(models.File{AwsBucket: awsConfig.Bucket, SHA1: "invalid"})
	awsConfig = tempConfig
	if err != nil {
		return false, err
	}
	return true, nil
}

func createSession() *session.Session {
	s3Config := &aws.Config{
		Credentials:      credentials.NewStaticCredentials(awsConfig.KeyId, awsConfig.KeySecret, ""),
		Endpoint:         aws.String(awsConfig.Endpoint),
		Region:           aws.String(awsConfig.Region),
		S3ForcePathStyle: aws.Bool(true),
	}
	return session.Must(session.NewSession(s3Config))
}

// Upload uploads a file to AWS
func Upload(input io.Reader, file models.File) (string, error) {
	sess := createSession()
	uploader := s3manager.NewUploader(sess)

	result, err := uploader.Upload(&s3manager.UploadInput{
		Bucket: aws.String(file.AwsBucket),
		Key:    aws.String(file.SHA1),
		Body:   input,
	})
	if err != nil {
		return "", err
	}
	return result.Location, nil
}

// Download downloads a file from AWS, used for encrypted files and testing
func Download(writer io.WriterAt, file models.File) (int64, error) {
	sess := createSession()
	downloader := s3manager.NewDownloader(sess)

	size, err := downloader.Download(writer, &s3.GetObjectInput{
		Bucket: aws.String(file.AwsBucket),
		Key:    aws.String(file.SHA1),
	})
	if err != nil {
		return 0, err
	}
	return size, nil
}

// ServeFile either redirects the user to a pre-signed download url (default) or downloads the file and serves it as a proxy (depending
// on configuration). Returns true if blocking operation (in order to set download status) or false if non-blocking.
func ServeFile(w http.ResponseWriter, r *http.Request, file models.File, forceDownload bool) (bool, error) {
	if !awsConfig.ProxyDownload {
		return false, redirectToDownload(w, r, file, forceDownload)
	}
	return true, proxyDownload(w, file, forceDownload)
}

func getPresignedUrl(file models.File, forceDownload bool) (string, error) {
	sess := createSession()
	s3svc := s3.New(sess)

	dispositionType := "inline"
	if forceDownload {
		dispositionType = "Attachment"
	}
	// Use RFC 6266 format to support UTF-8 filenames and avoid encoding errors.
	contentDisposition := fmt.Sprintf("%s; filename*=UTF-8''%s", dispositionType, url.PathEscape(file.Name))

	req, _ := s3svc.GetObjectRequest(&s3.GetObjectInput{
		Bucket:                     aws.String(file.AwsBucket),
		Key:                        aws.String(file.SHA1),
		ResponseContentDisposition: aws.String(contentDisposition),
		ResponseCacheControl:       aws.String("no-store"),
		ResponseContentType:        aws.String(file.ContentType),
	})

	return req.Presign(15 * time.Second)
}

// redirectToDownload creates a presigned link that is valid for 15 seconds and redirects the
// client to this url
func redirectToDownload(w http.ResponseWriter, r *http.Request, file models.File, forceDownload bool) error {
	url, err := getPresignedUrl(file, forceDownload)
	if err != nil {
		return err
	}

	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
	return nil
}

// proxyDownload streams the file from S3 as a proxy, by downloading a presigned url
func proxyDownload(w http.ResponseWriter, file models.File, forceDownload bool) error {
	url, err := getPresignedUrl(file, forceDownload)
	if err != nil {
		return err
	}

	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	headers.Write(file, w, forceDownload)
	_, _ = io.Copy(w, resp.Body)
	return nil
}

func getTimeoutContext() (context.Context, context.CancelFunc) {
	ctx := context.Background()
	rContext, rCancel := context.WithTimeout(ctx, 5*time.Second)
	return rContext, func() {
		if rCancel != nil {
			rCancel()
		}
	}
}

// FileExists returns true if the object is stored in S3
func FileExists(file models.File) (bool, int64, error) {
	return fileExists(file.AwsBucket, file.SHA1)
}

func fileExists(bucket, filename string) (bool, int64, error) {
	sess := createSession()
	svc := s3.New(sess)

	ctx, cancelCtx := getTimeoutContext()
	defer cancelCtx()

	info, err := svc.HeadObjectWithContext(ctx, &s3.HeadObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(filename),
	})

	if err != nil {
		var aerr awserr.Error
		ok := errors.As(err, &aerr)
		if ok {
			if aerr.Code() == "NotFound" {
				return false, 0, nil
			}
		}
		if aerr.Code() == request.CanceledErrorCode {
			return false, 0, errors.New("Timeout - could not connect to " + *svc.Config.Endpoint)
		}
		return false, 0, err
	}
	return true, *info.ContentLength, nil
}

// DeleteObject deletes a file from S3
func DeleteObject(file models.File) (bool, error) {
	sess := createSession()
	svc := s3.New(sess)

	ctx, cancelCtx := getTimeoutContext()
	defer cancelCtx()

	_, err := svc.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(file.AwsBucket),
		Key:    aws.String(file.SHA1),
	})

	if err != nil {
		return false, err
	}
	return true, nil
}

// IsCorsCorrectlySet returns true if CORS rules allow download from Gokapi
func IsCorsCorrectlySet(bucket, gokapiUrl string) (bool, error) {
	sess := createSession()
	svc := s3.New(sess)
	input := &s3.GetBucketCorsInput{
		Bucket: aws.String(bucket),
	}

	ctx, cancelCtx := getTimeoutContext()
	defer cancelCtx()

	result, err := svc.GetBucketCorsWithContext(ctx, input)
	if err != nil {
		var aerr awserr.Error
		ok := errors.As(err, &aerr)
		if ok && aerr.Code() == "NoSuchCorsConfiguration" {
			return false, nil
		}
		return false, err
	}

	for _, rule := range result.CORSRules {
		for _, origin := range rule.AllowedOrigins {
			if *origin == "*" {
				return true, nil
			}
			if strings.HasPrefix(gokapiUrl, *origin) {
				return true, nil
			}
		}
	}
	return false, nil
}

// GetDefaultBucketName returns the default bucketname where new files are stored
func GetDefaultBucketName() string {
	return awsConfig.Bucket
}
