// Package banview mirrors the gateway's active IP bans for the admin console and
// collects operator unban requests for the gateway to apply. Like ratewatch it is
// in-memory, single-instance and resets on a backend restart by design — the
// gateway re-reports its active set on the next sync, and the durable effect (the
// ban itself) lives in the gateway, not here.
package banview

import (
	"sort"
	"sync"
	"time"
)

// Ban is one active IP ban as reported by the gateway.
type Ban struct {
	IP      string
	Reason  string
	Since   time.Time
	Expires time.Time
}

// View holds the last-reported active bans and the operator's pending unbans.
type View struct {
	now func() time.Time

	mu    sync.Mutex
	bans  map[string]Ban      // last reported active set, keyed by IP
	unban map[string]struct{} // IPs an operator marked for unban
}

// New constructs an empty View.
func New() *View {
	return &View{now: time.Now, bans: make(map[string]Ban), unban: make(map[string]struct{})}
}

// Ingest replaces the mirrored active set with the gateway's latest report,
// skipping entries with an empty IP or one that has already expired.
func (v *View) Ingest(active []Ban) {
	now := v.now()
	v.mu.Lock()
	defer v.mu.Unlock()
	v.bans = make(map[string]Ban, len(active))
	for _, b := range active {
		if b.IP == "" || !now.Before(b.Expires) {
			continue
		}
		v.bans[b.IP] = b
	}
}

// Recent returns the mirrored active bans, most recently banned first.
func (v *View) Recent() []Ban {
	now := v.now()
	v.mu.Lock()
	defer v.mu.Unlock()
	out := make([]Ban, 0, len(v.bans))
	for _, b := range v.bans {
		if now.Before(b.Expires) {
			out = append(out, b)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Since.After(out[j].Since) })
	return out
}

// RequestUnban records an operator request to lift the ban on ip; the gateway
// applies it on its next sync (so the console reflects it within the sync
// interval). An empty ip is ignored.
func (v *View) RequestUnban(ip string) {
	if ip == "" {
		return
	}
	v.mu.Lock()
	defer v.mu.Unlock()
	v.unban[ip] = struct{}{}
}

// DrainUnbans returns and clears the IPs operators have marked for unban since the
// previous drain. It returns nil when there are none.
func (v *View) DrainUnbans() []string {
	v.mu.Lock()
	defer v.mu.Unlock()
	if len(v.unban) == 0 {
		return nil
	}
	out := make([]string, 0, len(v.unban))
	for ip := range v.unban {
		out = append(out, ip)
	}
	clear(v.unban)
	return out
}
