// Copyright (C) MongoDB, Inc. 2023-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 logger

import (
	"os"
	"strconv"

	"gitea.psichedelico.com/go/bson"
)

const (
	CommandFailed                    = "Command failed"
	CommandStarted                   = "Command started"
	CommandSucceeded                 = "Command succeeded"
	ConnectionPoolCreated            = "Connection pool created"
	ConnectionPoolReady              = "Connection pool ready"
	ConnectionPoolCleared            = "Connection pool cleared"
	ConnectionPoolClosed             = "Connection pool closed"
	ConnectionCreated                = "Connection created"
	ConnectionReady                  = "Connection ready"
	ConnectionClosed                 = "Connection closed"
	ConnectionCheckoutStarted        = "Connection checkout started"
	ConnectionCheckoutFailed         = "Connection checkout failed"
	ConnectionCheckedOut             = "Connection checked out"
	ConnectionCheckedIn              = "Connection checked in"
	ServerSelectionFailed            = "Server selection failed"
	ServerSelectionStarted           = "Server selection started"
	ServerSelectionSucceeded         = "Server selection succeeded"
	ServerSelectionWaiting           = "Waiting for suitable server to become available"
	TopologyClosed                   = "Stopped topology monitoring"
	TopologyDescriptionChanged       = "Topology description changed"
	TopologyOpening                  = "Starting topology monitoring"
	TopologyServerClosed             = "Stopped server monitoring"
	TopologyServerHeartbeatFailed    = "Server heartbeat failed"
	TopologyServerHeartbeatStarted   = "Server heartbeat started"
	TopologyServerHeartbeatSucceeded = "Server heartbeat succeeded"
	TopologyServerOpening            = "Starting server monitoring"
)

const (
	KeyAwaited             = "awaited"
	KeyCommand             = "command"
	KeyCommandName         = "commandName"
	KeyDatabaseName        = "databaseName"
	KeyDriverConnectionID  = "driverConnectionId"
	KeyDurationMS          = "durationMS"
	KeyError               = "error"
	KeyFailure             = "failure"
	KeyMaxConnecting       = "maxConnecting"
	KeyMaxIdleTimeMS       = "maxIdleTimeMS"
	KeyMaxPoolSize         = "maxPoolSize"
	KeyMessage             = "message"
	KeyMinPoolSize         = "minPoolSize"
	KeyNewDescription      = "newDescription"
	KeyOperation           = "operation"
	KeyOperationID         = "operationId"
	KeyPreviousDescription = "previousDescription"
	KeyRemainingTimeMS     = "remainingTimeMS"
	KeyReason              = "reason"
	KeyReply               = "reply"
	KeyRequestID           = "requestId"
	KeySelector            = "selector"
	KeyServerConnectionID  = "serverConnectionId"
	KeyServerHost          = "serverHost"
	KeyServerPort          = "serverPort"
	KeyServiceID           = "serviceId"
	KeyTimestamp           = "timestamp"
	KeyTopologyDescription = "topologyDescription"
	KeyTopologyID          = "topologyId"
)

// KeyValues is a list of key-value pairs.
type KeyValues []interface{}

// Add adds a key-value pair to an instance of a KeyValues list.
func (kvs *KeyValues) Add(key string, value interface{}) {
	*kvs = append(*kvs, key, value)
}

const (
	ReasonConnClosedStale              = "Connection became stale because the pool was cleared"
	ReasonConnClosedIdle               = "Connection has been available but unused for longer than the configured max idle time"
	ReasonConnClosedError              = "An error occurred while using the connection"
	ReasonConnClosedPoolClosed         = "Connection pool was closed"
	ReasonConnCheckoutFailedTimout     = "Wait queue timeout elapsed without a connection becoming available"
	ReasonConnCheckoutFailedError      = "An error occurred while trying to establish a new connection"
	ReasonConnCheckoutFailedPoolClosed = "Connection pool was closed"
)

// Component is an enumeration representing the "components" which can be
// logged against. A LogLevel can be configured on a per-component basis.
type Component int

const (
	// ComponentAll enables logging for all components.
	ComponentAll Component = iota

	// ComponentCommand enables command monitor logging.
	ComponentCommand

	// ComponentTopology enables topology logging.
	ComponentTopology

	// ComponentServerSelection enables server selection logging.
	ComponentServerSelection

	// ComponentConnection enables connection services logging.
	ComponentConnection
)

const (
	mongoDBLogAllEnvVar             = "MONGODB_LOG_ALL"
	mongoDBLogCommandEnvVar         = "MONGODB_LOG_COMMAND"
	mongoDBLogTopologyEnvVar        = "MONGODB_LOG_TOPOLOGY"
	mongoDBLogServerSelectionEnvVar = "MONGODB_LOG_SERVER_SELECTION"
	mongoDBLogConnectionEnvVar      = "MONGODB_LOG_CONNECTION"
)

var componentEnvVarMap = map[string]Component{
	mongoDBLogAllEnvVar:             ComponentAll,
	mongoDBLogCommandEnvVar:         ComponentCommand,
	mongoDBLogTopologyEnvVar:        ComponentTopology,
	mongoDBLogServerSelectionEnvVar: ComponentServerSelection,
	mongoDBLogConnectionEnvVar:      ComponentConnection,
}

// EnvHasComponentVariables returns true if the environment contains any of the
// component environment variables.
func EnvHasComponentVariables() bool {
	for envVar := range componentEnvVarMap {
		if os.Getenv(envVar) != "" {
			return true
		}
	}

	return false
}

