// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package approle

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"

	hclog "github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/command/agentproxyshared/auth"
)

type approleMethod struct {
	logger    hclog.Logger
	mountPath string

	roleIDFilePath                 string
	secretIDFilePath               string
	cachedRoleID                   string
	cachedSecretID                 string
	removeSecretIDFileAfterReading bool
	secretIDResponseWrappingPath   string
}

func NewApproleAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
	if conf == nil {
		return nil, errors.New("empty config")
	}
	if conf.Config == nil {
		return nil, errors.New("empty config data")
	}

	a := &approleMethod{
		logger:                         conf.Logger,
		mountPath:                      conf.MountPath,
		removeSecretIDFileAfterReading: true,
	}

	roleIDFilePathRaw, ok := conf.Config["role_id_file_path"]
	if !ok {
		return nil, errors.New("missing 'role_id_file_path' value")
	}
	a.roleIDFilePath, ok = roleIDFilePathRaw.(string)
	if !ok {
		return nil, errors.New("could not convert 'role_id_file_path' config value to string")
	}
	if a.roleIDFilePath == "" {
		return nil, errors.New("'role_id_file_path' value is empty")
	}

	secretIDFilePathRaw, ok := conf.Config["secret_id_file_path"]
	if ok {
		a.secretIDFilePath, ok = secretIDFilePathRaw.(string)
		if !ok {
			return nil, errors.New("could not convert 'secret_id_file_path' config value to string")
		}
		if a.secretIDFilePath == "" {
			return a, nil
		}

		removeSecretIDFileAfterReadingRaw, ok := conf.Config["remove_secret_id_file_after_reading"]
		if ok {
			removeSecretIDFileAfterReading, err := parseutil.ParseBool(removeSecretIDFileAfterReadingRaw)
			if err != nil {
				return nil, fmt.Errorf("error parsing 'remove_secret_id_file_after_reading' value: %w", err)
			}
			a.removeSecretIDFileAfterReading = removeSecretIDFileAfterReading
		}

		secretIDResponseWrappingPathRaw, ok := conf.Config["secret_id_response_wrapping_path"]
		if ok {
			a.secretIDResponseWrappingPath, ok = secretIDResponseWrappingPathRaw.(string)
			if !ok {
				return nil, errors.New("could not convert 'secret_id_response_wrapping_path' config value to string")
			}
			if a.secretIDResponseWrappingPath == "" {
				return nil, errors.New("'secret_id_response_wrapping_path' value is empty")
			}
		}
	}

	return a, nil
}

func (a *approleMethod) Authenticate(ctx context.Context, client *api.Client) (string, http.Header, map[string]interface{}, error) {
	if _, err := os.Stat(a.roleIDFilePath); err == nil {
		roleID, err := ioutil.ReadFile(a.roleIDFilePath)
		if err != nil {
			if a.cachedRoleID == "" {
				return "", nil, nil, fmt.Errorf("error reading role ID file and no cached role ID known: %w", err)
			}
			a.logger.Warn("error reading role ID file", "error", err)
		}
		if len(roleID) == 0 {
			if a.cachedRoleID == "" {
				return "", nil, nil, errors.New("role ID file empty and no cached role ID known")
			}
			a.logger.Warn("role ID file exists but read empty value, re-using cached value")
		} else {
			a.cachedRoleID = strings.TrimSpace(string(roleID))
		}
	}

	if a.cachedRoleID == "" {
		return "", nil, nil, errors.New("no known role ID")
	}

	if a.secretIDFilePath == "" {
		return fmt.Sprintf("%s/login", a.mountPath), nil, map[string]interface{}{
			"role_id": a.cachedRoleID,
		}, nil
	}

	if _, err := os.Stat(a.secretIDFilePath); err == nil {
		secretID, err := ioutil.ReadFile(a.secretIDFilePath)
		if err != nil {
			if a.cachedSecretID == "" {
				return "", nil, nil, fmt.Errorf("error reading secret ID file and no cached secret ID known: %w", err)
			}
			a.logger.Warn("error reading secret ID file", "error", err)
		}
		if len(secretID) == 0 {
			if a.cachedSecretID == "" {
				return "", nil, nil, errors.New("secret ID file empty and no cached secret ID known")
			}
			a.logger.Warn("secret ID file exists but read empty value, re-using cached value")
		} else {
			stringSecretID := strings.TrimSpace(string(secretID))
			if a.secretIDResponseWrappingPath != "" {
				clonedClient, err := client.Clone()
				if err != nil {
					return "", nil, nil, fmt.Errorf("error cloning client to unwrap secret ID: %w", err)
				}
				clonedClient.SetToken(stringSecretID)
				// Validate the creation path
				resp, err := clonedClient.Logical().ReadWithContext(ctx, "sys/wrapping/lookup")
				if err != nil {
					return "", nil, nil, fmt.Errorf("error looking up wrapped secret ID: %w", err)
				}
				if resp == nil {
					return "", nil, nil, errors.New("response nil when looking up wrapped secret ID")
				}
				if resp.Data == nil {
					return "", nil, nil, errors.New("data in response nil when looking up wrapped secret ID")
				}
				creationPathRaw, ok := resp.Data["creation_path"]
				if !ok {
					return "", nil, nil, errors.New("creation_path in response nil when looking up wrapped secret ID")
				}
				creationPath, ok := creationPathRaw.(string)
				if !ok {
					return "", nil, nil, errors.New("creation_path in response could not be parsed as string when looking up wrapped secret ID")
				}
				if creationPath != a.secretIDResponseWrappingPath {
					a.logger.Error("SECURITY: unable to validate wrapping token creation path", "expected", a.secretIDResponseWrappingPath, "found", creationPath)
					return "", nil, nil, errors.New("unable to validate wrapping token creation path")
				}
				// Now get the secret ID
				resp, err = clonedClient.Logical().UnwrapWithContext(ctx, "")
				if err != nil {
					return "", nil, nil, fmt.Errorf("error unwrapping secret ID: %w", err)
				}
				if resp == nil {
					return "", nil, nil, errors.New("response nil when unwrapping secret ID")
				}
				if resp.Data == nil {
					return "", nil, nil, errors.New("data in response nil when unwrapping secret ID")
				}
				secretIDRaw, ok := resp.Data["secret_id"]
				if !ok {
					return "", nil, nil, errors.New("secret_id in response nil when unwrapping secret ID")
				}
				secretID, ok := secretIDRaw.(string)
				if !ok {
					return "", nil, nil, errors.New("secret_id in response could not be parsed as string when unwrapping secret ID")
				}
				stringSecretID = secretID
			}
			a.cachedSecretID = stringSecretID
			if a.removeSecretIDFileAfterReading {
				if err := os.Remove(a.secretIDFilePath); err != nil {
					a.logger.Error("error removing secret ID file after reading", "error", err)
				}
			}
		}
	}

	if a.cachedSecretID == "" {
		return "", nil, nil, errors.New("no known secret ID")
	}

	return fmt.Sprintf("%s/login", a.mountPath), nil, map[string]interface{}{
		"role_id":   a.cachedRoleID,
		"secret_id": a.cachedSecretID,
	}, nil
}

func (a *approleMethod) NewCreds() chan struct{} {
	return nil
}

func (a *approleMethod) CredSuccess() {
}

func (a *approleMethod) Shutdown() {
}
