// Copyright 2021 iLogtail 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 util

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"hash/fnv"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"
	"sync/atomic"
	"time"
	"unicode"
	"unsafe"

	"github.com/alibaba/ilogtail/pkg/protocol"
)

const alphanum string = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

const (
	ShardHashTagKey = "__shardhash__"
	PackIDTagKey    = "__pack_id__"
	AliCloudPrefix  = "ALICLOUD_"
	LoongPrefix     = "LOONG_"
)

var (
	ErrCommandTimeout = errors.New("command time out")
	ErrNotImplemented = errors.New("not implemented yet")
	ErrInvalidEnvType = errors.New("invalid env type")
)

// ReadLines reads contents from a file and splits them by new lines.
// A convenience wrapper to ReadLinesOffsetN(filename, 0, -1).
func ReadLines(filename string) ([]string, error) {
	return ReadLinesOffsetN(filename, 0, -1)
}

// ReadFirstBlock read first \S+ from head of line
func ReadFirstBlock(line string) string {
	for i, c := range line {
		// 32 -> [SPACE] 33 -> ! 126 -> ~ 127 -> [DEL]
		if c < 33 || c > 126 {
			return line[0:i]
		}
	}
	return line
}

// ReadLinesOffsetN reads contents from file and splits them by new line.
// The offset tells at which line number to start.
// The count determines the number of lines to read (starting from offset):
// n >= 0: at most n lines
// n < 0: whole file
func ReadLinesOffsetN(filename string, offset uint, n int) ([]string, error) {
	f, err := os.Open(filepath.Clean(filename))
	if err != nil {
		return []string{""}, err
	}
	defer func(f *os.File) {
		_ = f.Close()
	}(f)

	var ret []string

	r := bufio.NewReader(f)
	for i := 0; i < n+int(offset) || n < 0; i++ {
		line, err := r.ReadString('\n')
		if err != nil {
			break
		}
		if i < int(offset) {
			continue
		}
		ret = append(ret, strings.Trim(line, "\n"))
	}

	return ret, nil
}

// RandomString returns a random string of alpha-numeric characters
func RandomString(n int) string {
	var slice = make([]byte, n)
	_, _ = rand.Read(slice)
	for i, b := range slice {
		slice[i] = alphanum[b%byte(len(alphanum))]
	}
	return string(slice)
}

// GetTLSConfig gets a tls.Config object from the given certs, key, and CA files.
// you must give the full path to the files.
// If all files are blank and InsecureSkipVerify=false, returns a nil pointer.
func GetTLSConfig(sslCert, sslKey, sslCA string, insecureSkipVerify bool) (*tls.Config, error) {
	if sslCert == "" && sslKey == "" && sslCA == "" && !insecureSkipVerify {
		return nil, nil
	}

	t := &tls.Config{InsecureSkipVerify: insecureSkipVerify} //nolint:gosec

	if sslCA != "" {
		caCert, err := os.ReadFile(filepath.Clean(sslCA))
		if err != nil {
			return nil, fmt.Errorf("Could not load TLS CA: %v", err)
		}

		caCertPool := x509.NewCertPool()
		caCertPool.AppendCertsFromPEM(caCert)
		t.RootCAs = caCertPool
	}

	if sslCert != "" && sslKey != "" {
		cert, err := tls.LoadX509KeyPair(sslCert, sslKey)
		if err != nil {
			return nil, fmt.Errorf("could not load TLS client key/certificate from %s:%s: %s", sslKey, sslCert, err)
		}

		t.Certificates = []tls.Certificate{cert}
		t.BuildNameToCertificate()
	}

	// will be nil by default if nothing is provided
	return t, 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)
}

// CombinedOutputTimeout runs the given command with the given timeout and
// returns the combined output of stdout and stderr.
// If the command times out, it attempts to kill the process.
func CombinedOutputTimeout(c *exec.Cmd, timeout time.Duration) ([]byte, error) {
	var b bytes.Buffer
	c.Stdout = &b
	c.Stderr = &b
	if err := c.Start(); err != nil {
		return nil, err
	}
	err := WaitTimeout(c, timeout)
	return b.Bytes(), err
}

// RunTimeout runs the given command with the given timeout.
// If the command times out, it attempts to kill the process.
func RunTimeout(c *exec.Cmd, timeout time.Duration) error {
	if err := c.Start(); err != nil {
		return err
	}
	return WaitTimeout(c, timeout)
}

// WaitTimeout waits for the given command to finish with a timeout.
// It assumes the command has already been started.
// If the command times out, it attempts to kill the process.
func WaitTimeout(c *exec.Cmd, timeout time.Duration) error {
	timer := time.NewTimer(timeout)
	done := make(chan error)
	go func() { done <- c.Wait() }()
	select {
	case err := <-done:
		timer.Stop()
		return err
	case <-timer.C:
		if err := c.Process.Kill(); err != nil {
			log.Printf("E! FATAL error killing process: %s", err)
			return err
		}
		// wait for the command to return after killing it
		<-done
		return ErrCommandTimeout
	}
}

// return true if shutdown is signaled
func RandomSleep(base time.Duration, precisionLose float64, shutdown <-chan struct{}) bool {
	// TODO: Last implementation costs too much CPU, find a better way to implement it.
	return Sleep(base, shutdown)
}

// Sleep returns true if shutdown is signaled.
func Sleep(interval time.Duration, shutdown <-chan struct{}) bool {
	select {
	case <-time.After(interval):
		return false
	case <-shutdown:
		return true
	}
}

