package hcvault

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/hashicorp/vault/api"
	"github.com/mitchellh/go-homedir"
	"github.com/sirupsen/logrus"

	"github.com/getsops/sops/v3/logging"
)

const (
	// KeyTypeIdentifier is the string used to identify a Vault MasterKey.
	KeyTypeIdentifier = "hc_vault"
	// SopsHCVaultAllowlist can be set as an environment variable with a string list
	// of age keys as value.
	SopsHCVaultAllowlist = "SOPS_HC_VAULT_ALLOWLIST"
	// Special value for allowlist that allows all hosts
	AllowlistAllHosts = "all"
	// Special value for allowlist that allows no hosts
	AllowlistNoHosts = "none"
	// Default value of allowlist. Should eventually be changed to "none".
	AllowlistDefault = AllowlistAllHosts
)

type allowList struct {
	All  bool
	URIs []string
}

func (al *allowList) Allows(address string) bool {
	if al.All {
		return true
	}
	if !strings.HasSuffix(address, "/") {
		address = address + "/"
	}
	for _, uri := range al.URIs {
		if strings.HasPrefix(address, uri) {
			return true
		}
	}
	return false
}

func parseAllowlistString(allowlistStr string) (allowList, error) {
	switch allowlistStr {
	case AllowlistAllHosts:
		return allowList{
			All:  true,
			URIs: nil,
		}, nil
	case AllowlistNoHosts:
		return allowList{
			All:  false,
			URIs: nil,
		}, nil
	}
	uris := strings.Split(allowlistStr, ",")
	for idx, uri := range uris {
		uri = strings.Trim(uri, " ")
		if uri == "" {
			return allowList{}, fmt.Errorf("%s's entry %d is empty", SopsHCVaultAllowlist, idx+1)
		}
		if !strings.HasSuffix(uri, "/") {
			uri = uri + "/"
		}
		uris[idx] = uri
	}
	return allowList{
		All:  false,
		URIs: uris,
	}, nil
}

func getAllowlist() (allowList, error) {
	var allowlistStr = AllowlistDefault
	if allowlist, ok := os.LookupEnv(SopsHCVaultAllowlist); ok && len(allowlist) > 0 {
		allowlistStr = allowlist
	}
	return parseAllowlistString(allowlistStr)
}

func init() {
	log = logging.NewLogger("VAULT_TRANSIT")
}

var (
	// log is the global logger for any Vault Transit MasterKey.
	log *logrus.Logger
	// vaultTTL is the duration after which a MasterKey requires rotation.
	vaultTTL = time.Hour * 24 * 30 * 6
	// defaultTokenFile is the name of the file in the user's home directory
	// where a Vault token is expected to be stored.
	defaultTokenFile = ".vault-token"
)

// Token used for authenticating towards a Vault server.
type Token string

// ApplyToMasterKey configures the token on the provided key.
func (t Token) ApplyToMasterKey(key *MasterKey) {
	key.token = string(t)
}

// MasterKey is a Vault Transit backend path used to Encrypt and Decrypt
// SOPS' data key.
type MasterKey struct {
	// VaultAddress is the address of the Vault server.
	VaultAddress string
	// EnginePath is the path to the Vault Transit Secret engine relative
	// to the VaultAddress.
	EnginePath string
	// KeyName is the name of the key in the Vault Transit engine.
	KeyName string
	// EncryptedKey contains the SOPS data key encrypted with the Vault Transit
	// key.
	EncryptedKey string
	// CreationDate of the MasterKey, used to determine if the EncryptedKey
	// needs rotation.
	CreationDate time.Time

	// token is the token used for authenticating against the VaultAddress
	// server. It can be injected by a (local) keyservice.KeyServiceServer
	// Token.ApplyToMasterKey. If empty, the default client configuration
	// is used, before falling back to the token stored in defaultTokenFile.
	token string
	// httpClient is used to override the default HTTP client used by the Vault client.
	httpClient *http.Client
}

// NewMasterKeysFromURIs creates a list of MasterKeys from a list of Vault
// URIs.
func NewMasterKeysFromURIs(uris string) ([]*MasterKey, error) {
	var keys []*MasterKey
	if uris == "" {
		return keys, nil
	}
	for _, uri := range strings.Split(uris, ",") {
		if uri == "" {
			continue
		}
		key, err := NewMasterKeyFromURI(uri)
		if err != nil {
			return nil, err
		}
		keys = append(keys, key)
	}
	return keys, nil
}

// NewMasterKeyFromURI obtains the Vault address, Transit backend path and the
// key name from the full URI of the key.
func NewMasterKeyFromURI(uri string) (*MasterKey, error) {
	var key *MasterKey
	if uri == "" {
		return key, nil
	}
	u, err := url.Parse(uri)
	if err != nil {
		return nil, err
	}
	if u.Scheme == "" {
		return nil, fmt.Errorf("missing scheme in Vault URL (should be like this: +"+
			"https://vault.example.com:8200/v1/transit/keys/keyName), got: %v", uri)
	}
	enginePath, keyName, err := engineAndKeyFromPath(u.RequestURI())
	if err != nil {
		return nil, err
	}
	u.Path = ""
	return NewMasterKey(u.String(), enginePath, keyName), nil

}

