package shelldriver

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"sort"
	"strings"
)

var (

	// errMissingConfig indicates that one or more of the external actions are not configured
	errMissingConfig = errors.New("missing config value")

	// errNoSecretData indicates that there is not data associated with an id
	errNoSecretData = errors.New("no secret data with ID")

	// errInvalidKey indicates that something about your key is wrong
	errInvalidKey = errors.New("invalid key")
)

type driverConfig struct {
	// DeleteCommand contains a shell command that deletes a secret.
	// The secret id is provided as environment variable SECRET_ID
	DeleteCommand string
	// ListCommand contains a shell command that lists all secrets.
	// The output is expected to be one id per line
	ListCommand string
	// LookupCommand contains a shell command that retrieves a secret.
	// The secret id is provided as environment variable SECRET_ID
	LookupCommand string
	// StoreCommand contains a shell command that stores a secret.
	// The secret id is provided as environment variable SECRET_ID
	// The secret value itself is provided over stdin
	StoreCommand string
}

func (cfg *driverConfig) ParseOpts(opts map[string]string) error {
	for key, value := range opts {
		switch key {
		case "delete":
			cfg.DeleteCommand = value
		case "list":
			cfg.ListCommand = value
		case "lookup":
			cfg.LookupCommand = value
		case "store":
			cfg.StoreCommand = value
		default:
			return fmt.Errorf("invalid shell driver option: %q", key)
		}
	}
	if cfg.DeleteCommand == "" ||
		cfg.ListCommand == "" ||
		cfg.LookupCommand == "" ||
		cfg.StoreCommand == "" {

		return errMissingConfig
	}
	return nil
}

// Driver is the passdriver object
type Driver struct {
	driverConfig
}

// NewDriver creates a new secret driver.
func NewDriver(opts map[string]string) (*Driver, error) {
	cfg := &driverConfig{}
	if err := cfg.ParseOpts(opts); err != nil {
		return nil, err
	}

	driver := &Driver{
		driverConfig: *cfg,
	}

	return driver, nil
}

// List returns all secret IDs
func (d *Driver) List() (secrets []string, err error) {
	cmd := exec.CommandContext(context.TODO(), "/bin/sh", "-c", d.ListCommand)
	cmd.Env = os.Environ()
	cmd.Stderr = os.Stderr

	buf := &bytes.Buffer{}
	cmd.Stdout = buf

	err = cmd.Run()
	if err != nil {
		return nil, err
	}

	parts := bytes.Split(buf.Bytes(), []byte("\n"))
	for _, part := range parts {
		id := strings.Trim(string(part), " \r\n")
		if len(id) > 0 {
			secrets = append(secrets, id)
		}
	}
	sort.Strings(secrets)

	return secrets, nil
}

// Lookup returns the bytes associated with a secret ID
func (d *Driver) Lookup(id string) ([]byte, error) {
	if strings.Contains(id, "..") {
		return nil, errInvalidKey
	}

	cmd := exec.CommandContext(context.TODO(), "/bin/sh", "-c", d.LookupCommand)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SECRET_ID="+id)
	cmd.Stderr = os.Stderr

	buf := &bytes.Buffer{}
	cmd.Stdout = buf

	err := cmd.Run()
	if err != nil {
		return nil, fmt.Errorf("%s: %w", id, errNoSecretData)
	}
	return buf.Bytes(), nil
}

// Store saves the bytes associated with an ID. An error is returned if the ID already exists
func (d *Driver) Store(id string, data []byte) error {
	if strings.Contains(id, "..") {
		return errInvalidKey
	}

	cmd := exec.CommandContext(context.TODO(), "/bin/sh", "-c", d.StoreCommand)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SECRET_ID="+id)

	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout
	cmd.Stdin = bytes.NewReader(data)

	return cmd.Run()
}

// Delete removes the secret associated with the specified ID.  An error is returned if no matching secret is found.
func (d *Driver) Delete(id string) error {
	if strings.Contains(id, "..") {
		return errInvalidKey
	}

	cmd := exec.CommandContext(context.TODO(), "/bin/sh", "-c", d.DeleteCommand)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SECRET_ID="+id)

	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

	err := cmd.Run()
	if err != nil {
		return fmt.Errorf("%s: %w", id, errNoSecretData)
	}

	return nil
}