// Command is a struct defining common fields that must be included in all
// commands.
type Command struct {
	DriverConnectionID int64          // Driver's ID for the connection
	Name               string         // Command name
	DatabaseName       string         // Database name
	Message            string         // Message associated with the command
	OperationID        int32          // Driver-generated operation ID
	RequestID          int64          // Driver-generated request ID
	ServerConnectionID *int64         // Server's ID for the connection used for the command
	ServerHost         string         // Hostname or IP address for the server
	ServerPort         string         // Port for the server
	ServiceID          *bson.ObjectID // ID for the command  in load balancer mode
}

// SerializeCommand takes a command and a variable number of key-value pairs and
// returns a slice of interface{} that can be passed to the logger for
// structured logging.
func SerializeCommand(cmd Command, extraKeysAndValues ...interface{}) KeyValues {
	// Initialize the boilerplate keys and values.
	keysAndValues := KeyValues{
		KeyCommandName, cmd.Name,
		KeyDatabaseName, cmd.DatabaseName,
		KeyDriverConnectionID, cmd.DriverConnectionID,
		KeyMessage, cmd.Message,
		KeyOperationID, cmd.OperationID,
		KeyRequestID, cmd.RequestID,
		KeyServerHost, cmd.ServerHost,
	}

	// Add the extra keys and values.
	for i := 0; i < len(extraKeysAndValues); i += 2 {
		keysAndValues.Add(extraKeysAndValues[i].(string), extraKeysAndValues[i+1])
	}

	port, err := strconv.ParseInt(cmd.ServerPort, 10, 32)
	if err == nil {
		keysAndValues.Add(KeyServerPort, port)
	}

	// Add the "serverConnectionId" if it is not nil.
	if cmd.ServerConnectionID != nil {
		keysAndValues.Add(KeyServerConnectionID, *cmd.ServerConnectionID)
	}

	// Add the "serviceId" if it is not nil.
	if cmd.ServiceID != nil {
		keysAndValues.Add(KeyServiceID, cmd.ServiceID.Hex())
	}

	return keysAndValues
}

// Connection contains data that all connection log messages MUST contain.
type Connection struct {
	Message    string // Message associated with the connection
	ServerHost string // Hostname or IP address for the server
	ServerPort string // Port for the server
}

// SerializeConnection serializes a Connection message into a slice of keys and
// values that can be passed to a logger.
func SerializeConnection(conn Connection, extraKeysAndValues ...interface{}) KeyValues {
	// Initialize the boilerplate keys and values.
	keysAndValues := KeyValues{
		KeyMessage, conn.Message,
		KeyServerHost, conn.ServerHost,
	}

	// Add the optional keys and values.
	for i := 0; i < len(extraKeysAndValues); i += 2 {
		keysAndValues.Add(extraKeysAndValues[i].(string), extraKeysAndValues[i+1])
	}

	port, err := strconv.ParseInt(conn.ServerPort, 10, 32)
	if err == nil {
		keysAndValues.Add(KeyServerPort, port)
	}

	return keysAndValues
}

// Server contains data that all server messages MAY contain.
type Server struct {
	DriverConnectionID int64         // Driver's ID for the connection
	TopologyID         bson.ObjectID // Driver's unique ID for this topology
	Message            string        // Message associated with the topology
	ServerConnectionID *int64        // Server's ID for the connection
	ServerHost         string        // Hostname or IP address for the server
	ServerPort         string        // Port for the server
}

// SerializeServer serializes a Server message into a slice of keys and
// values that can be passed to a logger.
func SerializeServer(srv Server, extraKV ...interface{}) KeyValues {
	// Initialize the boilerplate keys and values.
	keysAndValues := KeyValues{
		KeyDriverConnectionID, srv.DriverConnectionID,
		KeyMessage, srv.Message,
		KeyServerHost, srv.ServerHost,
		KeyTopologyID, srv.TopologyID.Hex(),
	}

	if connID := srv.ServerConnectionID; connID != nil {
		keysAndValues.Add(KeyServerConnectionID, *connID)
	}

	port, err := strconv.ParseInt(srv.ServerPort, 10, 32)
	if err == nil {
		keysAndValues.Add(KeyServerPort, port)
	}

	// Add the optional keys and values.
	for i := 0; i < len(extraKV); i += 2 {
		keysAndValues.Add(extraKV[i].(string), extraKV[i+1])
	}

	return keysAndValues
}

// ServerSelection contains data that all server selection messages MUST
// contain.
type ServerSelection struct {
	Selector            string
	OperationID         *int32
	Operation           string
	TopologyDescription string
}

// SerializeServerSelection serializes a Topology message into a slice of keys
// and values that can be passed to a logger.
func SerializeServerSelection(srvSelection ServerSelection, extraKV ...interface{}) KeyValues {
	keysAndValues := KeyValues{
		KeySelector, srvSelection.Selector,
		KeyOperation, srvSelection.Operation,
		KeyTopologyDescription, srvSelection.TopologyDescription,
	}

	if srvSelection.OperationID != nil {
		keysAndValues.Add(KeyOperationID, *srvSelection.OperationID)
	}

	// Add the optional keys and values.
	for i := 0; i < len(extraKV); i += 2 {
		keysAndValues.Add(extraKV[i].(string), extraKV[i+1])
	}

	return keysAndValues
}

// Topology contains data that all topology messages MAY contain.
type Topology struct {
	ID      bson.ObjectID // Driver's unique ID for this topology
	Message string        // Message associated with the topology
}

// SerializeTopology serializes a Topology message into a slice of keys and
// values that can be passed to a logger.
func SerializeTopology(topo Topology, extraKV ...interface{}) KeyValues {
	keysAndValues := KeyValues{
		KeyTopologyID, topo.ID.Hex(),
	}

	// Add the optional keys and values.
	for i := 0; i < len(extraKV); i += 2 {
		keysAndValues.Add(extraKV[i].(string), extraKV[i+1])
	}

	return keysAndValues
}
