// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0

package cert

import (
	"context"
	"errors"
	"fmt"

	"github.com/hashicorp/vault/api"
)

const defaultMountPath = "cert"

type CertAuth struct {
	role      string
	mountPath string
}

var _ api.AuthMethod = (*CertAuth)(nil)

type LoginOption func(a *CertAuth) error

// NewCertAuth initializes a new Cert auth method interface to be
// passed as a parameter to the client.Auth().Login method. The client and other
// TLS configuration should be set up on the passed in client, you can use
// the NewCertAuthClient function to set up the client with the proper TLS client attributes.
//
// Supported options: WithRole, WithMountPath
func NewCertAuth(opts ...LoginOption) (*CertAuth, error) {
	a := &CertAuth{}

	// Loop through each option
	for _, opt := range opts {
		// Call the option giving the instantiated
		// *CertAuth as the argument
		err := opt(a)
		if err != nil {
			return nil, fmt.Errorf("error with login option: %w", err)
		}
	}

	if a.mountPath == "" {
		a.mountPath = defaultMountPath
	}

	// return the modified auth struct instance
	return a, nil
}

// Login sets up the required request body for the Cert auth method's /login
// endpoint, and performs a write to it. We assume the passed in client has the
// proper TLS client certificates set up.
func (a *CertAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) {
	if ctx == nil {
		ctx = context.Background()
	}

	if client == nil {
		return nil, fmt.Errorf("client is required for login with the associated client certs initialized")
	}

	loginData := make(map[string]interface{})
	if a.role != "" {
		loginData["name"] = a.role
	}

	certAuthPath := fmt.Sprintf("auth/%s/login", a.mountPath)
	resp, err := client.Logical().WriteWithContext(ctx, certAuthPath, loginData)
	if err != nil {
		return nil, fmt.Errorf("unable to log in with cert auth: %w", err)
	}

	return resp, nil
}

// WithRole specifies the role to use for the login request.
func WithRole(roleName string) LoginOption {
	return func(a *CertAuth) error {
		a.role = roleName
		return nil
	}
}

// WithMountPath specifies the mount path to use for the login request.
func WithMountPath(mountPath string) LoginOption {
	return func(a *CertAuth) error {
		a.mountPath = mountPath
		return nil
	}
}

// NewDefaultCertAuthClient initializes a new client with a default configuration
// with the provided address and TLS configuration. The TLSConfig must have the ClientCert and ClientKey
// fields set.
func NewDefaultCertAuthClient(address string, tlsConfig *api.TLSConfig) (*api.Client, error) {
	if tlsConfig == nil {
		return nil, errors.New("tls config is required for cert auth client")
	}

	if tlsConfig.ClientCert == "" || tlsConfig.ClientKey == "" {
		return nil, errors.New("client cert and key are required for cert auth client")
	}

	if len(address) == 0 {
		return nil, errors.New("address is required for cert auth client")
	}

	cfg := api.DefaultConfig()
	cfg.Address = address
	err := cfg.ConfigureTLS(tlsConfig)
	if err != nil {
		return nil, fmt.Errorf("failed configuring TLS on client config: %w", err)
	}

	client, err := api.NewClient(cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to create client: %v", err)
	}

	return client, nil
}

// NewCertAuthClient initializes a new client based on the passed in client
// with the provided TLS configuration. The TLSConfig must have the ClientCert and ClientKey
// fields set.
func NewCertAuthClient(c *api.Client, config *api.TLSConfig) (*api.Client, error) {
	if c == nil {
		return nil, errors.New("base client is required for cert auth client")
	}
	if config == nil {
		return nil, errors.New("tls config is required for cert auth client")
	}

	if config.ClientCert == "" || config.ClientKey == "" {
		return nil, errors.New("client cert and key are required for cert auth client")
	}

	conf := c.CloneConfig()
	err := conf.ConfigureTLS(config)
	if err != nil {
		return nil, fmt.Errorf("failed to configure TLS on client: %w", err)
	}

	client, err := api.NewClient(conf)
	if err != nil {
		return nil, fmt.Errorf("failed to create client: %v", err)
	}

	return client, nil
}
