package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
)

// Identitas menjawab "siapa pemanggilnya" setelah autentikasi berhasil.
type Identitas struct {
	Nama  string
	Peran string
}

// kunciKonteks memisahkan namespace nilai context milik program ini.
type kunciKonteks struct{}

// profilPublik adalah bentuk identitas yang dikirim ke klien.
type profilPublik struct {
	Nama  string `json:"nama"`
	Peran string `json:"peran"`
}

type profilBody struct {
	Data profilPublik `json:"data"`
}

// Autentikasi menyimpan hash token sebagai pengganti token asli,
// sehingga kebocoran isi memori server tidak langsung membocorkan kredensial.
type Autentikasi struct {
	pemegang map[string]Identitas
}

// baruAutentikasi meng-hash seluruh token dari konfigurasi sekali saat startup.
func baruAutentikasi(kredensial map[string]Identitas) *Autentikasi {
	a := &Autentikasi{pemegang: make(map[string]Identitas, len(kredensial))}
	for token, id := range kredensial {
		a.pemegang[hashToken(token)] = id
	}
	return a
}

// hashToken menghitung sidik jari SHA-256 dari sebuah token.
func hashToken(token string) string {
	jumlah := sha256.Sum256([]byte(token))
	return hex.EncodeToString(jumlah[:])
}

// middleware memverifikasi kredensial lalu menempelkan identitas
// pada request context sebelum handler berikutnya dijalankan.
func (a *Autentikasi) middleware(berikut http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id, ok := a.periksa(r)
		if !ok {
			// 401: klien belum terautentikasi; header ini memintanya mengirim token.
			w.Header().Set("WWW-Authenticate", `Bearer realm="api"`)
			tulisGalat(w, http.StatusUnauthorized, "UNAUTHENTICATED",
				"lampirkan Bearer token pada header Authorization")
			return
		}
		ctx := context.WithValue(r.Context(), kunciKonteks{}, id)
		berikut.ServeHTTP(w, r.WithContext(ctx))
	})
}

// periksa menerima Bearer token di Authorization atau kunci di X-API-Key.
// Kedua jenis kredensial diverifikasi lewat tabel hash yang sama.
func (a *Autentikasi) periksa(r *http.Request) (Identitas, bool) {
	if kepala, token, ada := strings.Cut(r.Header.Get("Authorization"), " "); ada {
		if strings.EqualFold(kepala, "Bearer") {
			id, ok := a.pemegang[hashToken(strings.TrimSpace(token))]
			return id, ok
		}
	}
	if kunci := r.Header.Get("X-API-Key"); kunci != "" {
		id, ok := a.pemegang[hashToken(kunci)]
		return id, ok
	}
	return Identitas{}, false
}

// profil mengembalikan identitas pemanggil yang sudah terautentikasi.
func profil(w http.ResponseWriter, r *http.Request) {
	id := r.Context().Value(kunciKonteks{}).(Identitas)
	tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}

// panelAdmin menunjukkan beda 403: pemanggil dikenal, tetapi perannya kurang.
func panelAdmin(w http.ResponseWriter, r *http.Request) {
	id := r.Context().Value(kunciKonteks{}).(Identitas)
	if id.Peran != "admin" {
		tulisGalat(w, http.StatusForbidden, "FORBIDDEN", "akses panel hanya untuk admin")
		return
	}
	tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}

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() {
	autentikasi := baruAutentikasi(map[string]Identitas{
		"tok-budi-7f3a": {Nama: "budi", Peran: "user"},
		"tok-siti-9c2e": {Nama: "siti", Peran: "admin"},
	})

	mux := http.NewServeMux()
	mux.HandleFunc("GET /profil", profil)
	mux.HandleFunc("GET /panel-admin", panelAdmin)
	server := autentikasi.middleware(mux)

	// 401: tidak ada kredensial sama sekali.
	kirim(server, "GET", "/profil", "", nil)

	// 200: Bearer token dikenal.
	kirim(server, "GET", "/profil", "", map[string]string{
		"Authorization": "Bearer tok-budi-7f3a",
	})

	// 401: token tidak dikenal.
	kirim(server, "GET", "/profil", "", map[string]string{
		"Authorization": "Bearer tok-palsu",
	})

	// 401: skema Authorization bukan Bearer (contoh salah pakai Basic).
	kirim(server, "GET", "/profil", "", map[string]string{
		"Authorization": "Basic dXNlcjpwYXNzd29yZA==",
	})

	// 200: API key pada X-API-Key sebagai alternatif token.
	kirim(server, "GET", "/profil", "", map[string]string{
		"X-API-Key": "tok-siti-9c2e",
	})

	// 403: budi terautentikasi, tetapi panel admin bukan haknya.
	kirim(server, "GET", "/panel-admin", "", map[string]string{
		"Authorization": "Bearer tok-budi-7f3a",
	})

	// 200: siti adalah admin.
	kirim(server, "GET", "/panel-admin", "", map[string]string{
		"Authorization": "Bearer tok-siti-9c2e",
	})
}

// kirim mengeksekusi satu permintaan lalu mencetak status, header
// WWW-Authenticate, dan body — semuanya lewat httptest tanpa jaringan.
func kirim(server http.Handler, metode, target, body string, header map[string]string) {
	var pembaca io.Reader
	if body != "" {
		pembaca = strings.NewReader(body)
	}
	req := httptest.NewRequest(metode, target, pembaca)
	for k, v := range header {
		req.Header.Set(k, v)
	}
	rec := httptest.NewRecorder()
	server.ServeHTTP(rec, req)
	fmt.Printf("%-5s %-12s -> %d", metode, target, rec.Code)
	if tanya := rec.Header().Get("WWW-Authenticate"); tanya != "" {
		fmt.Printf(" (WWW-Authenticate: %s)", tanya)
	}
	fmt.Println()
	if rec.Body.Len() > 0 {
		fmt.Printf("   %s", rec.Body.String())
	}
}
