// Copyright 2012-2025 The NATS Authors
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net"
	"net/url"
	"reflect"
	"runtime"
	"strconv"
	"strings"
	"time"
)

// This map is used to store URLs string as the key with a reference count as
// the value. This is used to handle gossiped URLs such as connect_urls, etc..
type refCountedUrlSet map[string]int

// Ascii numbers 0-9
const (
	asciiZero = 48
	asciiNine = 57
)

func versionComponents(version string) (major, minor, patch int, err error) {
	m := semVerRe.FindStringSubmatch(version)
	if len(m) == 0 {
		return 0, 0, 0, errors.New("invalid semver")
	}
	major, err = strconv.Atoi(m[1])
	if err != nil {
		return -1, -1, -1, err
	}
	minor, err = strconv.Atoi(m[2])
	if err != nil {
		return -1, -1, -1, err
	}
	patch, err = strconv.Atoi(m[3])
	if err != nil {
		return -1, -1, -1, err
	}
	return major, minor, patch, err
}

func versionAtLeastCheckError(version string, emajor, eminor, epatch int) (bool, error) {
	major, minor, patch, err := versionComponents(version)
	if err != nil {
		return false, err
	}
	if major > emajor ||
		(major == emajor && minor > eminor) ||
		(major == emajor && minor == eminor && patch >= epatch) {
		return true, nil
	}
	return false, err
}

func versionAtLeast(version string, emajor, eminor, epatch int) bool {
	res, _ := versionAtLeastCheckError(version, emajor, eminor, epatch)
	return res
}

// parseSize expects decimal positive numbers. We
// return -1 to signal error.
func parseSize(d []byte) (n int) {
	const maxParseSizeLen = 9 //999M

	l := len(d)
	if l == 0 || l > maxParseSizeLen {
		return -1
	}
	var (
		i   int
		dec byte
	)

	// Note: Use `goto` here to avoid for loop in order
	// to have the function be inlined.
	// See: https://github.com/golang/go/issues/14768
loop:
	dec = d[i]
	if dec < asciiZero || dec > asciiNine {
		return -1
	}
	n = n*10 + (int(dec) - asciiZero)

	i++
	if i < l {
		goto loop
	}
	return n
}

// parseInt64 expects decimal positive numbers. We
// return -1 to signal error
func parseInt64(d []byte) (n int64) {
	if len(d) == 0 {
		return -1
	}
	for _, dec := range d {
		if dec < asciiZero || dec > asciiNine {
			return -1
		}
		n = n*10 + (int64(dec) - asciiZero)
	}
	return n
}

// Helper to move from float seconds to time.Duration
func secondsToDuration(seconds float64) time.Duration {
	ttl := seconds * float64(time.Second)
	return time.Duration(ttl)
}

// Parse a host/port string with a default port to use
// if none (or 0 or -1) is specified in `hostPort` string.
func parseHostPort(hostPort string, defaultPort int) (host string, port int, err error) {
	if hostPort != "" {
		host, sPort, err := net.SplitHostPort(hostPort)
		if ae, ok := err.(*net.AddrError); ok && strings.Contains(ae.Err, "missing port") {
			// try appending the current port
			host, sPort, err = net.SplitHostPort(fmt.Sprintf("%s:%d", hostPort, defaultPort))
		}
		if err != nil {
			return "", -1, err
		}
		port, err = strconv.Atoi(strings.TrimSpace(sPort))
		if err != nil {
			return "", -1, err
		}
		if port == 0 || port == -1 {
			port = defaultPort
		}
		return strings.TrimSpace(host), port, nil
	}
	return "", -1, errors.New("no hostport specified")
}

// Returns true if URL u1 represents the same URL than u2,
// false otherwise.
func urlsAreEqual(u1, u2 *url.URL) bool {
	return reflect.DeepEqual(u1, u2)
}

// comma produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. comma(834142) -> 834,142
//
// This function was copied from the github.com/dustin/go-humanize
// package and is Copyright Dustin Sallings <dustin@spy.net>
func comma(v int64) string {
	sign := ""

	// Min int64 can't be negated to a usable value, so it has to be special cased.
	if v == math.MinInt64 {
		return "-9,223,372,036,854,775,808"
	}

	if v < 0 {
		sign = "-"
		v = 0 - v
	}

	parts := []string{"", "", "", "", "", "", ""}
	j := len(parts) - 1

	for v > 999 {
		parts[j] = strconv.FormatInt(v%1000, 10)
		switch len(parts[j]) {
		case 2:
			parts[j] = "0" + parts[j]
		case 1:
			parts[j] = "00" + parts[j]
		}
		v = v / 1000
		j--
	}
	parts[j] = strconv.Itoa(int(v))
	return sign + strings.Join(parts[j:], ",")
}

// Adds urlStr to the given map. If the string was already present, simply
// bumps the reference count.
// Returns true only if it was added for the first time.
func (m refCountedUrlSet) addUrl(urlStr string) bool {
	m[urlStr]++
	return m[urlStr] == 1
}

