package main

/**
Secret2file fetches a secret's key-value content from AWS Secrets Manager
and stores it as a file using the specified format.

Application requires the next CLI arguments:
  -secret string
        Secret's name in Secrets Manager.
  -file string
        File to write secret's content to.
  -format string
        File's line format. (default "{k}:{v}\n")
*/

import (
	"crypto/md5"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"sort"
	"strings"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
)

func main() {
	secret, file, format := parseCliArgs()

	log.SetFlags(0)
	log.SetOutput(new(logWriter))

	log.Println(fmt.Sprintf("Fetching %s secret from Secrets Manager", secret))
	value := getSecret(secret)

	log.Println(fmt.Sprintf("Processing %s secret's content", secret))
	content := buildContent(format, value)

	log.Println(fmt.Sprintf("Comparing %s secret's content with %s file", secret, file))
	if getStringHash(content) == getFileHash(file) {
		log.Println(fmt.Sprintf("No changes detected for %s secret, exiting", secret))
		return
	}

	log.Println(fmt.Sprintf("Changes found, updating %s file", file))
	updateFile(file, content)
	log.Println(fmt.Sprintf("Done updating %s file", file))
}

func parseCliArgs() (string, string, string) {
	var secret, file, format string

	flag.StringVar(&secret, "secret", "", "Secret's name in Secrets Manager.")
	flag.StringVar(&file, "file", "", "File to write secret's content to.")
	flag.StringVar(&format, "format", "{k}:{v}\n", "File's line format.")
	flag.Parse()

	if secret == "" || file == "" {
		fmt.Println("Error: Not enough arguments.")
		fmt.Println("Usage example:")
		flag.PrintDefaults()
		os.Exit(1)
	}

	return secret, file, format
}

func getEnv(key, def string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}

	return def
}

func getSecret(key string) map[string]string {
	region := getEnv("AWS_DEFAULT_REGION", "us-east-1")

	// Init AWS session.
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String(region),
	})

	if err != nil {
		log.Fatalln(fmt.Errorf("Error: Can't initialize AWS session: %w", err))
	}

	// Fetch secret from Secrets Manager.
	sm := secretsmanager.New(sess)

	in := &secretsmanager.GetSecretValueInput{
		SecretId: aws.String(key),
	}

	out, err := sm.GetSecretValue(in)
	if err != nil {
		log.Fatalln(fmt.Errorf("Error: Can't fetch %s secret from Secrets Manager: %w", key, err))
	}

	// Convert secret's content from JSON to map.
	value := *out.SecretString

	var kv map[string]string
	err = json.Unmarshal([]byte(value), &kv)
	if err != nil {
		log.Fatalln(fmt.Errorf("Error: Can't convert %s secret's content to the key-value map: %w", key, err))
	}

	return kv
}

func buildContent(format string, value map[string]string) string {
	var content string

	keys := make([]string, 0, len(value))
	for k := range value {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		s := format
		s = strings.ReplaceAll(s, "{k}", k)
		s = strings.ReplaceAll(s, "{v}", value[k])
		content = content + s
	}

	return content
}

func getStringHash(str string) string {
	h := md5.New()
	h.Write([]byte(str))

	return fmt.Sprintf("%x", h.Sum(nil))
}

func getFileHash(file string) string {
	f, err := os.Open(file)
	if err != nil {
		log.Println(fmt.Errorf("Warning: Can't open %s file: %w", file, err))
		return ""
	}
	defer f.Close()

	h := md5.New()
	if _, err := io.Copy(h, f); err != nil {
		log.Fatalln(fmt.Errorf("Error: Can't read %s file content: %w", file, err))
	}

	return fmt.Sprintf("%x", h.Sum(nil))
}

func updateFile(file, content string) {
	err := os.WriteFile(file, []byte(content), 0644)
	if err != nil {
		log.Fatalln(fmt.Errorf("Error: Can't write %s file: %w", file, err))
	}
}