// NewMasterKey creates a new MasterKey from a Vault address, Transit backend
// path and a key name.
func NewMasterKey(address, enginePath, keyName string) *MasterKey {
	key := &MasterKey{
		VaultAddress: address,
		EnginePath:   enginePath,
		KeyName:      keyName,
		CreationDate: time.Now().UTC(),
	}
	return key
}

// HTTPClient is a wrapper around http.Client used for configuring the
// Vault client.
type HTTPClient struct {
	hc *http.Client
}

// NewHTTPClient creates a new HTTPClient with the provided http.Client.
func NewHTTPClient(hc *http.Client) *HTTPClient {
	return &HTTPClient{hc: hc}
}

// ApplyToMasterKey configures the HTTP client on the provided key.
func (h HTTPClient) ApplyToMasterKey(key *MasterKey) {
	key.httpClient = h.hc
}

// Encrypt takes a SOPS data key, encrypts it with Vault Transit, and stores
// the result in the EncryptedKey field.
//
// Consider using EncryptContext instead.
func (key *MasterKey) Encrypt(dataKey []byte) error {
	return key.EncryptContext(context.Background(), dataKey)
}

// EncryptContext takes a SOPS data key, encrypts it with Vault Transit, and stores
// the result in the EncryptedKey field.
func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error {
	fullPath := key.encryptPath()

	client, err := vaultClient(key.VaultAddress, key.token, key.httpClient)
	if err != nil {
		log.WithField("Path", fullPath).Info("Encryption failed")
		return err
	}

	secret, err := client.Logical().WriteWithContext(ctx, fullPath, encryptPayload(dataKey))
	if err != nil {
		log.WithField("Path", fullPath).Info("Encryption failed")
		return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err)
	}
	encryptedKey, err := encryptedKeyFromSecret(secret)
	if err != nil {
		log.WithField("Path", fullPath).Info("Encryption failed")
		return fmt.Errorf("failed to encrypt sops data key to Vault transit backend '%s': %w", fullPath, err)
	}

	key.EncryptedKey = encryptedKey
	log.WithField("Path", fullPath).Info("Encryption successful")
	return nil
}

// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
	if key.EncryptedKey == "" {
		return key.Encrypt(dataKey)
	}
	return nil
}

// EncryptedDataKey returns the encrypted data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
	return []byte(key.EncryptedKey)
}

// SetEncryptedDataKey sets the encrypted data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
	key.EncryptedKey = string(enc)
}

// Decrypt decrypts the EncryptedKey field with Vault Transit and returns the result.
//
// Consider using DecryptContext instead.
func (key *MasterKey) Decrypt() ([]byte, error) {
	return key.DecryptContext(context.Background())
}

// DecryptContext decrypts the EncryptedKey field with Vault Transit and returns the result.
func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) {
	fullPath := key.decryptPath()

	client, err := vaultClient(key.VaultAddress, key.token, key.httpClient)
	if err != nil {
		log.WithField("Path", fullPath).Info("Decryption failed")
		return nil, err
	}

	secret, err := client.Logical().WriteWithContext(ctx, fullPath, decryptPayload(key.EncryptedKey))
	if err != nil {
		log.WithField("Path", fullPath).Info("Decryption failed")
		return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err)
	}
	dataKey, err := dataKeyFromSecret(secret)
	if err != nil {
		log.WithField("Path", fullPath).Info("Decryption failed")
		return nil, fmt.Errorf("failed to decrypt sops data key from Vault transit backend '%s': %w", fullPath, err)
	}

	log.WithField("Path", fullPath).Info("Decryption successful")
	return dataKey, nil
}

// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
	// TODO: manage rewrapping https://www.vaultproject.io/api/secret/transit/index.html#rewrap-data
	return time.Since(key.CreationDate) > (vaultTTL)
}

// ToString converts the key to a string representation.
func (key *MasterKey) ToString() string {
	return fmt.Sprintf("%s/v1/%s/keys/%s", key.VaultAddress, key.EnginePath, key.KeyName)
}

// ToMap converts the MasterKey to a map for serialization purposes.
func (key MasterKey) ToMap() map[string]interface{} {
	out := make(map[string]interface{})
	out["vault_address"] = key.VaultAddress
	out["key_name"] = key.KeyName
	out["engine_path"] = key.EnginePath
	out["enc"] = key.EncryptedKey
	out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
	return out
}

// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
	return KeyTypeIdentifier
}

// encryptPath returns the path for Encrypt requests.
func (key *MasterKey) encryptPath() string {
	return path.Join(key.EnginePath, "encrypt", key.KeyName)
}

// decryptPath returns the path for Decrypt requests.
func (key *MasterKey) decryptPath() string {
	return path.Join(key.EnginePath, "decrypt", key.KeyName)
}

