package middleware

import (
	"bookhoard/internal/database"
	"fmt"
	"regexp"
	"sync"

	"github.com/go-playground/validator/v10"
)

// specialCharRegex matches the historical "special character" set used by the
// password complexity rules.
const specialCharRegex = `[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`

// PasswordValidator validates password complexity against the configured rules.
// When a database.SettingsRegistry is wired via SetSettings, rules are read live and the
// regex set is recompiled under a mutex on each validation. Without a registry
// the historical hardcoded defaults (8+ chars, upper/lower/number/special) apply.
type PasswordValidator struct {
	settings *database.SettingsRegistry
}

// SetSettings wires the tunable settings registry.
func (v *PasswordValidator) SetSettings(s *database.SettingsRegistry) { v.settings = s }

// compileSpecialRegex isolates the regexp compile (which is safe to call
// concurrently, but we keep it behind a cached var for the no-registry path).
var (
	specialOnce sync.Once
	specialRe   *regexp.Regexp
)

func specialRegex() *regexp.Regexp {
	specialOnce.Do(func() {
		specialRe = regexp.MustCompile(specialCharRegex)
	})
	return specialRe
}

func (v *PasswordValidator) rules() database.PasswordRules {
	if v.settings != nil {
		return v.settings.PasswordRules()
	}
	return database.PasswordRules{MinLength: 8, Upper: true, Lower: true, Number: true, Special: true}
}

// Validate checks if a password meets the configured complexity requirements.
func (v *PasswordValidator) Validate(fl validator.FieldLevel) bool {
	return v.CheckPassword(fl.Field().String())
}

// CheckPassword applies the active rules to a single password.
func (v *PasswordValidator) CheckPassword(password string) bool {
	r := v.rules()
	if len(password) < r.MinLength {
		return false
	}
	if r.Upper && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
		return false
	}
	if r.Lower && !regexp.MustCompile(`[a-z]`).MatchString(password) {
		return false
	}
	if r.Number && !regexp.MustCompile(`[0-9]`).MatchString(password) {
		return false
	}
	if r.Special && !specialRegex().MatchString(password) {
		return false
	}
	return true
}

// GetPasswordRequirements returns a human-readable list of the active password
// requirements, driven by the configured rules when a registry is wired.
func GetPasswordRequirements() []string {
	return defaultPasswordValidator.Requirements()
}

// Requirements returns the human-readable list for the receiver's active rules.
func (v *PasswordValidator) Requirements() []string {
	r := v.rules()
	var out []string
	out = append(out, fmt.Sprintf("At least %d characters long", r.MinLength))
	if r.Upper {
		out = append(out, "At least one uppercase letter (A-Z)")
	}
	if r.Lower {
		out = append(out, "At least one lowercase letter (a-z)")
	}
	if r.Number {
		out = append(out, "At least one number (0-9)")
	}
	if r.Special {
		out = append(out, "At least one special character (!@#$%^&*()_+-=[]{}|;':\",./<>?)")
	}
	return out
}

// ValidatePassword checks a password against the default (hardcoded) rules and
// returns an error describing the first unmet requirement. Retained for callers
// that don't have access to a configured PasswordValidator instance.
func ValidatePassword(password string) error {
	v := defaultPasswordValidator
	r := v.rules()
	if len(password) < r.MinLength {
		return fmt.Errorf("password must be at least %d characters long", r.MinLength)
	}
	if r.Upper && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
		return fmt.Errorf("password must contain at least one uppercase letter")
	}
	if r.Lower && !regexp.MustCompile(`[a-z]`).MatchString(password) {
		return fmt.Errorf("password must contain at least one lowercase letter")
	}
	if r.Number && !regexp.MustCompile(`[0-9]`).MatchString(password) {
		return fmt.Errorf("password must contain at least one number")
	}
	if r.Special && !specialRegex().MatchString(password) {
		return fmt.Errorf("password must contain at least one special character")
	}
	return nil
}

// defaultPasswordValidator is used by the package-level helpers
// (GetPasswordRequirements, ValidatePassword) and as the fallback inside
// RegisterPasswordValidation when no registry has been wired. Callers that want
// live rule updates should construct their own PasswordValidator and call
// SetSettings.
var defaultPasswordValidator = &PasswordValidator{}

// RegisterPasswordValidation registers the password validator with the
// validator instance. The registered func re-evaluates rules on every call, so
// changes to the wired registry take effect immediately.
func RegisterPasswordValidation(v *validator.Validate) error {
	return v.RegisterValidation("passwordcomplex", func(fl validator.FieldLevel) bool {
		return defaultPasswordValidator.CheckPassword(fl.Field().String())
	})
}

// SetDefaultPasswordSettings wires the settings registry into the package-level
// default validator so that the struct-tag validator (used by echo's
// CustomValidator) and ValidatePassword follow live configuration. Intended to
// be called once at startup.
func SetDefaultPasswordSettings(s *database.SettingsRegistry) {
	defaultPasswordValidator.SetSettings(s)
}
