package adapters

import (
	"archive/zip"
	"bytes"
	"context"
	"devsecops-quickstart/opa-scan/pkg/utils"
	"errors"
	"io/ioutil"
	"log"
	"net/url"
	"regexp"
	"strings"
	"sync"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

var (
	s3Svc *S3Addapter
	once  sync.Once
)

type S3Addapter struct {
	AddapterInterface
	s3Client *s3.Client
}

// New Creates a new S3Addapter Object
func NewS3Addapter() *S3Addapter {
	// method to ensure singleton behaviour
	once.Do(func() {
		cfg, err := config.LoadDefaultConfig(context.TODO())
		if err != nil {
			log.Fatalf("unable to load SDK config, %v", err)
		}

		svc := s3.NewFromConfig(cfg)

		s3Svc = &S3Addapter{
			s3Client: svc,
		}
	})

	return s3Svc
}
func (a S3Addapter) validateS3Url(path string) (*url.URL, error) {
	u, err := url.Parse(path)
	if err != nil || u.Scheme != "s3" {
		return u, errors.New("Url " + path + " is not valid S3 url.")
	}

	u.Path = strings.Replace(u.Path, "/", "", 1)

	return u, nil
}

func (a S3Addapter) PathExists(path string) (bool, error) {
	// Validate S3 url
	u, err := a.validateS3Url(path)
	if err != nil {
		return false, err
	}

	resp, err := a.s3Client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
		Bucket: aws.String(u.Host),
		Prefix: aws.String(u.Path),
	})

	return len(resp.Contents) > 0, nil
}

func (a S3Addapter) Read(filePath string, experesion string, walk bool) ([]utils.File, error) {
	inputFiles := []utils.File{}

	// Validate Regular expresion
	libRegEx, err := regexp.Compile(experesion)
	if err != nil {
		return inputFiles, err
	}

	// Validate S3 url
	u, err := a.validateS3Url(filePath)

	resp, err := a.s3Client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
		Bucket: aws.String(u.Host),
		Prefix: aws.String(u.Path),
		//Delimiter: aws.String("/"),
		//StartAfter: aws.String(key),
	})
	if err != nil {
		return inputFiles, err
	}

	for _, item := range resp.Contents {
		files, err := a.readContent(u.Host, *item.Key)
		if err != nil {
			return inputFiles, err
		}

		for _, file := range files {
			if libRegEx.MatchString(file.FilePath) {
				inputFiles = append(inputFiles, file)
			}
		}
	}

	if len(inputFiles) == 0 {
		return inputFiles, errors.New("S3 object " + filePath + " does not exist.")
	}

	return inputFiles, nil
}

func (a S3Addapter) readContent(bucket string, key string) ([]utils.File, error) {
	ctx := context.Background()
	req, err := a.s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return []utils.File{}, err
	}

	s3objectBytes, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return []utils.File{}, err
	}

	if *req.ContentType != "application/zip" {
		file := utils.File{
			Type:     "S3",
			FilePath: "s3://" + bucket + "/" + key,
			Data:     string(s3objectBytes),
		}

		return []utils.File{file}, nil
	}

	zipReader, err := zip.NewReader(bytes.NewReader(s3objectBytes), int64(len(s3objectBytes)))
	if err != nil {
		return []utils.File{}, err
	}

	// Read all the files from zip archive
	files := []utils.File{}
	for _, zipFile := range zipReader.File {
		zipFileBytes, err := readZipFile(zipFile)
		if err != nil {
			return []utils.File{}, err
		}

		file := utils.File{
			Type:     "S3",
			FilePath: "s3://" + bucket + "/" + key + "/" + zipFile.FileHeader.Name,
			Data:     string(zipFileBytes),
		}
		files = append(files, file)
	}

	return files, nil
}

func readZipFile(zf *zip.File) ([]byte, error) {
	f, err := zf.Open()
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return ioutil.ReadAll(f)
}

func (a S3Addapter) Write(location map[string]string, content string) error {
	ctx := context.Background()

	_, err := a.s3Client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:             aws.String(location["bucket"]),
		Key:                aws.String(location["key"]),
		Body:               strings.NewReader(content), // bytes.NewReader(buffer),
		ContentDisposition: aws.String("attachment"),
		// ContentLength:      aws.Int64(int64(len(buffer))),
		// ContentType:        aws.String(http.DetectContentType(buffer)),
		// ServerSideEncryption: aws.String("AES256"),
	})

	return err
}
