package webui

import (
	"net/http"
	"sync"
	"time"
)

// ipRateLimiter is a small in-memory fixed-window counter — same map+mutex shape as
// pgpKeyCache elsewhere in this package. Bounds how many login POSTs a single source
// IP can make per window, independent of the per-account lockout in login.go/
// webmail_login.go (that one tracks failures against one identifier from any IP;
// this one bounds request volume from one IP regardless of which account(s) it's
// trying — the two layers catch different attack shapes: a botnet spreading guesses
// across many accounts, versus one machine hammering a single account).
type ipRateLimiter struct {
	mu     sync.Mutex
	limit  int
	window time.Duration
	counts map[string]*ipWindow
}

type ipWindow struct {
	count      int
	windowEnds time.Time
}

func newIPRateLimiter(limit int, window time.Duration) *ipRateLimiter {
	return &ipRateLimiter{limit: limit, window: window, counts: map[string]*ipWindow{}}
}

// allow reports whether ip may make another request right now, incrementing its
// count as a side effect. Expired windows reset lazily on next access rather than
// via a background sweep — fine at this app's scale (a handful of login attempts
// per real user), and avoids a goroutine that outlives the App's own lifecycle.
func (l *ipRateLimiter) allow(ip string) bool {
	l.mu.Lock()
	defer l.mu.Unlock()
	now := time.Now()
	w, ok := l.counts[ip]
	if !ok || now.After(w.windowEnds) {
		w = &ipWindow{count: 0, windowEnds: now.Add(l.window)}
		l.counts[ip] = w
	}
	w.count++
	return w.count <= l.limit
}

// rateLimitLogin replies 429 and returns false if the request's source IP has
// exceeded the per-IP login rate limit — callers should return immediately without
// touching the DB or checking a password when this returns false.
func (a *App) rateLimitLogin(w http.ResponseWriter, r *http.Request) bool {
	if a.loginLimiter.allow(a.requestIP(r)) {
		return true
	}
	http.Error(w, "Too many login attempts — try again in a minute.", http.StatusTooManyRequests)
	return false
}

// accountLocked reports whether authType/identifier has accumulated enough recent
// failures (from any IP — see ratelimit.go's doc comment for why that's the point)
// to refuse another attempt right now, per the [Auth] login_attempt_limit/window_minutes
// config. Fails open (returns false) on a DB error rather than locking everyone out
// over a transient issue.
func (a *App) accountLocked(authType, identifier string) bool {
	limit := a.Cfg.Section("Auth").Key("login_attempt_limit").MustInt(8)
	windowMinutes := a.Cfg.Section("Auth").Key("login_attempt_window_minutes").MustInt(15)
	since := time.Now().Add(-time.Duration(windowMinutes) * time.Minute)
	n, err := a.DB.CountRecentFailedAttempts(authType, identifier, since)
	if err != nil {
		return false
	}
	return n >= limit
}