func CutString(val string, maxLen int) string {
	if len(val) < maxLen {
		return val
	}
	return val[0:maxLen]
}

func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

func SplitPath(path string) (dir string, filename string) {
	lastIndex := strings.LastIndexByte(path, '/')
	lastIndex2 := strings.LastIndexByte(path, '\\')
	if lastIndex < 0 && lastIndex2 < 0 {
		return "", ""
	}
	index := 0
	if lastIndex > lastIndex2 {
		index = lastIndex
	} else {
		index = lastIndex2
	}
	return path[0:index], path[index+1:]
}

func InitFromEnvBool(key string, value *bool, defaultValue bool) error {
	var envValue string
	if strings.Contains(key, AliCloudPrefix) {
		envValue = GetEnvTags(strings.Replace(key, AliCloudPrefix, LoongPrefix, 1), key)
	} else {
		envValue = os.Getenv(key)
	}
	if len(envValue) > 0 {
		lowErVal := strings.ToLower(envValue)
		if strings.HasPrefix(lowErVal, "y") || strings.HasPrefix(lowErVal, "t") || strings.HasPrefix(lowErVal, "on") || strings.HasPrefix(lowErVal, "ok") {
			*value = true
		} else {
			*value = false
		}
		return nil
	}
	*value = defaultValue
	return nil
}

func InitFromEnvInt(key string, value *int, defaultValue int) error {
	var envValue string
	if strings.Contains(key, AliCloudPrefix) {
		envValue = GetEnvTags(strings.Replace(key, AliCloudPrefix, LoongPrefix, 1), key)
	} else {
		envValue = os.Getenv(key)
	}
	if len(envValue) > 0 {
		if val, err := strconv.Atoi(envValue); err == nil {
			*value = val
			return nil
		}
		*value = defaultValue
		return ErrInvalidEnvType
	}
	*value = defaultValue
	return nil
}

func InitFromEnvString(key string, value *string, defaultValue string) error {
	var envValue string
	if strings.Contains(key, AliCloudPrefix) {
		envValue = GetEnvTags(strings.Replace(key, AliCloudPrefix, LoongPrefix, 1), key)
	} else {
		envValue = os.Getenv(key)
	}
	if len(envValue) > 0 {
		*value = envValue
		return nil
	}
	*value = defaultValue
	return nil
}

func GetEnvTags(firstKey, secondKey string) string {
	tag := os.Getenv(firstKey)
	if len(tag) == 0 {
		return os.Getenv(secondKey)
	}
	return tag
}

// GuessRegionByEndpoint guess region
// cn-hangzhou.log.aliyuncs.com cn-hangzhou-intranet.log.aliyuncs.com cn-hangzhou-vpc.log.aliyuncs.com cn-hangzhou-share.log.aliyuncs.com
func GuessRegionByEndpoint(endPoint, defaultRegion string) string {

	if strings.HasPrefix(endPoint, "http://") {
		endPoint = endPoint[len("http://"):]
	} else {
		endPoint = strings.TrimPrefix(endPoint, "https://")
	}
	if dotIndex := strings.IndexByte(endPoint, '.'); dotIndex > 0 {
		region := endPoint[0:dotIndex]
		if strings.HasSuffix(region, "-intranet") || strings.HasSuffix(region, "-vpc") || strings.HasSuffix(region, "-share") {
			region = region[0:strings.LastIndexByte(region, '-')]
		}
		return region
	}
	return defaultRegion
}

func DeepCopy(src *map[string]interface{}) *map[string]interface{} {
	if src == nil {
		return nil
	}
	var buf []byte
	var err error
	if buf, err = json.Marshal(src); err != nil {
		return nil
	}
	dst := new(map[string]interface{})
	if err := json.Unmarshal(buf, dst); err != nil {
		return nil
	}
	return dst
}

func InterfaceToString(val interface{}) (string, bool) {
	if val == nil {
		return "", false
	}
	strVal, ok := val.(string)
	return strVal, ok
}

func InterfaceToJSONString(val interface{}) (string, error) {
	b, err := json.Marshal(val)
	return string(b), err
}

func NewPackIDPrefix(text string) string {
	h := fnv.New64a()
	_, _ = h.Write([]byte(text + GetIPAddress() + time.Now().String()))
	return fmt.Sprintf("%X-", h.Sum64())
}

func NewLogTagForPackID(prefix string, seqNum *int64) *protocol.LogTag {
	tag := &protocol.LogTag{
		Key:   PackIDTagKey,
		Value: fmt.Sprintf("%s%X", prefix, atomic.LoadInt64(seqNum)),
	}
	atomic.AddInt64(seqNum, 1)
	return tag
}

func MinInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// StringDeepCopy returns a deep copy or src.
// Because we can not make sure the life cycle of string passed from C++,
// so we have to make a deep copy of them so that they are always valid in Go.
func StringDeepCopy(src string) string {
	return string([]byte(src))
}

// StringPointer returns the pointer of the given string.
// nolint:gosec
func StringPointer(s string) unsafe.Pointer {
	p := (*reflect.StringHeader)(unsafe.Pointer(&s))
	return unsafe.Pointer(p.Data)
}

// UniqueStrings Merge (append) slices and remove duplicate from them!
func UniqueStrings(strSlices ...[]string) []string {
	uniqueMap := map[string]bool{}
	for _, strSlice := range strSlices {
		for _, number := range strSlice {
			uniqueMap[number] = true
		}
	}
	result := make([]string, 0, len(uniqueMap))
	for key := range uniqueMap {
		result = append(result, key)
	}
	return result
}

func Contains[T comparable](s []T, e T) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}
