// 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 topology

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"sync/atomic"
	"time"

	"go.mongodb.org/mongo-driver/v2/event"
	"go.mongodb.org/mongo-driver/v2/internal/logger"
	"go.mongodb.org/mongo-driver/v2/internal/optionsutil"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/auth"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/description"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/ocsp"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/operation"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/session"
)

const (
	defaultServerSelectionTimeout = 30 * time.Second
	defaultConnectionTimeout      = 30 * time.Second
)

// Config is used to construct a topology.
type Config struct {
	Mode                   MonitorMode
	ReplicaSetName         string
	SeedList               []string
	ServerOpts             []ServerOption
	URI                    string
	ConnectTimeout         time.Duration
	Timeout                *time.Duration
	ServerSelectionTimeout time.Duration
	ServerMonitor          *event.ServerMonitor
	SRVMaxHosts            int
	SRVServiceName         string
	LoadBalanced           bool
	logger                 *logger.Logger
}

// ConvertToDriverAPIOptions converts a given ServerAPIOptions object from the
// options package to a ServerAPIOptions object from the driver package.
func ConvertToDriverAPIOptions(opts *options.ServerAPIOptions) *driver.ServerAPIOptions {
	driverOpts := driver.NewServerAPIOptions(string(opts.ServerAPIVersion))
	if opts.Strict != nil {
		driverOpts.SetStrict(*opts.Strict)
	}
	if opts.DeprecationErrors != nil {
		driverOpts.SetDeprecationErrors(*opts.DeprecationErrors)
	}
	return driverOpts
}

func newLogger(opts *options.LoggerOptions) (*logger.Logger, error) {
	if opts == nil {
		opts = options.Logger()
	}

	componentLevels := make(map[logger.Component]logger.Level)
	for component, level := range opts.ComponentLevels {
		componentLevels[logger.Component(component)] = logger.Level(level)
	}

	log, err := logger.New(opts.Sink, opts.MaxDocumentLength, componentLevels)
	if err != nil {
		return nil, fmt.Errorf("error creating logger: %w", err)
	}

	return log, nil
}

// convertOIDCArgs converts the internal *driver.OIDCArgs into the equivalent
// public type *options.OIDCArgs.
func convertOIDCArgs(args *driver.OIDCArgs) *options.OIDCArgs {
	if args == nil {
		return nil
	}
	return &options.OIDCArgs{
		Version:      args.Version,
		IDPInfo:      (*options.IDPInfo)(args.IDPInfo),
		RefreshToken: args.RefreshToken,
	}
}

// ConvertCreds takes an [options.Credential] and returns the equivalent
// [driver.Cred].
func ConvertCreds(cred *options.Credential) *driver.Cred {
	if cred == nil {
		return nil
	}

	var oidcMachineCallback auth.OIDCCallback
	if cred.OIDCMachineCallback != nil {
		oidcMachineCallback = func(ctx context.Context, args *driver.OIDCArgs) (*driver.OIDCCredential, error) {
			cred, err := cred.OIDCMachineCallback(ctx, convertOIDCArgs(args))
			return (*driver.OIDCCredential)(cred), err
		}
	}

	var oidcHumanCallback auth.OIDCCallback
	if cred.OIDCHumanCallback != nil {
		oidcHumanCallback = func(ctx context.Context, args *driver.OIDCArgs) (*driver.OIDCCredential, error) {
			cred, err := cred.OIDCHumanCallback(ctx, convertOIDCArgs(args))
			return (*driver.OIDCCredential)(cred), err
		}
	}

	return &auth.Cred{
		Source:              cred.AuthSource,
		Username:            cred.Username,
		Password:            cred.Password,
		PasswordSet:         cred.PasswordSet,
		Props:               cred.AuthMechanismProperties,
		OIDCMachineCallback: oidcMachineCallback,
		OIDCHumanCallback:   oidcHumanCallback,
	}
}

