// Copyright 2022 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
	"encoding/json"
	"io"
	"net/url"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/rs/xid"
	"go.uber.org/zap"
)

const (
	// todo: we can adjust this value if it's needed
	firstPage   = 1
	maxPage     = 1_000_000
	maxPageSize = 30
)

type Pager struct {
	Page     int32
	PageSize int32
	Offset   int32
}

// AdjustedPager adjust pager param and returns adjusted pager
// page ∈ [1, maxPage]
// pageSize ∈ [1, maxPageSize]
func AdjustedPager(pagePtr, pageSizePtr *int32) Pager {
	valueOrDefault := func(v *int32, def int32) int32 {
		if v == nil {
			return def
		}
		return *v
	}

	pager := Pager{
		Page:     valueOrDefault(pagePtr, firstPage),
		PageSize: valueOrDefault(pageSizePtr, defaultPageSize),
	}
	if pager.Page <= 0 {
		pager.Page = firstPage
	}
	if pager.Page > maxPage {
		pager.Page = maxPage
	}
	if pager.PageSize <= 0 {
		pager.PageSize = defaultPageSize
	}
	if pager.PageSize > maxPageSize {
		pager.PageSize = maxPageSize
	}
	pager.Offset = (pager.Page - 1) * pager.PageSize
	return pager
}

// LogClose do close and log err if needed
func LogClose(c io.Closer, logger *zap.Logger) {
	if err := errors.Trace(c.Close()); err != nil {
		logger.Warn("failed to close", zap.Error(err))
	}
}

func WrapAsErrorCodeErr(err error, msg string) error {
	if _, ok := err.(errcode.ErrorCode); ok {
		return err
	}

	return errcode.NewInternalErr(errors.New(msg))
}

func IsContain(items []string, item string) bool {
	for _, v := range items {
		if v == item {
			return true
		}
	}

	return false
}

func StructCover(in interface{}, to interface{}) error {
	b, err := json.Marshal(&in)
	if err != nil {
		return err
	}

	_ = json.Unmarshal(b, to)
	return nil
}

// CheckTLSParam check whether input cert param is allowed combination, we don't check it's content.
// CA is optional in any case, so we only need to check whether both or none of cert and key is set.
// allowed combination for SSL/TLS is:
// - CA, Cert, Key
// - Cert, Key
// - CA
func CheckTLSParam(cert string, key string) error {
	if (len(cert) == 0 && len(key) == 0) ||
		(len(cert) > 0 && len(key) > 0) {
		return nil
	}
	return errcode.NewInvalidInputErr(errors.New("Certificate and private key should be set at same time, or set none of them."))
}

type ctxKey string

var (
	CtxClusterIDKey   ctxKey = "cluster_id"
	CtxProjectIDKey   ctxKey = "project_id"
	CtxOrgIDKey       ctxKey = "org_id"
	CtxInnerCallerKey ctxKey = "x_inner_caller_auth_user"
)

// UnpackFullTableName unpacks full table name to <schema, table> pair.
// an example: `db1`.`tb1` => ("db1", "tb1").
func UnpackFullTableName(fullTableName string) (string, string, error) {
	suggestion := "a simple full table name example: `db1`.`tb1`"
	fullTableName = strings.Trim(fullTableName, " ")
	// length tableID `a`.`a` is at least 7
	if len(fullTableName) < 7 {
		return "", "", errors.New("length tableID is at least 7." + suggestion)
	}
	pair := strings.Split(fullTableName, "`")
	// first and last should be empty
	if len(pair) != 5 || len(pair[0]) != 0 || len(pair[4]) != 0 {
		return "", "", errors.New("full table name is invalid." + suggestion)
	}
	schemaName := pair[1]
	tableName := pair[3]
	dot := pair[2]
	// schema and table should be not empty
	if len(schemaName) == 0 || len(tableName) == 0 || dot != "." {
		return "", "", errors.New("schema and table should be not empty." + suggestion)
	}
	return schemaName, tableName, nil
}

