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

import (
	"errors"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/internal/bsonutil"
	"go.mongodb.org/mongo-driver/v2/internal/handshake"
	"go.mongodb.org/mongo-driver/v2/internal/ptrutil"
	"go.mongodb.org/mongo-driver/v2/mongo/address"
	"go.mongodb.org/mongo-driver/v2/tag"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/description"
)

const (
	MinWireVersion = 8
	MaxWireVersion = 25
)

func equalWireVersion(wv1, wv2 *description.VersionRange) bool {
	if wv1 == nil && wv2 == nil {
		return true
	}

	if wv1 == nil || wv2 == nil {
		return false
	}

	return wv1.Min == wv2.Min && wv1.Max == wv2.Max
}

// EqualServers compares two server descriptions and returns true if they are
// equal.
func EqualServers(srv1, srv2 description.Server) bool {
	if srv1.CanonicalAddr.String() != srv2.CanonicalAddr.String() {
		return false
	}

	if !sliceStringEqual(srv1.Arbiters, srv2.Arbiters) {
		return false
	}

	if !sliceStringEqual(srv1.Hosts, srv2.Hosts) {
		return false
	}

	if !sliceStringEqual(srv1.Passives, srv2.Passives) {
		return false
	}

	if srv1.Primary != srv2.Primary {
		return false
	}

	if srv1.SetName != srv2.SetName {
		return false
	}

	if srv1.Kind != srv2.Kind {
		return false
	}

	if srv1.LastError != nil || srv2.LastError != nil {
		if srv1.LastError == nil || srv2.LastError == nil {
			return false
		}
		if srv1.LastError.Error() != srv2.LastError.Error() {
			return false
		}
	}

	if !equalWireVersion(srv1.WireVersion, srv2.WireVersion) {
		return false
	}

	if len(srv1.Tags) != len(srv2.Tags) || !srv1.Tags.ContainsAll(srv2.Tags) {
		return false
	}

	if srv1.SetVersion != srv2.SetVersion {
		return false
	}

	if srv1.ElectionID != srv2.ElectionID {
		return false
	}

	if ptrutil.CompareInt64(srv1.SessionTimeoutMinutes, srv2.SessionTimeoutMinutes) != 0 {
		return false
	}

	// If TopologyVersion is nil for both servers, CompareToIncoming will return -1 because it assumes that the
	// incoming response is newer. We want the descriptions to be considered equal in this case, though, so an
	// explicit check is required.
	if srv1.TopologyVersion == nil && srv2.TopologyVersion == nil {
		return true
	}

	return CompareTopologyVersions(srv1.TopologyVersion, srv2.TopologyVersion) == 0
}

// IsServerLoadBalanced checks if a description.Server describes a server that
// is load balanced.
func IsServerLoadBalanced(srv description.Server) bool {
	return srv.Kind == description.ServerKindLoadBalancer || srv.ServiceID != nil
}

// stringSliceFromRawElement decodes the provided BSON element into a []string.
// This internally calls StringSliceFromRawValue on the element's value. The
// error conditions outlined in that function's documentation apply for this
// function as well.
func stringSliceFromRawElement(element bson.RawElement) ([]string, error) {
	return bsonutil.StringSliceFromRawValue(element.Key(), element.Value())
}

func decodeStringMap(element bson.RawElement, name string) (map[string]string, error) {
	doc, ok := element.Value().DocumentOK()
	if !ok {
		return nil, fmt.Errorf("expected '%s' to be a document but it's a BSON %s", name, element.Value().Type)
	}
	elements, err := doc.Elements()
	if err != nil {
		return nil, err
	}
	m := make(map[string]string)
	for _, element := range elements {
		key := element.Key()
		value, ok := element.Value().StringValueOK()
		if !ok {
			return nil, fmt.Errorf("expected '%s' to be a document of strings, but found a BSON %s", name, element.Value().Type)
		}
		m[key] = value
	}
	return m, nil
}

