package passgen

import (
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"math"
)

const (
	lowerChars  = "abcdefghijklmnopqrstuvwxyz"
	upperChars  = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	digitChars  = "0123456789"
	symbolChars = "!@#$%^&*()-_=+[]{};:,.?/"
)

var ErrEmptyAlphabet = errors.New("alphabet is empty")

type Options struct {
	Length     int
	UseLower   bool
	UseUpper   bool
	UseDigits  bool
	UseSymbols bool
	Random     io.Reader
}

func DefaultOptions() Options {
	return Options{
		Length:     24,
		UseLower:   true,
		UseUpper:   true,
		UseDigits:  true,
		UseSymbols: true,
	}
}

func Generate(opts Options) (string, error) {
	if opts.Length <= 0 {
		return "", fmt.Errorf("length must be positive")
	}

	alphabet := buildAlphabet(opts)
	if alphabet == "" {
		return "", ErrEmptyAlphabet
	}

	random := opts.Random
	if random == nil {
		random = rand.Reader
	}

	password := make([]byte, opts.Length)
	for i := range password {
		n, err := randomIndex(len(alphabet), random)
		if err != nil {
			return "", fmt.Errorf("choose password character: %w", err)
		}
		password[i] = alphabet[n]
	}

	return string(password), nil
}

func EntropyBits(length int, alphabetSize int) (float64, error) {
	if length <= 0 {
		return 0, fmt.Errorf("length must be positive")
	}
	if alphabetSize <= 1 {
		return 0, fmt.Errorf("alphabet size must be greater than 1")
	}

	return float64(length) * math.Log2(float64(alphabetSize)), nil
}

func AlphabetSize(opts Options) int {
	return len(buildAlphabet(opts))
}

func buildAlphabet(opts Options) string {
	alphabet := ""
	if opts.UseLower {
		alphabet += lowerChars
	}
	if opts.UseUpper {
		alphabet += upperChars
	}
	if opts.UseDigits {
		alphabet += digitChars
	}
	if opts.UseSymbols {
		alphabet += symbolChars
	}
	return alphabet
}

func randomIndex(size int, random io.Reader) (int, error) {
	if size <= 0 {
		return 0, ErrEmptyAlphabet
	}
	if size > 256 {
		return 0, fmt.Errorf("alphabet size %d exceeds supported maximum 256", size)
	}

	cutoff := 256 - (256 % size)
	for {
		value, err := randomByte(random)
		if err != nil {
			return 0, err
		}
		if int(value) < cutoff {
			return int(value) % size, nil
		}
	}
}

func randomByte(random io.Reader) (byte, error) {
	var b [1]byte
	if _, err := io.ReadFull(random, b[:]); err != nil {
		return 0, fmt.Errorf("read random byte: %w", err)
	}
	return b[0], nil
}
