package internal

import (
	"bufio"
	"compress/gzip"
	"context"
	cryptoRand "crypto/rand"
	"errors"
	"fmt"
	"io"
	"math/big"
	"math/rand"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"
	"unicode"

	"github.com/influxdata/telegraf/internal/choice"
)

const alphanum string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

var (
	ErrTimeout        = errors.New("command timed out")
	ErrNotImplemented = errors.New("not implemented yet")
)

// Set via LDFLAGS -X
var (
	Version = "unknown"
	Branch  = ""
	Commit  = ""
)

type ReadWaitCloser struct {
	pipeReader *io.PipeReader
	wg         sync.WaitGroup
}

func FormatFullVersion() string {
	var parts = []string{"Telegraf"}

	if Version != "" {
		parts = append(parts, Version)
	} else {
		parts = append(parts, "unknown")
	}

	if Branch != "" || Commit != "" {
		if Branch == "" {
			Branch = "unknown"
		}
		if Commit == "" {
			Commit = "unknown"
		}
		git := fmt.Sprintf("(git: %s@%s)", Branch, Commit)
		parts = append(parts, git)
	}

	return strings.Join(parts, " ")
}

// ProductToken returns a tag for Telegraf that can be used in user agents.
func ProductToken() string {
	return fmt.Sprintf("Telegraf/%s Go/%s",
		Version, strings.TrimPrefix(runtime.Version(), "go"))
}

// ReadLines reads contents from a file and splits them by new lines.
func ReadLines(filename string) ([]string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return []string{""}, err
	}
	defer f.Close()

	var ret []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		ret = append(ret, scanner.Text())
	}

	return ret, nil
}

// RandomString returns a random string of alphanumeric characters
func RandomString(n int) (string, error) {
	var bytes = make([]byte, n)
	_, err := cryptoRand.Read(bytes)
	if err != nil {
		return "", err
	}
	for i, b := range bytes {
		bytes[i] = alphanum[b%byte(len(alphanum))]
	}
	return string(bytes), nil
}

// SnakeCase converts the given string to snake case following the Golang format:
// acronyms are converted to lower-case and preceded by an underscore.
func SnakeCase(in string) string {
	runes := []rune(in)
	length := len(runes)

	var out []rune
	for i := 0; i < length; i++ {
		if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
			out = append(out, '_')
		}
		out = append(out, unicode.ToLower(runes[i]))
	}

	return string(out)
}

// RandomSleep will sleep for a random amount of time up to max.
// If the shutdown channel is closed, it will return before it has finished
// sleeping.
func RandomSleep(max time.Duration, shutdown chan struct{}) {
	if max == 0 {
		return
	}

	sleepns := rand.Int63n(max.Nanoseconds())

	t := time.NewTimer(time.Nanosecond * time.Duration(sleepns))
	select {
	case <-t.C:
		return
	case <-shutdown:
		t.Stop()
		return
	}
}

// RandomDuration returns a random duration between 0 and max.
func RandomDuration(max time.Duration) time.Duration {
	if max == 0 {
		return 0
	}

	sleepns := rand.Int63n(max.Nanoseconds())

	return time.Duration(sleepns)
}

// SleepContext sleeps until the context is closed or the duration is reached.
func SleepContext(ctx context.Context, duration time.Duration) error {
	if duration == 0 {
		return nil
	}

	t := time.NewTimer(duration)
	select {
	case <-t.C:
		return nil
	case <-ctx.Done():
		t.Stop()
		return ctx.Err()
	}
}

// AlignDuration returns the duration until next aligned interval.
// If the current time is aligned a 0 duration is returned.
func AlignDuration(tm time.Time, interval time.Duration) time.Duration {
	return AlignTime(tm, interval).Sub(tm)
}

// AlignTime returns the time of the next aligned interval.
// If the current time is aligned the current time is returned.
func AlignTime(tm time.Time, interval time.Duration) time.Time {
	truncated := tm.Truncate(interval)
	if truncated == tm {
		return tm
	}
	return truncated.Add(interval)
}

// ExitStatus takes the error from exec.Command
// and returns the exit status and true
// if error is not exit status, will return 0 and false
func ExitStatus(err error) (int, bool) {
	var exitErr *exec.ExitError
	if errors.As(err, &exitErr) {
		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
			return status.ExitStatus(), true
		}
	}
	return 0, false
}

func (r *ReadWaitCloser) Close() error {
	err := r.pipeReader.Close()
	r.wg.Wait() // wait for the gzip goroutine finish
	return err
}

// CompressWithGzip takes an io.Reader as input and pipes it through a
// gzip.Writer returning an io.Reader containing the gzipped data.
// Errors occurring during compression are returned to the instance reading
// from the returned reader via through the corresponding read call
// (e.g. io.Copy or io.ReadAll).
func CompressWithGzip(data io.Reader) io.ReadCloser {
	pipeReader, pipeWriter := io.Pipe()
	gzipWriter := gzip.NewWriter(pipeWriter)

	// Start copying from the uncompressed reader to the output reader
	// in the background until the input reader is closed (or errors out).
	go func() {
		// This copy will block until "data" reached EOF or an error occurs
		_, err := io.Copy(gzipWriter, data)

		// Close the compression writer and make sure we do not overwrite
		// the copy error if any.
		gzipErr := gzipWriter.Close()
		if err == nil {
			err = gzipErr
		}

		// Subsequent reads from the output reader (connected to "pipeWriter"
		// via pipe) will return the copy (or closing) error if any to the
		// instance reading from the reader returned by the CompressWithGzip
		// function. If "err" is nil, the below function will correctly report
		// io.EOF.
		_ = pipeWriter.CloseWithError(err)
	}()

	// Return a reader which then can be read by the caller to collect the
	// compressed stream.
	return pipeReader
}