// NewTopologyVersion creates a TopologyVersion based on doc
func NewTopologyVersion(doc bson.Raw) (*description.TopologyVersion, error) {
	elements, err := doc.Elements()
	if err != nil {
		return nil, err
	}
	var tv description.TopologyVersion
	var ok bool
	for _, element := range elements {
		switch element.Key() {
		case "processId":
			tv.ProcessID, ok = element.Value().ObjectIDOK()
			if !ok {
				return nil, fmt.Errorf("expected 'processId' to be a objectID but it's a BSON %s", element.Value().Type)
			}
		case "counter":
			tv.Counter, ok = element.Value().Int64OK()
			if !ok {
				return nil, fmt.Errorf("expected 'counter' to be an int64 but it's a BSON %s", element.Value().Type)
			}
		}
	}
	return &tv, nil
}

// NewVersionRange creates a new VersionRange given a min and a max.
func NewVersionRange(min, max int32) description.VersionRange {
	return description.VersionRange{Min: min, Max: max}
}

// VersionRangeIncludes returns a bool indicating whether the supplied integer
// is included in the range.
func VersionRangeIncludes(versionRange description.VersionRange, v int32) bool {
	return v >= versionRange.Min && v <= versionRange.Max
}

// CompareTopologyVersions compares the receiver, which represents the currently
// known TopologyVersion for a server, to an incoming TopologyVersion extracted
// from a server command response.
//
// This returns -1 if the receiver version is less than the response, 0 if the
// versions are equal, and 1 if the receiver version is greater than the
// response. This comparison is not commutative.
func CompareTopologyVersions(receiver, response *description.TopologyVersion) int {
	if receiver == nil || response == nil {
		return -1
	}
	if receiver.ProcessID != response.ProcessID {
		return -1
	}
	if receiver.Counter == response.Counter {
		return 0
	}
	if receiver.Counter < response.Counter {
		return -1
	}
	return 1
}

