package secure

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"strings"
)

const pkceCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"

// RandomBytes returns securely generated random bytes of given length.
func RandomBytes(length int) ([]byte, error) {
	if length <= 0 {
		return nil, fmt.Errorf("length must be positive")
	}
	b := make([]byte, length)
	if _, err := rand.Read(b); err != nil {
		return nil, err
	}
	return b, nil
}

// RandomString returns a base64url encoded random string of approximately the requested length.
func RandomString(length int) (string, error) {
	if length <= 0 {
		return "", fmt.Errorf("length must be positive")
	}
	// Generate ceil(length * 6/8) bytes to have enough entropy for base64 url encoding.
	byteLen := (length*6 + 7) / 8
	bytes, err := RandomBytes(byteLen)
	if err != nil {
		return "", err
	}
	s := base64.RawURLEncoding.EncodeToString(bytes)
	if len(s) > length {
		return s[:length], nil
	}
	// If encoded string shorter, pad using charset
	if len(s) < length {
		sb := strings.Builder{}
		sb.WriteString(s)
		extraNeeded := length - len(s)
		more, err := randomFromCharset(extraNeeded)
		if err != nil {
			return "", err
		}
		sb.WriteString(more)
		return sb.String(), nil
	}
	return s, nil
}

// PKCECodeVerifier generates a random string compliant with RFC 7636.
func PKCECodeVerifier(length int) (string, error) {
	if length < 43 {
		length = 43
	}
	if length > 128 {
		length = 128
	}
	return randomFromCharset(length)
}

// randomFromCharset returns a random string of given length using pkceCharset.
func randomFromCharset(length int) (string, error) {
	if length <= 0 {
		return "", fmt.Errorf("length must be positive")
	}
	bytes, err := RandomBytes(length)
	if err != nil {
		return "", err
	}
	out := make([]byte, length)
	for i, b := range bytes {
		out[i] = pkceCharset[int(b)%len(pkceCharset)]
	}
	return string(out), nil
}

// Base64URLEncode encodes the input bytes using base64 URL encoding without padding.
func Base64URLEncode(data []byte) string {
	return base64.RawURLEncoding.EncodeToString(data)
}
