// Copyright Project Harbor 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 utils

import (
	"crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	cronlib "github.com/robfig/cron/v3"

	"github.com/goharbor/harbor/src/common"
	"github.com/goharbor/harbor/src/lib/log"
)

// ParseEndpoint parses endpoint to a URL
func ParseEndpoint(endpoint string) (*url.URL, error) {
	endpoint = strings.Trim(endpoint, " ")
	endpoint = strings.TrimRight(endpoint, "/")
	if len(endpoint) == 0 {
		return nil, fmt.Errorf("empty URL")
	}
	i := strings.Index(endpoint, "://")
	if i >= 0 {
		scheme := endpoint[:i]
		if scheme != "http" && scheme != "https" {
			return nil, fmt.Errorf("invalid scheme: %s", scheme)
		}
	} else {
		endpoint = "http://" + endpoint
	}

	return url.ParseRequestURI(endpoint)
}

// ParseRepository splits a repository into two parts: project and rest
func ParseRepository(repository string) (project, rest string) {
	repository = strings.TrimLeft(repository, "/")
	repository = strings.TrimRight(repository, "/")
	if !strings.ContainsRune(repository, '/') {
		rest = repository
		return
	}
	index := strings.Index(repository, "/")
	project = repository[0:index]
	rest = repository[index+1:]
	return
}

// GenerateRandomStringWithLen generates a random string with length
func GenerateRandomStringWithLen(length int) string {
	const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	l := len(chars)
	result := make([]byte, length)
	_, err := rand.Read(result)
	if err != nil {
		log.Warningf("Error reading random bytes: %v", err)
	}
	for i := range length {
		result[i] = chars[int(result[i])%l]
	}
	return string(result)
}

// GenerateRandomString generate a random string with 32 byte length
func GenerateRandomString() string {
	return GenerateRandomStringWithLen(32)
}

// TestTCPConn tests TCP connection
// timeout: the total time before returning if something is wrong
// with the connection, in second
// interval: the interval time for retrying after failure, in second
func TestTCPConn(addr string, timeout, interval int) error {
	success := make(chan int, 1)
	cancel := make(chan int, 1)

	go func() {
		n := 1

	loop:
		for {
			select {
			case <-cancel:
				break loop
			default:
				conn, err := net.DialTimeout("tcp", addr, time.Duration(n)*time.Second)
				if err != nil {
					log.Errorf("failed to connect to tcp://%s, retry after %d seconds :%v",
						addr, interval, err)
					n = n * 2
					time.Sleep(time.Duration(interval) * time.Second)
					continue
				}
				if err = conn.Close(); err != nil {
					log.Errorf("failed to close the connection: %v", err)
				}
				success <- 1
				break loop
			}
		}
	}()

	select {
	case <-success:
		return nil
	case <-time.After(time.Duration(timeout) * time.Second):
		cancel <- 1
		return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout)
	}
}

// ParseTimeStamp parse timestamp to time
func ParseTimeStamp(timestamp string) (*time.Time, error) {
	i, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return nil, err
	}
	t := time.Unix(i, 0)
	return &t, nil
}

// ConvertMapToStruct is used to fill the specified struct with map.
func ConvertMapToStruct(object any, values any) error {
	if object == nil {
		return errors.New("nil struct is not supported")
	}

	if reflect.TypeOf(object).Kind() != reflect.Ptr {
		return errors.New("object should be referred by pointer")
	}

	bytes, err := json.Marshal(values)
	if err != nil {
		return err
	}

	return json.Unmarshal(bytes, object)
}

// ParseProjectIDOrName parses value to ID(int64) or name(string)
func ParseProjectIDOrName(value any) (int64, string, error) {
	if value == nil {
		return 0, "", errors.New("harborIDOrName is nil")
	}

	var id int64
	var name string
	switch v := value.(type) {
	case int, int64:
		id = reflect.ValueOf(v).Int()
	case string:
		name = value.(string)
	default:
		return 0, "", fmt.Errorf("unsupported type")
	}
	return id, name, nil
}

