package common

import (
	"bytes"
	"compress/zlib"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"hash/fnv"
	"io"
	"math/big"
	mathrand "math/rand"
	"os"
	"runtime"
	"sort"
	"sync"
)

var (
	Letters                 = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	GCMNonce NonceGenerator = NewNonceGenerator(12)
)

func EncryptString(secret string, data string) (string, error) {
	d, err := compress([]byte(data))
	if err != nil {
		return "", err
	}
	d, err = EncryptAESGCM([]byte(secret), d)
	if err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(d), nil
}

func DecryptString(secret string, data string) (string, error) {
	d, err := base64.URLEncoding.DecodeString(data)
	if err != nil {
		return "", err
	}
	d, err = decrypt([]byte(secret), d)
	if err != nil {
		return "", err
	}
	d, err = decompress(d)
	if err != nil {
		return "", err
	}
	return string(d), nil
}

func Hash(str string, n int) string {
	hasher := sha256.New()
	hasher.Write([]byte(str))
	return hashSize(hasher.Sum(nil), n)
}

func QuickHash(str string, n int) string {
	hasher := fnv.New64()
	hasher.Write([]byte(str))
	return hashSize(hasher.Sum(nil), n)
}

func HashStream(r io.Reader, n int) string {
	hasher := sha256.New()
	io.Copy(hasher, r)
	h := hex.EncodeToString(hasher.Sum(nil))
	if n == 0 {
		return h
	} else if n >= len(h) {
		return h
	}
	return h[0:n]
}

func hashSize(b []byte, n int) string {
	h := ""
	for i := 0; i < len(b); i++ {
		if n > 0 && len(h) >= n {
			break
		}
		h += ReversedBaseChange(Letters, int(b[i]))
	}

	if len(h) > n {
		return h[0 : len(h)-1]
	}
	return h
}

func ReversedBaseChange(alphabet []rune, i int) string {
	str := ""
	for {
		str += string(alphabet[i%len(alphabet)])
		i = i / len(alphabet)
		if i == 0 {
			break
		}
	}
	return str
}

func RandomString(n int) string {
	b := make([]rune, n)
	for i := range b {
		max := *big.NewInt(int64(len(Letters)))
		r, err := rand.Int(rand.Reader, &max)
		if err != nil {
			b[i] = Letters[0]
		} else {
			b[i] = Letters[r.Int64()]
		}
	}
	return string(b)
}

func QuickString(n int) string {
	b := make([]rune, n)
	for i := range b {
		b[i] = Letters[mathrand.Intn(len(Letters))]
	}
	return string(b)
}

func EncryptAESGCM(key []byte, plaintext []byte) ([]byte, error) {
	c, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(c)
	if err != nil {
		return nil, err
	}

	nonce := GCMNonce.Next()
	if gcm.NonceSize() != len(nonce) {
		Log.Error("common::crypto nonce size isn't '12' but '%d'", gcm.NonceSize())
		return nil, ErrNotValid
	}
	return gcm.Seal(nonce, nonce, plaintext, nil), nil
}

func decrypt(key []byte, ciphertext []byte) ([]byte, error) {
	c, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(c)
	if err != nil {
		return nil, err
	}

	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, NewError("ciphertext too short", 500)
	}

	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	return gcm.Open(nil, nonce, ciphertext, nil)
}

func compress(something []byte) ([]byte, error) {
	var b bytes.Buffer
	w := zlib.NewWriter(&b)
	w.Write(something)
	w.Close()
	return b.Bytes(), nil
}

func decompress(something []byte) ([]byte, error) {
	b := bytes.NewBuffer(something)
	r, err := zlib.NewReader(b)
	if err != nil {
		return []byte(""), nil
	}
	r.Close()
	return io.ReadAll(r)
}

func sign(something []byte) ([]byte, error) {
	return something, nil
}

func verify(something []byte) ([]byte, error) {
	return something, nil
}

// Create a unique ID that can be use to identify different session
func GenerateID(params map[string]string) string {
	p := ""
	orderedKeys := make([]string, len(params))
	for key, _ := range params {
		orderedKeys = append(orderedKeys, key)
	}
	sort.Strings(orderedKeys)

	for _, key := range orderedKeys {
		switch key {
		case "timestamp":
		case "password":
		case "path":
		default:
			if val := params[key]; val != "" {
				p += key + "=>" + params[key] + ", "
			}
		}
	}
	if p == "" {
		return "na"
	}
	p += "salt=>" + SECRET_KEY
	return Hash(p, 20)
}

// Create an ID that identify a machine
func GenerateMachineID() string {
	if runtime.GOOS == "linux" {
		if f, err := os.OpenFile("/etc/machine-id", os.O_RDONLY, os.ModePerm); err == nil {
			defer f.Close()
			b := make([]byte, 32)
			if _, err = f.Read(b); err == nil {
				return string(b)
			}
		} else if f, err := os.OpenFile("/var/lib/dbus/machine-id", os.O_RDONLY, os.ModePerm); err == nil {
			defer f.Close()
			b := make([]byte, 32)
			if _, err = f.Read(b); err == nil {
				return string(b)
			}
		}
	}
	return "na"
}

type NonceGenerator struct {
	current []byte
	count   int
	*sync.Mutex
}

func NewNonceGenerator(size int) NonceGenerator {
	firstNonce := make([]byte, size)
	io.ReadFull(rand.Reader, firstNonce)
	var m sync.Mutex
	return NonceGenerator{firstNonce, size, &m}
}

func (this *NonceGenerator) Next() []byte {
	this.Lock()
	newNonce := make([]byte, this.count)
	for i := len(this.current) - 1; i >= 0; i-- {
		if this.current[i] < 255 {
			this.current[i] += 1
			break
		}
		this.current[i] = 0
	}
	newNonce = this.current
	this.Unlock()
	return newNonce
}