// Removes urlStr from the given map. If the string is not present, nothing
// is done and false is returned.
// If the string was present, its reference count is decreased. Returns true
// if this was the last reference, false otherwise.
func (m refCountedUrlSet) removeUrl(urlStr string) bool {
	removed := false
	if ref, ok := m[urlStr]; ok {
		if ref == 1 {
			removed = true
			delete(m, urlStr)
		} else {
			m[urlStr]--
		}
	}
	return removed
}

// Returns the unique URLs in this map as a slice
func (m refCountedUrlSet) getAsStringSlice() []string {
	a := make([]string, 0, len(m))
	for u := range m {
		a = append(a, u)
	}
	return a
}

// natsListenConfig provides a common configuration to match the one used by
// net.Listen() but with our own defaults.
// Go 1.13 introduced default-on TCP keepalives with aggressive timings and
// there's no sane portable way in Go with stdlib to split the initial timer
// from the retry timer.  Linux/BSD defaults are 2hrs/75s and Go sets both
// to 15s; the issue re making them indepedently tunable has been open since
// 2014 and this code here is being written in 2020.
// The NATS protocol has its own L7 PING/PONG keepalive system and the Go
// defaults are inappropriate for IoT deployment scenarios.
// Replace any NATS-protocol calls to net.Listen(...) with
// natsListenConfig.Listen(ctx,...) or use natsListen(); leave calls for HTTP
// monitoring, etc, on the default.
var natsListenConfig = &net.ListenConfig{
	KeepAlive: -1,
}

// natsListen() is the same as net.Listen() except that TCP keepalives are
// disabled (to match Go's behavior before Go 1.13).
func natsListen(network, address string) (net.Listener, error) {
	return natsListenConfig.Listen(context.Background(), network, address)
}

// natsDialTimeout is the same as net.DialTimeout() except the TCP keepalives
// are disabled (to match Go's behavior before Go 1.13).
func natsDialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
	d := net.Dialer{
		Timeout:   timeout,
		KeepAlive: -1,
	}
	return d.Dial(network, address)
}

// redactURLList() returns a copy of a list of URL pointers where each item
// in the list will either be the same pointer if the URL does not contain a
// password, or to a new object if there is a password.
// The intended use-case is for logging lists of URLs safely.
func redactURLList(unredacted []*url.URL) []*url.URL {
	r := make([]*url.URL, len(unredacted))
	// In the common case of no passwords, if we don't let the new object leave
	// this function then GC should be easier.
	needCopy := false
	for i := range unredacted {
		if unredacted[i] == nil {
			r[i] = nil
			continue
		}
		if _, has := unredacted[i].User.Password(); !has {
			r[i] = unredacted[i]
			continue
		}
		needCopy = true
		ru := *unredacted[i]
		ru.User = url.UserPassword(ru.User.Username(), "xxxxx")
		r[i] = &ru
	}
	if needCopy {
		return r
	}
	return unredacted
}

// redactURLString() attempts to redact a URL string.
func redactURLString(raw string) string {
	if !strings.ContainsRune(raw, '@') {
		return raw
	}
	u, err := url.Parse(raw)
	if err != nil {
		return raw
	}
	return u.Redacted()
}

// getURLsAsString returns a slice of u.Host from the given slice of url.URL's
func getURLsAsString(urls []*url.URL) []string {
	a := make([]string, 0, len(urls))
	for _, u := range urls {
		a = append(a, u.Host)
	}
	return a
}

// copyBytes make a new slice of the same size as `src` and copy its content.
// If `src` is nil or its length is 0, then this returns `nil`
func copyBytes(src []byte) []byte {
	if len(src) == 0 {
		return nil
	}
	dst := make([]byte, len(src))
	copy(dst, src)
	return dst
}

// copyStrings make a new slice of the same size than `src` and copy its content.
// If `src` is nil, then this returns `nil`
func copyStrings(src []string) []string {
	if src == nil {
		return nil
	}
	dst := make([]string, len(src))
	copy(dst, src)
	return dst
}

// Returns a byte slice for the INFO protocol.
func generateInfoJSON(info *Info) []byte {
	b, _ := json.Marshal(info)
	pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
	return bytes.Join(pcs, []byte(" "))
}

// parallelTaskQueue starts a number of goroutines and returns a channel
// which functions can be sent to for queued parallel execution. The
// goroutines will stop running when the returned channel is closed and
// all queued tasks have completed. The passed in mp limits concurrency,
// or a value <= 0 will default to GOMAXPROCS.
func parallelTaskQueue(mp int) chan<- func() {
	if rmp := runtime.GOMAXPROCS(-1); mp <= 0 {
		mp = rmp
	} else {
		mp = max(rmp, mp)
	}
	tq := make(chan func(), mp)
	for range mp {
		go func() {
			for fn := range tq {
				fn()
			}
		}()
	}
	return tq
}