// NewConfig will translate data from client options into a topology config for
// building non-default deployments. Server and topology options are not honored
// if a custom deployment is used.
func NewConfig(opts *options.ClientOptions, clock *session.ClusterClock) (*Config, error) {
	var authenticator driver.Authenticator
	var err error
	if opts.Auth != nil {
		authenticator, err = auth.CreateAuthenticator(
			opts.Auth.AuthMechanism,
			ConvertCreds(opts.Auth),
			opts.HTTPClient,
		)
		if err != nil {
			return nil, fmt.Errorf("error creating authenticator: %w", err)
		}
	}
	return NewAuthenticatorConfig(authenticator,
		WithAuthConfigClock(clock),
		WithAuthConfigClientOptions(opts),
	)
}

type authConfigOptions struct {
	clock      *session.ClusterClock
	opts       *options.ClientOptions
	driverInfo *atomic.Pointer[options.DriverInfo]
}

// AuthConfigOption is a function that configures authConfigOptions.
type AuthConfigOption func(*authConfigOptions)

// WithAuthConfigClock sets the cluster clock in authConfigOptions.
func WithAuthConfigClock(clock *session.ClusterClock) AuthConfigOption {
	return func(co *authConfigOptions) {
		co.clock = clock
	}
}

// WithAuthConfigClientOptions sets the client options in authConfigOptions.
func WithAuthConfigClientOptions(opts *options.ClientOptions) AuthConfigOption {
	return func(co *authConfigOptions) {
		co.opts = opts
	}
}

// WithAuthConfigDriverInfo sets the driver info in authConfigOptions.
func WithAuthConfigDriverInfo(driverInfo *atomic.Pointer[options.DriverInfo]) AuthConfigOption {
	return func(co *authConfigOptions) {
		co.driverInfo = driverInfo
	}
}