// NewServerDescription creates a new server description from the given hello
// command response.
func NewServerDescription(addr address.Address, response bson.Raw) description.Server {
	desc := description.Server{Addr: addr, CanonicalAddr: addr, LastUpdateTime: time.Now().UTC()}
	elements, err := response.Elements()
	if err != nil {
		desc.LastError = err
		return desc
	}
	var ok bool
	var isReplicaSet, isWritablePrimary, hidden, secondary, arbiterOnly bool
	var msg string
	var versionRange description.VersionRange
	for _, element := range elements {
		switch element.Key() {
		case "arbiters":
			var err error
			desc.Arbiters, err = stringSliceFromRawElement(element)
			if err != nil {
				desc.LastError = err
				return desc
			}
		case "arbiterOnly":
			arbiterOnly, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'arbiterOnly' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "compression":
			var err error
			desc.Compression, err = stringSliceFromRawElement(element)
			if err != nil {
				desc.LastError = err
				return desc
			}
		case "electionId":
			desc.ElectionID, ok = element.Value().ObjectIDOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'electionId' to be a objectID but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "iscryptd":
			desc.IsCryptd, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'iscryptd' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "helloOk":
			desc.HelloOK, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'helloOk' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "hidden":
			hidden, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'hidden' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "hosts":
			var err error
			desc.Hosts, err = stringSliceFromRawElement(element)
			if err != nil {
				desc.LastError = err
				return desc
			}
		case "isWritablePrimary":
			isWritablePrimary, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'isWritablePrimary' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case handshake.LegacyHelloLowercase:
			isWritablePrimary, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected legacy hello to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "isreplicaset":
			isReplicaSet, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'isreplicaset' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "lastWrite":
			lastWrite, ok := element.Value().DocumentOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'lastWrite' to be a document but it's a BSON %s", element.Value().Type)
				return desc
			}
			dateTime, err := lastWrite.LookupErr("lastWriteDate")
			if err == nil {
				dt, ok := dateTime.DateTimeOK()
				if !ok {
					desc.LastError = fmt.Errorf("expected 'lastWriteDate' to be a datetime but it's a BSON %s", dateTime.Type)
					return desc
				}
				desc.LastWriteTime = time.Unix(dt/1000, dt%1000*1000000).UTC()
			}
		case "logicalSessionTimeoutMinutes":
			i64, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'logicalSessionTimeoutMinutes' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}

			desc.SessionTimeoutMinutes = &i64
		case "maxBsonObjectSize":
			i64, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'maxBsonObjectSize' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.MaxDocumentSize = uint32(i64)
		case "maxMessageSizeBytes":
			i64, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'maxMessageSizeBytes' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.MaxMessageSize = uint32(i64)
		case "maxWriteBatchSize":
			i64, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'maxWriteBatchSize' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.MaxBatchCount = uint32(i64)
		case "me":
			me, ok := element.Value().StringValueOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'me' to be a string but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.CanonicalAddr = address.Address(me).Canonicalize()
		case "maxWireVersion":
			verMax, ok := element.Value().AsInt64OK()
			versionRange.Max = int32(verMax)
			if !ok {
				desc.LastError = fmt.Errorf("expected 'maxWireVersion' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "minWireVersion":
			verMin, ok := element.Value().AsInt64OK()
			versionRange.Min = int32(verMin)
			if !ok {
				desc.LastError = fmt.Errorf("expected 'minWireVersion' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "msg":
			msg, ok = element.Value().StringValueOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'msg' to be a string but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "ok":
			okay, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'ok' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
			if okay != 1 {
				desc.LastError = errors.New("not ok")
				return desc
			}
		case "passives":
			var err error
			desc.Passives, err = stringSliceFromRawElement(element)
			if err != nil {
				desc.LastError = err
				return desc
			}
		case "passive":
			desc.Passive, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'passive' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "primary":
			primary, ok := element.Value().StringValueOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'primary' to be a string but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.Primary = address.Address(primary)
		case "readOnly":
			desc.ReadOnly, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'readOnly' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "secondary":
			secondary, ok = element.Value().BooleanOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'secondary' to be a boolean but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "serviceId":
			oid, ok := element.Value().ObjectIDOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'serviceId' to be an ObjectId but it's a BSON %s", element.Value().Type)
			}
			desc.ServiceID = &oid
		case "setName":
			desc.SetName, ok = element.Value().StringValueOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'setName' to be a string but it's a BSON %s", element.Value().Type)
				return desc
			}
		case "setVersion":
			i64, ok := element.Value().AsInt64OK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'setVersion' to be an integer but it's a BSON %s", element.Value().Type)
				return desc
			}
			desc.SetVersion = uint32(i64)
		case "tags":
			m, err := decodeStringMap(element, "tags")
			if err != nil {
				desc.LastError = err
				return desc
			}
			desc.Tags = tag.NewTagSetFromMap(m)
		case "topologyVersion":
			doc, ok := element.Value().DocumentOK()
			if !ok {
				desc.LastError = fmt.Errorf("expected 'topologyVersion' to be a document but it's a BSON %s", element.Value().Type)
				return desc
			}

			desc.TopologyVersion, err = NewTopologyVersion(doc)
			if err != nil {
				desc.LastError = err
				return desc
			}
		}
	}

	for _, host := range desc.Hosts {
		desc.Members = append(desc.Members, address.Address(host).Canonicalize())
	}

	for _, passive := range desc.Passives {
		desc.Members = append(desc.Members, address.Address(passive).Canonicalize())
	}

	for _, arbiter := range desc.Arbiters {
		desc.Members = append(desc.Members, address.Address(arbiter).Canonicalize())
	}

	desc.Kind = description.ServerKindStandalone

	switch {
	case isReplicaSet:
		desc.Kind = description.ServerKindRSGhost
	case desc.SetName != "":
		switch {
		case isWritablePrimary:
			desc.Kind = description.ServerKindRSPrimary
		case hidden:
			desc.Kind = description.ServerKindRSMember
		case secondary:
			desc.Kind = description.ServerKindRSSecondary
		case arbiterOnly:
			desc.Kind = description.ServerKindRSArbiter
		default:
			desc.Kind = description.ServerKindRSMember
		}
	case msg == "isdbgrid":
		desc.Kind = description.ServerKindMongos
	}

	desc.WireVersion = &versionRange

	return desc
}

func sliceStringEqual(a []string, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	for i, v := range a {
		if v != b[i] {
			return false
		}
	}

	return true
}
