package main

import (
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"net/http/httptest"
	"strconv"
	"time"
)

// PembacaJam adalah sumber waktu yang bisa disuntik. Produksi memakai
// pembungkus tipis di atas time.Now; demo memakai jam beku.
type PembacaJam interface {
	Sekarang() time.Time
}

// jamBeku berangkat dari satu waktu tetap dan hanya maju bila diminta,
// sehingga urutan kejadian — dan output program — selalu sama.
type jamBeku struct {
	kini time.Time
}

func (j *jamBeku) Sekarang() time.Time { return j.kini }

// Maju menggeser jam tiruan ke depan tanpa menunggu waktu sungguhan.
func (j *jamBeku) Maju(d time.Duration) { j.kini = j.kini.Add(d) }

// Bucket menyimpan kondisi token satu kunci API.
type Bucket struct {
	sisa     float64
	terakhir time.Time
}

// Limiter menerapkan algoritma token bucket untuk banyak kunci sekaligus.
type Limiter struct {
	jam       PembacaJam
	kapasitas float64 // token saat penuh: ukuran burst
	pengisian float64 // token per detik: laju berkelanjutan
	bucket    map[string]*Bucket
}

// Izinkan menghabiskan satu token bila tersedia; token terisi ulang
// sesuai waktu yang berlalu sejak kunjungan terakhir kunci tersebut.
func (l *Limiter) Izinkan(kunci string) bool {
	now := l.jam.Sekarang()
	b, ada := l.bucket[kunci]
	if !ada {
		b = &Bucket{sisa: l.kapasitas, terakhir: now}
		l.bucket[kunci] = b
	}
	jeda := now.Sub(b.terakhir).Seconds()
	b.sisa = min(l.kapasitas, b.sisa+jeda*l.pengisian)
	b.terakhir = now
	if b.sisa >= 1 {
		b.sisa--
		return true
	}
	return false
}

// detikTunggu menghitung kapan satu token berikutnya tersedia,
// dibulatkan ke atas untuk header Retry-After.
func (l *Limiter) detikTunggu(kunci string) int {
	b := l.bucket[kunci]
	return int(math.Ceil((1 - b.sisa) / l.pengisian))
}

// middleware membatasi laju tiap permintaan berdasarkan kunci API.
func (l *Limiter) middleware(berikut http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		kunci := r.Header.Get("X-API-Key")
		if kunci == "" {
			tulisGalat(w, http.StatusUnauthorized, "MISSING_API_KEY", "kirim header X-API-Key")
			return
		}
		if !l.Izinkan(kunci) {
			w.Header().Set("Retry-After", strconv.Itoa(l.detikTunggu(kunci)))
			tulisGalat(w, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED",
				"batas laju terlampaui; ulangi setelah Retry-After")
			return
		}
		berikut.ServeHTTP(w, r)
	})
}

// laporan adalah endpoint yang dilindungi limiter.
func laporan(w http.ResponseWriter, r *http.Request) {
	tulisJSON(w, http.StatusOK, map[string]string{"data": "ringkasan-penjualan"})
}

type galatBody struct {
	Error galatRincian `json:"error"`
}

type galatRincian struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func tulisGalat(w http.ResponseWriter, status int, kode, pesan string) {
	tulisJSON(w, status, galatBody{Error: galatRincian{Code: kode, Message: pesan}})
}

func tulisJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}

func main() {
	jam := &jamBeku{kini: time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)}
	limiter := &Limiter{
		jam:       jam,
		kapasitas: 3, // burst: tiga permintaan boleh sekaligus
		pengisian: 1, // sustained: rata-rata satu permintaan per detik
		bucket:    make(map[string]*Bucket),
	}
	mux := http.NewServeMux()
	mux.HandleFunc("GET /laporan", laporan)
	server := limiter.middleware(mux)

	// Bagian 1: burst — lima permintaan berturut-turut tanpa jeda waktu.
	for nomor := 1; nomor <= 5; nomor++ {
		kirim(server, fmt.Sprintf("#%d kunci-budi", nomor), "kunci-budi")
	}

	// Bagian 2: satu detik berlalu — satu token terisi kembali.
	fmt.Println("-- jam dimajukan 1 detik --")
	jam.Maju(time.Second)
	kirim(server, "#6 kunci-budi", "kunci-budi")

	// Bagian 3: kunci lain punya bucket sendiri, tidak ikut terblokir.
	kirim(server, "kunci-siti", "kunci-siti")

	// Bagian 4: tanpa kunci, limiter tidak punya identitas untuk dihitung.
	kirim(server, "tanpa-kunci", "")
}

// kirim mengeksekusi satu permintaan GET dengan kunci API tertentu,
// lalu mencetak status, header Retry-After, dan body.
func kirim(server http.Handler, label, kunci string) {
	req := httptest.NewRequest(http.MethodGet, "/laporan", nil)
	if kunci != "" {
		req.Header.Set("X-API-Key", kunci)
	}
	rec := httptest.NewRecorder()
	server.ServeHTTP(rec, req)
	fmt.Printf("%-5s %-9s %-13s -> %d", "GET", "/laporan", label, rec.Code)
	if coba := rec.Header().Get("Retry-After"); coba != "" {
		fmt.Printf(" (Retry-After: %s)", coba)
	}
	fmt.Println()
	if rec.Body.Len() > 0 {
		fmt.Printf("   %s", rec.Body.String())
	}
}