// ParseTimestamp parses a Time according to the standard Telegraf options.
// These are generally displayed in the toml similar to:
//
//	json_time_key= "timestamp"
//	json_time_format = "2006-01-02T15:04:05Z07:00"
//	json_timezone = "America/Los_Angeles"
//
// The format can be one of "unix", "unix_ms", "unix_us", "unix_ns", or a Go
// time layout suitable for time.Parse.
//
// When using the "unix" format, an optional fractional component is allowed.
// Specific unix time precisions cannot have a fractional component.
//
// Unix times may be an int64, float64, or string.  When using a Go format
// string the timestamp must be a string.
//
// The location is a location string suitable for time.LoadLocation.  Unix
// times do not use the location string, a unix time is always return in the
// UTC location.
func ParseTimestamp(format string, timestamp interface{}, location *time.Location, separator ...string) (time.Time, error) {
	switch format {
	case "unix", "unix_ms", "unix_us", "unix_ns":
		sep := []string{",", "."}
		if len(separator) > 0 {
			sep = separator
		}
		return parseUnix(format, timestamp, sep)
	default:
		v, ok := timestamp.(string)
		if !ok {
			return time.Unix(0, 0), errors.New("unsupported type")
		}
		return parseTime(format, v, location)
	}
}

// parseTime parses a timestamp in unix format with different resolutions
func parseUnix(format string, timestamp interface{}, separator []string) (time.Time, error) {
	// Extract the scaling factor to nanoseconds from "format"
	var factor int64
	switch format {
	case "unix":
		factor = int64(time.Second)
	case "unix_ms":
		factor = int64(time.Millisecond)
	case "unix_us":
		factor = int64(time.Microsecond)
	case "unix_ns":
		factor = int64(time.Nanosecond)
	}

	zero := time.Unix(0, 0)

	// Convert the representation to time
	switch v := timestamp.(type) {
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		t, err := ToInt64(v)
		if err != nil {
			return zero, err
		}
		return time.Unix(0, t*factor).UTC(), nil
	case float32, float64:
		ts, err := ToFloat64(v)
		if err != nil {
			return zero, err
		}

		// Parse the float as a precise fraction to avoid precision loss
		f := big.Rat{}
		if f.SetFloat64(ts) == nil {
			return zero, errors.New("invalid number")
		}
		return timeFromFraction(&f, factor), nil
	case string:
		// Sanitize the string to have no thousand separators and dot
		// as decimal separator to ease later parsing
		v = sanitizeTimestamp(v, separator)

		// Parse the string as a precise fraction to avoid precision loss
		f := big.Rat{}
		if _, ok := f.SetString(v); !ok {
			return zero, errors.New("invalid number")
		}
		return timeFromFraction(&f, factor), nil
	}

	return zero, errors.New("unsupported type")
}

func timeFromFraction(f *big.Rat, factor int64) time.Time {
	// Extract the numerator and denominator and scale to nanoseconds
	num := f.Num()
	denom := f.Denom()
	num.Mul(num, big.NewInt(factor))

	// Get the integer (non-fractional part) of the timestamp and convert
	// it into time
	t := big.Int{}
	t.Div(num, denom)

	return time.Unix(0, t.Int64()).UTC()
}

// sanitizeTimestamp removes thousand separators and uses dot as
// decimal separator. Returns also a boolean indicating success.
func sanitizeTimestamp(timestamp string, decimalSeparator []string) string {
	// Remove thousand-separators that are not used for decimal separation
	sanitized := timestamp
	for _, s := range []string{" ", ",", "."} {
		if !choice.Contains(s, decimalSeparator) {
			sanitized = strings.ReplaceAll(sanitized, s, "")
		}
	}

	// Replace decimal separators by dot to have a standard, parsable format
	for _, s := range decimalSeparator {
		// Make sure we replace only the first occurrence of any separator.
		if strings.Contains(sanitized, s) {
			return strings.Replace(sanitized, s, ".", 1)
		}
	}
	return sanitized
}

// parseTime parses a string timestamp according to the format string.
func parseTime(format string, timestamp string, location *time.Location) (time.Time, error) {
	loc := location
	if loc == nil {
		loc = time.UTC
	}

	switch strings.ToLower(format) {
	case "ansic":
		format = time.ANSIC
	case "unixdate":
		format = time.UnixDate
	case "rubydate":
		format = time.RubyDate
	case "rfc822":
		format = time.RFC822
	case "rfc822z":
		format = time.RFC822Z
	case "rfc850":
		format = time.RFC850
	case "rfc1123":
		format = time.RFC1123
	case "rfc1123z":
		format = time.RFC1123Z
	case "rfc3339":
		format = time.RFC3339
	case "rfc3339nano":
		format = time.RFC3339Nano
	case "stamp":
		format = time.Stamp
	case "stampmilli":
		format = time.StampMilli
	case "stampmicro":
		format = time.StampMicro
	case "stampnano":
		format = time.StampNano
	}
	return time.ParseInLocation(format, timestamp, loc)
}
