package ec2macosinit

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// SSHKeysModule contains all necessary configuration fields for running an SSH Keys module.
type SSHKeysModule struct {
	DedupKeys               bool     `toml:"DedupKeys"`
	GetIMDSOpenSSHKey       bool     `toml:"GetIMDSOpenSSHKey"`
	StaticOpenSSHKeys       []string `toml:"StaticOpenSSHKeys"`
	OverwriteAuthorizedKeys bool     `toml:"OverwriteAuthorizedKeys"`
	User                    string   `toml:"User"`
}

// Do for the SSHKeysModule does some brief validation, gets the IMDS key (if configured), appends static keys (if
// configured), and then writes them to the authorized_keys file for the user.
func (c *SSHKeysModule) Do(ctx *ModuleContext) (message string, err error) {
	// If we're not getting the key from IMDS and there are no keys provided, there's nothing to do here
	if !c.GetIMDSOpenSSHKey && len(c.StaticOpenSSHKeys) == 0 {
		return "nothing to do", nil
	}

	// If user is undefined, default to ec2-user
	if c.User == "" {
		c.User = "ec2-user"
	}

	// Verify that user exists
	exists, err := userExists(c.User)
	if err != nil {
		return "", fmt.Errorf("ec2macosinit: error while checking if user %s exists: %s\n", c.User, err)
	}
	if !exists { // if the user doesn't exist, error out
		return "", fmt.Errorf("ec2macosinit: user %s does not exist\n", c.User)
	}

	// Set directory and authorized_keys file
	authorizedKeysDir := filepath.Join("/Users", c.User, ".ssh")
	authorizedKeysFile := filepath.Join(authorizedKeysDir, "authorized_keys")
	if _, err := os.Stat(authorizedKeysDir); os.IsNotExist(err) { // If directory doesn't exist, create it
		err := os.MkdirAll(authorizedKeysDir, 0700)
		if err != nil {
			return "", fmt.Errorf("ec2macosinit: unable to create directory [%s]: %s\n", authorizedKeysDir, err)
		}
	}

	// Get IMDS key
	keySet := map[string]struct{}{}
	if c.GetIMDSOpenSSHKey {
		// Get IMDS property "meta-data/public-keys/0/openssh-key"
		imdsKey, respCode, err := ctx.IMDS.getIMDSProperty("meta-data/public-keys/0/openssh-key")
		if err != nil {
			return "", fmt.Errorf("ec2macosinit: error getting openSSH key from IMDS: %s\n", err)
		}
		if respCode == 200 { // 200 = ok
			keySet[strings.TrimSpace(imdsKey)] = struct{}{}
		} else if respCode != 404 { // 404 is the only other allowable response code as it indicates no key was provided - if not 404 error out
			return "", fmt.Errorf("ec2macosinit: received an unexpected response code from IMDS: %d - %s\n", respCode, err)
		}
	}

	// Add all unique provided static keys
	if len(c.StaticOpenSSHKeys) > 0 {
		for _, k := range c.StaticOpenSSHKeys {
			keySet[strings.TrimSpace(k)] = struct{}{}
		}
	}

	// If authorized_keys file exists and deduplication is requested, read file and add to set
	if _, err := os.Stat(authorizedKeysFile); err == nil && c.DedupKeys {
		file, err := os.Open(authorizedKeysFile)
		if err != nil {
			return "", fmt.Errorf("ec2macosinit: unable to open %s: %s\n", authorizedKeysFile, err)
		}
		defer file.Close()

		// Read file and add each line to set
		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			keySet[strings.TrimSpace(scanner.Text())] = struct{}{}
		}
		if err := scanner.Err(); err != nil {
			return "", fmt.Errorf("ec2macosinit: error while reading %s: %s\n", authorizedKeysFile, err)
		}

		// Set OverwriteAuthorizedKeys to true so that duplicate keys are overwritten
		c.OverwriteAuthorizedKeys = true
	}

	// Check if there's anything else to do
	if len(keySet) == 0 && !c.OverwriteAuthorizedKeys {
		return "no keys found and not overwriting authorized_keys", nil
	}

	// Add all keys to a slice
	var keys []string
	for k := range keySet {
		keys = append(keys, k)
	}

	// Write to authorized_keys file
	var f *os.File
	if !c.OverwriteAuthorizedKeys {
		// Append to authorized_keys
		f, err = os.OpenFile(authorizedKeysFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	} else {
		// Overwrite (truncate) authorized_keys
		f, err = os.OpenFile(authorizedKeysFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600)
	}
	if err != nil {
		f.Close()
		return "", fmt.Errorf("ec2macosinit: error while opening authorized_keys file: %s\n", err)
	}
	if _, err := f.WriteString(strings.Join(keys, "\n") + "\n"); err != nil {
		return "", fmt.Errorf("ec2macosinit: error while writing to authorized_keys file: %s\n", err)
	}
	f.Close()

	// Get UID and GID for user
	uid, gid, err := getUIDandGID(c.User)
	if err != nil && c.User == "ec2-user" {
		// Use default values for ec2-user
		uid = 501
		gid = 20
	} else if err != nil {
		return "", fmt.Errorf("ec2macosinit: error while getting user info: %s\n", err)
	}

	// Fix file ownership and directory permissions
	err = os.Chown(authorizedKeysDir, uid, gid)
	if err != nil {
		return "", fmt.Errorf("ec2macosinit: unable to change ownership of .ssh directory: %s\n", err)
	}
	err = os.Chown(authorizedKeysFile, uid, gid)
	if err != nil {
		return "", fmt.Errorf("ec2macosinit: unable to change ownership of authorized_keys file: %s\n", err)
	}

	return fmt.Sprintf("successfully added %d keys to authorized_users", len(keys)), nil
}