// SafeCastString -- cast an object to string safely
func SafeCastString(value any) string {
	if result, ok := value.(string); ok {
		return result
	}
	return ""
}

// SafeCastInt --
func SafeCastInt(value any) int {
	if result, ok := value.(int); ok {
		return result
	}
	return 0
}

// SafeCastBool --
func SafeCastBool(value any) bool {
	if result, ok := value.(bool); ok {
		return result
	}
	return false
}

// SetUserAgentHeader sets the User-Agent header on an HTTP request
func SetUserAgentHeader(req *http.Request) {
	req.Header.Set(common.UserAgentHeaderName, common.UserAgent)
}

// SafeCastFloat64 --
func SafeCastFloat64(value any) float64 {
	if result, ok := value.(float64); ok {
		return result
	}
	return 0
}

// TrimLower ...
func TrimLower(str string) string {
	return strings.TrimSpace(strings.ToLower(str))
}

// GetStrValueOfAnyType return string format of any value, for map, need to convert to json
func GetStrValueOfAnyType(value any) string {
	var strVal string
	if _, ok := value.(map[string]any); ok {
		b, err := json.Marshal(value)
		if err != nil {
			log.Errorf("can not marshal json object, error %v", err)
			return ""
		}
		strVal = string(b)
	} else {
		switch val := value.(type) {
		case float64:
			strVal = strconv.FormatFloat(val, 'f', -1, 64)
		case float32:
			strVal = strconv.FormatFloat(float64(val), 'f', -1, 32)
		default:
			strVal = fmt.Sprintf("%v", value)
		}
	}
	return strVal
}

// IsIllegalLength ...
func IsIllegalLength(s string, minVal int, maxVal int) bool {
	if minVal == -1 {
		return (len(s) > maxVal)
	}
	if maxVal == -1 {
		return (len(s) <= minVal)
	}
	return (len(s) < minVal || len(s) > maxVal)
}

// ParseJSONInt ...
func ParseJSONInt(value any) (int, bool) {
	switch v := value.(type) {
	case float64:
		return int(v), true
	case int:
		return v, true
	default:
		return 0, false
	}
}

// FindNamedMatches returns named matches of the regexp groups
func FindNamedMatches(regex *regexp.Regexp, str string) map[string]string {
	match := regex.FindStringSubmatch(str)

	results := map[string]string{}
	for i, name := range match {
		results[regex.SubexpNames()[i]] = name
	}
	return results
}

// NextSchedule return next scheduled time with a cron string and current time provided
// the cron string could contain 6 tokens
// if the cron string is invalid, it returns a zero time
func NextSchedule(cron string, curTime time.Time) time.Time {
	if len(cron) == 0 {
		return time.Time{}
	}
	cr := strings.TrimSpace(cron)
	s, err := CronParser().Parse(cr)
	if err != nil {
		log.Debugf("the cron string %v is invalid, error: %v", cron, err)
		return time.Time{}
	}
	return s.Next(curTime)
}

// CronParser returns the parser of cron string with format of "* * * * * *"
func CronParser() cronlib.Parser {
	return cronlib.NewParser(cronlib.Second | cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow)
}

// ValidateCronString check whether it is a valid cron string and whether the 1st field (indicating Seconds of time) of the cron string is a fixed value of 0 or not
func ValidateCronString(cron string) error {
	if len(cron) == 0 {
		return fmt.Errorf("empty cron string is invalid")
	}
	_, err := CronParser().Parse(cron)
	if err != nil {
		return err
	}
	cronParts := strings.Split(cron, " ")
	if len(cronParts) == 6 && cronParts[0] != "0" {
		return fmt.Errorf("the 1st field (indicating Seconds of time) of the cron setting must be 0")
	}
	return nil
}

// IsLocalPath checks if path is local, includes the empty path
func IsLocalPath(path string) bool {
	return len(path) == 0 || (strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "//"))
}
