// Package pkg provides functionality similar to the AWS CLI cloudformation package command
// but has greater flexibility, allowing content to be included anywhere in a template
//
// To include content into your templates, use any of the following either as YAML tags
// or as one-property objects, much as AWS instrinsic functions are used, e.g. "Fn::Join"
//
// `Rain::Include`: insert the content of the file into the template directly. The file must be in YAML or JSON format.
// `Rain::Env`: inserts environmental variable value into the template as a string. Variable must be set.
// `Rain::Embed`: insert the content of the file as a string
// `Rain::S3Http`: uploads the file or directory (zipping it first) to S3 and returns the HTTP URI (i.e. `https://bucket.s3.region.amazonaws.com/key`)
// `Rain::S3`: a string value uploads the file or directory (zipping it first) to S3 and returns the S3 URI (i.e. `s3://bucket/key`)
// `Rain::S3`: an object with the following properties
//
//	`Path`: path to the file or directory. If a directory is supplied, it will be zipped before uploading to S3
//	`BucketProperty`: Name of returned property that will contain the bucket name
//	`KeyProperty`: Name of returned property that will contain the object key
//	`VersionProperty`: (optional) Name of returned property that will contain the object version
//	`Extension`: (optional) Extension appended to the end of the Object Key in S3
package pkg

import (
	"archive/zip"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/aws-cloudformation/rain/internal/aws"
	"github.com/aws-cloudformation/rain/internal/aws/s3"
	"github.com/aws-cloudformation/rain/internal/config"
	"gopkg.in/yaml.v3"
)

type s3Path struct {
	bucket string
	key    string
	region string
}

func (s *s3Path) URI() string {
	return fmt.Sprintf("s3://%s/%s", s.bucket, s.key)
}

func (s *s3Path) HTTP() string {
	return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.bucket, s.region, s.key)
}

var uploads = map[string]*s3Path{}

func zipPath(root string) (string, error) {
	tmpFile, err := os.CreateTemp(os.TempDir(), "*.zip")
	if err != nil {
		return "", err
	}
	defer tmpFile.Close()

	w := zip.NewWriter(tmpFile)
	defer w.Close()

	zRoot := root
	info, err := os.Stat(zRoot)
	if err != nil {
		return "", err
	}
	if !info.IsDir() {
		zRoot = filepath.Dir(zRoot)
	}

	err = filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.Mode().IsRegular() {
			return nil
		}

		in, err := os.Open(path)
		if err != nil {
			return err
		}
		defer in.Close()

		zPath, err := filepath.Rel(zRoot, path)
		if err != nil {
			return err
		}

		zPath = filepath.ToSlash(zPath)

		fh, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}

		fh.Name = zPath
		fh.Method = zip.Deflate

		out, err := w.CreateHeader(fh)
		if err != nil {
			return err
		}

		_, err = io.Copy(out, in)
		return err
	})

	return tmpFile.Name(), err
}

// Upload a file or directory to S3.
// If path is a directory, it will be zipped first.
func upload(root, path string, force bool, extension string) (*s3Path, error) {
	if !filepath.IsAbs(path) {
		path = filepath.Join(root, path)
		if abs, err := filepath.Abs(path); err == nil {
			path = abs
		}
	}

	artifactName := path
	if force {
		artifactName = "zip:" + artifactName
	}

	if result, ok := uploads[artifactName]; ok {
		config.Debugf("Using existing upload for: %s\n", path)
		return result, nil
	}

	info, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	if info.IsDir() || force {
		// Zip it!
		zipped, err := zipPath(path)
		if err != nil {
			return nil, err
		}
		config.Debugf("Zipped %s as %s\n", path, zipped)
		path = zipped
	}

	config.Debugf("Uploading: %s\n", path)

	content, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	bucket := s3.RainBucket(false)
	key, err := s3.Upload(bucket, content, extension)

	uploads[artifactName] = &s3Path{
		bucket: bucket,
		key:    key,
		region: aws.Config().Region,
	}

	return uploads[artifactName], err
}

func expectString(n *yaml.Node) (string, error) {
	if len(n.Content) != 2 {
		return "", fmt.Errorf("expected a mapping node")
	}

	if n.Content[1].Kind != yaml.ScalarNode {
		return "", fmt.Errorf("expected a scalar value")
	}

	return n.Content[1].Value, nil
}

func expectFile(n *yaml.Node, root string) ([]byte, string, error) {

	//config.Debugf("expectFile: %v", node.ToJson(n))

	path, err := expectString(n)
	if err != nil {
		return nil, "", err
	}

	//config.Debugf("root: %v, path: %v", root, path)

	if !filepath.IsAbs(path) {
		path = filepath.Join(root, path)
	}

	//config.Debugf("path: %v", path)

	info, err := os.Stat(path)
	if err != nil {
		return nil, path, err
	}

	if info.IsDir() {
		return nil, path, fmt.Errorf("'%s' is a directory", path)
	}

	content, err := os.ReadFile(path)

	return content, path, err
}

// Convert a comma delimted string to a sequence node
func ConvertCsvToSequence(csv string) *yaml.Node {
	retval := &yaml.Node{
		Kind:    yaml.SequenceNode,
		Tag:     "!!seq",
		Content: make([]*yaml.Node, 0),
	}
	tokens := strings.Split(csv, ",")
	for _, t := range tokens {
		retval.Content = append(retval.Content,
			&yaml.Node{Kind: yaml.ScalarNode, Value: strings.TrimSpace(t)})
	}
	return retval
}

/*
func expectProps(n *yaml.Node, names ...string) (map[string]string, bool) {
	if len(n.Content) != 2 {
		return nil, false
	}

	if n.Content[1].Kind != yaml.MappingNode {
		return nil, false
	}

	var out map[string]interface{}

	err := n.Content[1].Decode(&out)
	if err != nil {
		return nil, false
	}

	props := make(map[string]string)

	for _, name := range names {
		value, exists := out[name]
		if !exists {
			return nil, false
		}

		str, ok := value.(string)
		if !ok {
			return nil, false
		}

		props[name] = str
	}

	return props, true
}
*/
