package utils

import (
	"baron-sso-backend/internal/domain"
	"crypto/rand"
	"fmt"
	"slices"
	"strings"
)

const (
	lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
	uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	numberChars    = "0123456789"
	symbolChars    = "!@#$%^&*()-_=+[]{}<>?/,.~"
)

// ValidatePasswordWithPolicy validates a password against the given policy.
func ValidatePasswordWithPolicy(policy *domain.PasswordPolicy, password string) error {
	if policy == nil {
		return nil
	}
	if len(password) < policy.MinLength {
		return fmt.Errorf("비밀번호는 최소 %d자 이상이어야 합니다", policy.MinLength)
	}

	hasLower := false
	hasUpper := false
	hasNumber := false
	hasSymbol := false
	types := 0

	for _, ch := range password {
		switch {
		case ch >= 'a' && ch <= 'z':
			hasLower = true
		case ch >= 'A' && ch <= 'Z':
			hasUpper = true
		case ch >= '0' && ch <= '9':
			hasNumber = true
		default:
			hasSymbol = true
		}
	}

	if hasLower {
		types++
	}
	if hasUpper {
		types++
	}
	if hasNumber {
		types++
	}
	if hasSymbol {
		types++
	}

	if policy.MinCharacterTypes > 0 && types < policy.MinCharacterTypes {
		return fmt.Errorf("비밀번호는 영문 대/소문자/숫자/특수문자 중 %d가지 이상을 포함해야 합니다", policy.MinCharacterTypes)
	}

	if policy.Lowercase && !hasLower {
		return fmt.Errorf("비밀번호에 소문자가 포함되어야 합니다")
	}
	if policy.Uppercase && !hasUpper {
		return fmt.Errorf("비밀번호에 대문자가 포함되어야 합니다")
	}
	if policy.Number && !hasNumber {
		return fmt.Errorf("비밀번호에 숫자가 포함되어야 합니다")
	}
	if policy.NonAlphanumeric && !hasSymbol {
		return fmt.Errorf("비밀번호에 특수문자가 포함되어야 합니다")
	}

	return nil
}

// GeneratePasswordWithPolicy creates a random password that satisfies the policy.
func GeneratePasswordWithPolicy(policy *domain.PasswordPolicy) (string, error) {
	if policy == nil {
		policy = &domain.PasswordPolicy{}
	}

	categories := []struct {
		name     string
		required bool
		chars    string
	}{
		{name: "lower", required: policy.Lowercase, chars: lowercaseChars},
		{name: "upper", required: policy.Uppercase, chars: uppercaseChars},
		{name: "number", required: policy.Number, chars: numberChars},
		{name: "symbol", required: policy.NonAlphanumeric, chars: symbolChars},
	}

	selected := make([]string, 0, len(categories))
	required := make([]string, 0, len(categories))
	for _, cat := range categories {
		if cat.chars == "" {
			continue
		}
		if cat.required {
			required = append(required, cat.chars)
		}
		selected = append(selected, cat.chars)
	}

	if len(selected) == 0 {
		selected = []string{lowercaseChars, uppercaseChars, numberChars, symbolChars}
	}

	additionalTypes := policy.MinCharacterTypes - len(required)
	if additionalTypes > 0 {
		pool := make([]string, 0, len(selected))
		for _, cat := range selected {
			isRequired := slices.Contains(required, cat)
			if !isRequired {
				pool = append(pool, cat)
			}
		}

		for i := 0; i < additionalTypes && len(pool) > 0; i++ {
			idx, err := randomIndex(len(pool))
			if err != nil {
				return "", err
			}
			required = append(required, pool[idx])
			pool = append(pool[:idx], pool[idx+1:]...)
		}
	}

	minLength := policy.MinLength
	if minLength <= 0 {
		minLength = 12
	}
	if minLength < len(required) {
		minLength = len(required)
	}

	passwordRunes := make([]rune, 0, minLength)
	for _, charset := range required {
		ch, err := randomChar(charset)
		if err != nil {
			return "", err
		}
		passwordRunes = append(passwordRunes, ch)
	}

	var combined strings.Builder
	for _, charset := range selected {
		combined.WriteString(charset)
	}
	for len(passwordRunes) < minLength {
		ch, err := randomChar(combined.String())
		if err != nil {
			return "", err
		}
		passwordRunes = append(passwordRunes, ch)
	}

	if err := shuffleRunes(passwordRunes); err != nil {
		return "", err
	}

	return string(passwordRunes), nil
}

func randomIndex(max int) (int, error) {
	if max <= 0 {
		return 0, fmt.Errorf("invalid max")
	}
	b := make([]byte, 1)
	for {
		if _, err := rand.Read(b); err != nil {
			return 0, err
		}
		if int(b[0]) < max*(256/max) {
			return int(b[0]) % max, nil
		}
	}
}

func randomChar(chars string) (rune, error) {
	idx, err := randomIndex(len(chars))
	if err != nil {
		return 0, err
	}
	return rune(chars[idx]), nil
}

func shuffleRunes(values []rune) error {
	for i := len(values) - 1; i > 0; i-- {
		j, err := randomIndex(i + 1)
		if err != nil {
			return err
		}
		values[i], values[j] = values[j], values[i]
	}
	return nil
}