// encryptPayload returns the payload for an encrypt request of the dataKey.
func encryptPayload(dataKey []byte) map[string]interface{} {
	encoded := base64.StdEncoding.EncodeToString(dataKey)
	return map[string]interface{}{
		"plaintext": encoded,
	}
}

// encryptedKeyFromSecret attempts to extract the encrypted key from the data
// of the provided secret.
func encryptedKeyFromSecret(secret *api.Secret) (string, error) {
	if secret == nil || secret.Data == nil {
		return "", fmt.Errorf("transit backend is empty")
	}
	encrypted, ok := secret.Data["ciphertext"]
	if !ok {
		return "", fmt.Errorf("no encrypted data")
	}
	encryptedKey, ok := encrypted.(string)
	if !ok {
		return "", fmt.Errorf("encrypted ciphertext cannot be cast to string")
	}
	return encryptedKey, nil
}

// decryptPayload returns the payload for a decrypt request of the
// encryptedKey.
func decryptPayload(encryptedKey string) map[string]interface{} {
	return map[string]interface{}{
		"ciphertext": encryptedKey,
	}
}

// dataKeyFromSecret attempts to extract the data key from the data of the
// provided secret.
func dataKeyFromSecret(secret *api.Secret) ([]byte, error) {
	if secret == nil || secret.Data == nil {
		return nil, fmt.Errorf("transit backend is empty")
	}
	decrypted, ok := secret.Data["plaintext"]
	if !ok {
		return nil, fmt.Errorf("no decrypted data")
	}
	plaintext, ok := decrypted.(string)
	if !ok {
		return nil, fmt.Errorf("decrypted plaintext data cannot be cast to string")
	}
	dataKey, err := base64.StdEncoding.DecodeString(plaintext)
	if err != nil {
		return nil, fmt.Errorf("cannot decode base64 plaintext into data key bytes")
	}
	return dataKey, nil
}

// vaultClient returns a new Vault client, configured with the given address
// and token.
func vaultClient(address, token string, hc *http.Client) (*api.Client, error) {
	cfg := api.DefaultConfig()
	cfg.Address = address

	allowlist, err := getAllowlist()
	if err != nil {
		return nil, err
	}
	if !allowlist.Allows(address) {
		return nil, fmt.Errorf("Allowlist does not allow %s", address)
	}

	if hc != nil {
		cfg.HttpClient = hc
	}

	client, err := api.NewClient(cfg)
	if err != nil {
		return nil, fmt.Errorf("cannot create Vault client: %w", err)
	}

	if token != "" {
		client.SetToken(token)
	}
	// Provided token takes precedence over the user's token.
	if client.Token() == "" {
		if token, err = userVaultToken(); err != nil {
			return nil, fmt.Errorf("cannot get Vault token: %w", err)
		}
		if token != "" {
			client.SetToken(token)
		}
	}

	return client, nil
}

// userVaultToken returns the token from `$HOME/.vault-token` if the file
// exists. It returns an error if the file exists but cannot be read from.
// If the file does not exist, it returns an empty string.
func userVaultToken() (string, error) {
	homePath, err := homedir.Dir()
	if err != nil {
		return "", fmt.Errorf("error getting user's home directory: %w", err)
	}
	tokenPath := filepath.Join(homePath, defaultTokenFile)

	f, err := os.Open(tokenPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", nil
		}
		return "", err
	}
	defer f.Close()

	buf := bytes.NewBuffer(nil)
	if _, err := io.Copy(buf, f); err != nil {
		return "", err
	}
	return strings.TrimSpace(buf.String()), nil
}

// engineAndKeyFromPath returns the engine path and key name from the full
// path, or an error.
func engineAndKeyFromPath(fullPath string) (enginePath, keyName string, err error) {
	// Running vault behind a reverse proxy with longer URLs seems not to be
	// supported by the Vault client API. Check for this here.
	// TODO(hidde): this may no longer be necessary with newer Vault versions,
	//  but needs to be confirmed.
	if re := regexp.MustCompile(`/[^/]+/v[\d]+/[^/]+/[^/]+/[^/]+`); re.Match([]byte(fullPath)) {
		err = fmt.Errorf("running Vault with a prefixed URL is not supported! (Format has to be like " +
			"https://vault.example.com:8200/v1/transit/keys/keyName)")
		return
	} else if re := regexp.MustCompile(`/v[\d]+/[^/]+/[^/]+/[^/]+`); !re.Match([]byte(fullPath)) {
		err = fmt.Errorf("vault path does not seem to be formatted correctly: (eg. " +
			"https://vault.example.com:8200/v1/transit/keys/keyName)")
		return
	}

	fullPath = strings.Trim(fullPath, "/")
	dirs := strings.Split(fullPath, "/")

	keyName = dirs[len(dirs)-1]
	enginePath = path.Join(dirs[1 : len(dirs)-2]...)
	return
}