func RestoreSpecialChars(str string) (string, error) {
	// https://docs.pingcap.com/tidb/stable/migrate-from-csv-using-tidb-lightning
	replaced := make([]rune, 0, len(str))
	s := []byte(str)
	r := 0
	for r < len(s) {
		if s[r] != '\\' {
			c, size := utf8.DecodeRune(s[r:])
			r += size
			replaced = append(replaced, c)
			continue
		}
		r += 1
		if r >= len(s) {
			return "", errors.New("Escape sequence at end of string.")
		}
		switch s[r] {
		default:
			return "", errors.Errorf("Expected valid escape code after \\, but got %q.", s[r])
		case ' ', '\t':
			return "", errors.Errorf("invalid escape: '\\%c'", s[r])
		case 'b':
			replaced = append(replaced, rune(0x0008))
			r += 1
		case 't':
			replaced = append(replaced, rune(0x0009))
			r += 1
		case 'n':
			replaced = append(replaced, rune(0x000A))
			r += 1
		case 'f':
			replaced = append(replaced, rune(0x000C))
			r += 1
		case 'r':
			replaced = append(replaced, rune(0x000D))
			r += 1
		case '"':
			replaced = append(replaced, rune(0x0022))
			r += 1
		case '\\':
			replaced = append(replaced, rune(0x005C))
			r += 1
		case 'u':
			// At this point, we know we have a Unicode escape of the form
			// `uXXXX` at [r, r+5). (Because the lexer guarantees this
			// for us.)
			escaped, err := asciiEscapeToUnicode(s[r+1 : r+5])
			if err != nil {
				return "", errors.Annotate(err, "ascii escape to unicode has error")
			}
			replaced = append(replaced, escaped)
			r += 5
		case 'U':
			// At this point, we know we have a Unicode escape of the form
			// `uXXXX` at [r, r+9). (Because the lexer guarantees this
			// for us.)
			escaped, err := asciiEscapeToUnicode(s[r+1 : r+9])
			if err != nil {
				return "", errors.Annotate(err, "ascii escape to unicode has error")
			}
			replaced = append(replaced, escaped)
			r += 9
		}
	}
	return string(replaced), nil
}

func asciiEscapeToUnicode(bs []byte) (rune, error) {
	s := string(bs)
	hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
	if err != nil {
		return 0, errors.Annotatef(err, "Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK", s)
	}
	if !utf8.ValidRune(rune(hex)) {
		return 0, errors.Annotatef(err, "Escaped character '\\u%s' is not valid UTF-8.", s)
	}
	return rune(hex), nil
}

func GetSasTokenFromSourceURL(sourceURL string) string {
	u, err := url.Parse(sourceURL)
	if err != nil {
		return ""
	}

	query := u.Query()
	return query.Get("sas-token")
}

func GetSourceURLWithoutSasToken(sourceURL string) string {
	u, err := url.Parse(sourceURL)
	if err != nil {
		return sourceURL
	}

	query := u.Query()
	query.Del("sas-token")
	u.RawQuery = query.Encode()
	return u.String()
}

func UniqueID() string {
	return xid.New().String()
}

func ToPtr[T any](x T) *T {
	return &x
}

func InArr[T comparable](target T, arr []T) bool {
	for i := range arr {
		if arr[i] == target {
			return true
		}
	}
	return false
}

func RemovePercent(s string) string {
	return strings.TrimRight(s, " %")
}

// StringToUint64 converts string to uint64, returns 0 if string is empty
func StringToUint64(s string) (uint64, error) {
	if s == "" {
		return 0, nil
	}
	n, err := strconv.ParseUint(s, 10, 64)
	if err != nil {
		return 0, err
	}
	return n, nil
}

// Uint64ToString converts uint64 to string
func Uint64ToString(n uint64) string {
	if n == 0 {
		return "0"
	}
	return strconv.FormatUint(n, 10)
}

// PtrToString converts *string to string, returns empty string if pointer is nil
func PtrToString(p *string) string {
	if p == nil {
		return ""
	}
	return *p
}

// PtrToInt32 converts *int32 to int32, returns 0 if pointer is nil
func PtrToInt32(p *int32) int32 {
	if p == nil {
		return 0
	}
	return *p
}

// PtrToBool converts *bool to bool, returns false if pointer is nil
func PtrToBool(p *bool) bool {
	if p == nil {
		return false
	}
	return *p
}
