// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package options

import (
	"crypto/tls"
	"fmt"
	"net/http"
	"time"

	"go.mongodb.org/mongo-driver/v2/internal/httputil"
)

// ClientEncryptionOptions represents all possible arguments used to configure a ClientEncryption instance.
//
// See corresponding setter methods for documentation.
type ClientEncryptionOptions struct {
	KeyVaultNamespace string
	KmsProviders      map[string]map[string]any
	TLSConfig         map[string]*tls.Config
	HTTPClient        *http.Client
	KeyExpiration     *time.Duration
}

// ClientEncryptionOptionsBuilder contains options to configure client
// encryption operations. Each option can be set through setter functions. See
// documentation for each setter function for an explanation of the option.
type ClientEncryptionOptionsBuilder struct {
	Opts []func(*ClientEncryptionOptions) error
}

// ClientEncryption creates a new ClientEncryptionOptions instance.
func ClientEncryption() *ClientEncryptionOptionsBuilder {
	return &ClientEncryptionOptionsBuilder{
		Opts: []func(*ClientEncryptionOptions) error{
			func(arg *ClientEncryptionOptions) error {
				arg.HTTPClient = httputil.DefaultHTTPClient
				return nil
			},
		},
	}
}

// List returns a list of ClientEncryptionOptions setter functions.
func (c *ClientEncryptionOptionsBuilder) List() []func(*ClientEncryptionOptions) error {
	return c.Opts
}

// SetKeyVaultNamespace specifies the namespace of the key vault collection. This is required.
func (c *ClientEncryptionOptionsBuilder) SetKeyVaultNamespace(ns string) *ClientEncryptionOptionsBuilder {
	c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error {
		opts.KeyVaultNamespace = ns
		return nil
	})
	return c
}

// SetKmsProviders specifies options for KMS providers. This is required.
func (c *ClientEncryptionOptionsBuilder) SetKmsProviders(providers map[string]map[string]any) *ClientEncryptionOptionsBuilder {
	c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error {
		opts.KmsProviders = providers
		return nil
	})
	return c
}

// SetTLSConfig specifies tls.Config instances for each KMS provider to use to configure TLS on all connections created
// to the KMS provider.
//
// This should only be used to set custom TLS configurations. By default, the connection will use an empty tls.Config{} with MinVersion set to tls.VersionTLS12.
func (c *ClientEncryptionOptionsBuilder) SetTLSConfig(cfg map[string]*tls.Config) *ClientEncryptionOptionsBuilder {
	c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error {
		opts.TLSConfig = cfg

		return nil
	})

	return c
}

// SetKeyExpiration specifies duration for the key expiration. 0 or negative value means "never expire".
// The granularity is in milliseconds. Any sub-millisecond fraction will be rounded up.
func (c *ClientEncryptionOptionsBuilder) SetKeyExpiration(expiration time.Duration) *ClientEncryptionOptionsBuilder {
	c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error {
		opts.KeyExpiration = &expiration

		return nil
	})

	return c
}

// BuildTLSConfig specifies tls.Config options for each KMS provider to use to configure TLS on all connections created
// to the KMS provider. The input map should contain a mapping from each KMS provider to a document containing the necessary
// options, as follows:
//
//	{
//			"kmip": {
//				"tlsCertificateKeyFile": "foo.pem",
//				"tlsCAFile": "fooCA.pem"
//			}
//	}
//
// Currently, the following TLS options are supported:
//
// 1. "tlsCertificateKeyFile" (or "sslClientCertificateKeyFile"): The "tlsCertificateKeyFile" option specifies a path to
// the client certificate and private key, which must be concatenated into one file.
//
// 2. "tlsCertificateKeyFilePassword" (or "sslClientCertificateKeyPassword"): Specify the password to decrypt the client
// private key file (e.g. "tlsCertificateKeyFilePassword=password").
//
// 3. "tlsCaFile" (or "sslCertificateAuthorityFile"): Specify the path to a single or bundle of certificate authorities
// to be considered trusted when making a TLS connection (e.g. "tlsCaFile=/path/to/caFile").
//
// This should only be used to set custom TLS options. By default, the connection will use an empty tls.Config{} with MinVersion set to tls.VersionTLS12.
func BuildTLSConfig(tlsOpts map[string]any) (*tls.Config, error) {
	// use TLS min version 1.2 to enforce more secure hash algorithms and advanced cipher suites
	cfg := &tls.Config{MinVersion: tls.VersionTLS12}

	for name := range tlsOpts {
		var err error
		switch name {
		case "tlsCertificateKeyFile", "sslClientCertificateKeyFile":
			clientCertPath, ok := tlsOpts[name].(string)
			if !ok {
				return nil, fmt.Errorf("expected %q value to be of type string, got %T", name, tlsOpts[name])
			}
			// apply custom key file password if found, otherwise use empty string
			if keyPwd, found := tlsOpts["tlsCertificateKeyFilePassword"].(string); found {
				_, err = addClientCertFromConcatenatedFile(cfg, clientCertPath, keyPwd)
			} else if keyPwd, found := tlsOpts["sslClientCertificateKeyPassword"].(string); found {
				_, err = addClientCertFromConcatenatedFile(cfg, clientCertPath, keyPwd)
			} else {
				_, err = addClientCertFromConcatenatedFile(cfg, clientCertPath, "")
			}
		case "tlsCertificateKeyFilePassword", "sslClientCertificateKeyPassword":
			continue
		case "tlsCAFile", "sslCertificateAuthorityFile":
			caPath, ok := tlsOpts[name].(string)
			if !ok {
				return nil, fmt.Errorf("expected %q value to be of type string, got %T", name, tlsOpts[name])
			}
			err = addCACertFromFile(cfg, caPath)
		default:
			return nil, fmt.Errorf("unrecognized TLS option %v", name)
		}

		if err != nil {
			return nil, err
		}
	}

	return cfg, nil
}