// NewAuthenticatorConfig will translate data from client options into a
// topology config for building non-default deployments. Server and topology
// options are not honored if a custom deployment is used. It uses a passed in
// authenticator to authenticate the connection.
func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...AuthConfigOption) (*Config, error) {
	settings := authConfigOptions{}
	for _, apply := range clientOpts {
		apply(&settings)
	}

	opts := settings.opts
	clock := settings.clock
	driverInfo := settings.driverInfo

	var serverAPI *driver.ServerAPIOptions

	if err := opts.Validate(); err != nil {
		return nil, err
	}

	var connOpts []ConnectionOption
	var serverOpts []ServerOption

	cfgp := &Config{
		Timeout: opts.Timeout,
	}

	// Set the default "ServerSelectionTimeout" to 30 seconds.
	cfgp.ServerSelectionTimeout = defaultServerSelectionTimeout

	// Set the default "ConnectionTimeout" to 30 seconds.
	cfgp.ConnectTimeout = defaultConnectionTimeout

	// Set the default "SeedList" to localhost.
	cfgp.SeedList = []string{"localhost:27017"}

	// TODO(GODRIVER-814): Add tests for topology, server, and connection related options.

	// ServerAPIOptions need to be handled early as other client and server options below reference
	// c.serverAPI and serverOpts.serverAPI.
	if opts.ServerAPIOptions != nil {
		serverAPI = ConvertToDriverAPIOptions(opts.ServerAPIOptions)
		serverOpts = append(serverOpts, WithServerAPI(func(*driver.ServerAPIOptions) *driver.ServerAPIOptions {
			return serverAPI
		}))
	}

	cfgp.URI = opts.GetURI()

	if opts.SRVServiceName != nil {
		cfgp.SRVServiceName = *opts.SRVServiceName
	}

	if opts.SRVMaxHosts != nil {
		cfgp.SRVMaxHosts = *opts.SRVMaxHosts
	}

	// AppName
	var appName string
	if opts.AppName != nil {
		appName = *opts.AppName

		serverOpts = append(serverOpts, WithServerAppName(func(string) string {
			return appName
		}))
	}

	if driverInfo != nil {
		serverOpts = append(serverOpts, WithDriverInfo(driverInfo))
	}

	// Compressors & ZlibLevel
	var comps []string
	if len(opts.Compressors) > 0 {
		comps = opts.Compressors

		connOpts = append(connOpts, WithCompressors(
			func(compressors []string) []string {
				return append(compressors, comps...)
			},
		))

		for _, comp := range comps {
			switch comp {
			case "zlib":
				connOpts = append(connOpts, WithZlibLevel(func(*int) *int {
					return opts.ZlibLevel
				}))
			case "zstd":
				connOpts = append(connOpts, WithZstdLevel(func(*int) *int {
					return opts.ZstdLevel
				}))
			}
		}

		serverOpts = append(serverOpts, WithCompressionOptions(
			func(opts ...string) []string { return append(opts, comps...) },
		))
	}

	var loadBalanced bool
	if opts.LoadBalanced != nil {
		loadBalanced = *opts.LoadBalanced
	}

	// Handshaker
	var handshaker func(driver.Handshaker) driver.Handshaker
	if authenticator != nil {
		handshaker = func(driver.Handshaker) driver.Handshaker {
			handshakeOpts := &auth.HandshakeOptions{
				AppName:       appName,
				Authenticator: authenticator,
				Compressors:   comps,
				ServerAPI:     serverAPI,
				LoadBalanced:  loadBalanced,
				ClusterClock:  clock,
			}

			if opts.Auth.AuthMechanism == "" {
				// Required for SASL mechanism negotiation during handshake
				handshakeOpts.DBUser = opts.Auth.AuthSource + "." + opts.Auth.Username
			}

			if auth, ok := optionsutil.Value(opts.Custom, "authenticateToAnything").(bool); ok && auth {
				// Authenticate arbiters
				handshakeOpts.PerformAuthentication = func(_ description.Server) bool {
					return true
				}
			}

			if driverInfo != nil {
				if di := driverInfo.Load(); di != nil {
					handshakeOpts.OuterLibraryName = di.Name
					handshakeOpts.OuterLibraryVersion = di.Version
					handshakeOpts.OuterLibraryPlatform = di.Platform
				}
			}

			return auth.Handshaker(nil, handshakeOpts)
		}
	} else {
		handshaker = func(driver.Handshaker) driver.Handshaker {
			op := operation.NewHello().
				AppName(appName).
				Compressors(comps).
				ClusterClock(clock).
				ServerAPI(serverAPI).
				LoadBalanced(loadBalanced)

			if driverInfo != nil {
				if di := driverInfo.Load(); di != nil {
					op = op.OuterLibraryName(di.Name).
						OuterLibraryVersion(di.Version).
						OuterLibraryPlatform(di.Platform)
				}
			}

			return op
		}
	}

	connOpts = append(connOpts, WithHandshaker(handshaker))

	// Dialer
	if opts.Dialer != nil {
		connOpts = append(connOpts, WithDialer(
			func(Dialer) Dialer { return opts.Dialer },
		))
	}
	// Direct
	if opts.Direct != nil && *opts.Direct {
		cfgp.Mode = SingleMode
	}

	// HeartbeatInterval
	if opts.HeartbeatInterval != nil {
		serverOpts = append(serverOpts, WithHeartbeatInterval(
			func(time.Duration) time.Duration { return *opts.HeartbeatInterval },
		))
	}
	// Hosts
	cfgp.SeedList = []string{"localhost:27017"} // default host
	if len(opts.Hosts) > 0 {
		cfgp.SeedList = opts.Hosts
	}

	// MaxConIdleTime
	if opts.MaxConnIdleTime != nil {
		serverOpts = append(serverOpts, WithConnectionPoolMaxIdleTime(
			func(time.Duration) time.Duration { return *opts.MaxConnIdleTime },
		))
	}
	// MaxPoolSize
	if opts.MaxPoolSize != nil {
		serverOpts = append(
			serverOpts,
			WithMaxConnections(func(uint64) uint64 { return *opts.MaxPoolSize }),
		)
	}
	// MinPoolSize
	if opts.MinPoolSize != nil {
		serverOpts = append(
			serverOpts,
			WithMinConnections(func(uint64) uint64 { return *opts.MinPoolSize }),
		)
	}
	// MaxConnecting
	if opts.MaxConnecting != nil {
		serverOpts = append(
			serverOpts,
			WithMaxConnecting(func(uint64) uint64 { return *opts.MaxConnecting }),
		)
	}
	// PoolMonitor
	if opts.PoolMonitor != nil {
		serverOpts = append(
			serverOpts,
			WithConnectionPoolMonitor(func(*event.PoolMonitor) *event.PoolMonitor { return opts.PoolMonitor }),
		)
	}
	// Monitor
	if opts.Monitor != nil {
		connOpts = append(connOpts, WithMonitor(
			func(*event.CommandMonitor) *event.CommandMonitor { return opts.Monitor },
		))
	}
	// ServerMonitor
	if opts.ServerMonitor != nil {
		serverOpts = append(
			serverOpts,
			WithServerMonitor(func(*event.ServerMonitor) *event.ServerMonitor { return opts.ServerMonitor }),
		)
		cfgp.ServerMonitor = opts.ServerMonitor
	}
	// ReplicaSet
	if opts.ReplicaSet != nil {
		cfgp.ReplicaSetName = *opts.ReplicaSet
	}
	// ServerSelectionTimeout
	if opts.ServerSelectionTimeout != nil {
		cfgp.ServerSelectionTimeout = *opts.ServerSelectionTimeout
	}
	// ConnectionTimeout
	if opts.ConnectTimeout != nil {
		cfgp.ConnectTimeout = *opts.ConnectTimeout
	}
	// TLSConfig
	if opts.TLSConfig != nil {
		connOpts = append(connOpts, WithTLSConfig(
			func(*tls.Config) *tls.Config {
				return opts.TLSConfig
			},
		))
	}

	// HTTP Client
	if opts.HTTPClient != nil {
		connOpts = append(connOpts, WithHTTPClient(
			func(*http.Client) *http.Client {
				return opts.HTTPClient
			},
		))
	}

	// OCSP cache
	ocspCache := ocsp.NewCache()
	connOpts = append(
		connOpts,
		WithOCSPCache(func(ocsp.Cache) ocsp.Cache { return ocspCache }),
	)

	// Disable communication with external OCSP responders.
	if opts.DisableOCSPEndpointCheck != nil {
		connOpts = append(
			connOpts,
			WithDisableOCSPEndpointCheck(func(bool) bool { return *opts.DisableOCSPEndpointCheck }),
		)
	}

	// LoadBalanced
	if opts.LoadBalanced != nil {
		cfgp.LoadBalanced = *opts.LoadBalanced

		serverOpts = append(
			serverOpts,
			WithServerLoadBalanced(func(bool) bool { return *opts.LoadBalanced }),
		)
		connOpts = append(
			connOpts,
			WithConnectionLoadBalanced(func(bool) bool { return *opts.LoadBalanced }),
		)
	}

	lgr, err := newLogger(opts.LoggerOptions)
	if err != nil {
		return nil, err
	}

	serverOpts = append(
		serverOpts,
		withLogger(func() *logger.Logger { return lgr }),
		withServerMonitoringMode(opts.ServerMonitoringMode),
	)

	cfgp.logger = lgr

	serverOpts = append(
		serverOpts,
		WithClock(func(*session.ClusterClock) *session.ClusterClock { return clock }),
		WithConnectionOptions(func(...ConnectionOption) []ConnectionOption { return connOpts }))

	cfgp.ServerOpts = serverOpts

	return cfgp, nil
}
