package main

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

// Identitas pemanggil yang dibawa dari proses autentikasi.
type Identitas struct {
	Nama  string
	Peran string
}

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

// Dokumen adalah resource yang dimiliki tepat satu pengguna.
type Dokumen struct {
	ID      int    `json:"id"`
	Judul   string `json:"judul"`
	Pemilik string `json:"pemilik"`
}

// amplopDaftar membungkus daftar dokumen sesuai konvensi respons section ini.
type amplopDaftar struct {
	Data []Dokumen `json:"data"`
}

type amplopSatu struct {
	Data Dokumen `json:"data"`
}

// Handler menyimpan data, tabel izin, dan tabel hash token.
type Handler struct {
	dokumen []Dokumen
	izin    map[string][]string
	token   map[string]Identitas
}

// petaToken mengubah pasangan token-identitas menjadi tabel hash SHA-256,
// pola penyimpanan kredensial dari chapter Authentication.
func petaToken(kredensial map[string]Identitas) map[string]Identitas {
	peta := make(map[string]Identitas, len(kredensial))
	for token, id := range kredensial {
		jumlah := sha256.Sum256([]byte(token))
		peta[hex.EncodeToString(jumlah[:])] = id
	}
	return peta
}

// routes mendaftarkan endpoint; tiap endpoint dibungkus wajibPeran
// dengan pola yang sama sehingga izin terkumpul pada satu tabel.
func (h *Handler) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /dokumen", h.wajibPeran("GET /dokumen", h.daftar))
	mux.HandleFunc("GET /dokumen/{id}", h.wajibPeran("GET /dokumen/{id}", h.detail))
	mux.HandleFunc("DELETE /dokumen/{id}", h.wajibPeran("DELETE /dokumen/{id}", h.hapus))
	return mux
}

// autentikasi memverifikasi token lalu menempelkan identitas di context.
func (h *Handler) autentikasi(berikut http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, token, _ := strings.Cut(r.Header.Get("Authorization"), " ")
		jumlah := sha256.Sum256([]byte(token))
		id, ok := h.token[hex.EncodeToString(jumlah[:])]
		if !ok {
			tulisGalat(w, http.StatusUnauthorized, "UNAUTHENTICATED", "token tidak dikenal")
			return
		}
		ctx := context.WithValue(r.Context(), kunciKonteks{}, id)
		berikut.ServeHTTP(w, r.WithContext(ctx))
	})
}

// wajibPeran memeriksa tabel izin: pola yang tidak terdaftar, atau peran
// yang tidak masuk daftar, langsung ditolak — deny by default.
func (h *Handler) wajibPeran(pola string, berikut http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Context().Value(kunciKonteks{}).(Identitas)
		if !slices.Contains(h.izin[pola], id.Peran) {
			tulisGalat(w, http.StatusForbidden, "FORBIDDEN",
				fmt.Sprintf("peran %s tidak diizinkan untuk %s", id.Peran, pola))
			return
		}
		berikut(w, r)
	}
}

// daftar menyaring data: pengguna biasa hanya menerima dokumen miliknya,
// admin menerima seluruh dokumen.
func (h *Handler) daftar(w http.ResponseWriter, r *http.Request) {
	id := r.Context().Value(kunciKonteks{}).(Identitas)
	terlihat := make([]Dokumen, 0, len(h.dokumen))
	for _, d := range h.dokumen {
		if id.Peran == "admin" || d.Pemilik == id.Nama {
			terlihat = append(terlihat, d)
		}
	}
	tulisJSON(w, http.StatusOK, amplopDaftar{Data: terlihat})
}

// detail menerapkan izin granular: hanya pemilik atau admin boleh membaca.
func (h *Handler) detail(w http.ResponseWriter, r *http.Request) {
	id := r.Context().Value(kunciKonteks{}).(Identitas)
	nomor, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		tulisGalat(w, http.StatusBadRequest, "INVALID_ID", "id harus angka")
		return
	}
	for _, d := range h.dokumen {
		if d.ID != nomor {
			continue
		}
		if d.Pemilik != id.Nama && id.Peran != "admin" {
			tulisGalat(w, http.StatusForbidden, "FORBIDDEN", "dokumen bukan milik Anda")
			return
		}
		tulisJSON(w, http.StatusOK, amplopSatu{Data: d})
		return
	}
	tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
		fmt.Sprintf("dokumen %d tidak ditemukan", nomor))
}

// hapus tidak memeriksa peran lagi — tabel izin sudah menjamin hanya admin.
func (h *Handler) hapus(w http.ResponseWriter, r *http.Request) {
	nomor, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		tulisGalat(w, http.StatusBadRequest, "INVALID_ID", "id harus angka")
		return
	}
	for i, d := range h.dokumen {
		if d.ID == nomor {
			h.dokumen = slices.Delete(h.dokumen, i, i+1)
			w.WriteHeader(http.StatusNoContent)
			return
		}
	}
	tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
		fmt.Sprintf("dokumen %d tidak ditemukan", nomor))
}

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() {
	h := &Handler{
		dokumen: []Dokumen{
			{ID: 1, Judul: "Laporan Penjualan", Pemilik: "budi"},
			{ID: 2, Judul: "Rencana Anggaran", Pemilik: "siti"},
			{ID: 3, Judul: "Notulen Rapat", Pemilik: "budi"},
		},
		izin: map[string][]string{
			"GET /dokumen":         {"user", "admin"},
			"GET /dokumen/{id}":    {"user", "admin"},
			"DELETE /dokumen/{id}": {"admin"},
		},
		token: petaToken(map[string]Identitas{
			"tok-budi-7f3a": {Nama: "budi", Peran: "user"},
			"tok-siti-9c2e": {Nama: "siti", Peran: "admin"},
		}),
	}
	server := h.autentikasi(h.routes())

	// Penyaringan data: budi hanya menerima dokumen miliknya.
	kirim(server, "GET", "/dokumen", "budi", "tok-budi-7f3a")

	// Penyaringan data: admin menerima seluruh dokumen.
	kirim(server, "GET", "/dokumen", "admin", "tok-siti-9c2e")

	// Izin granular: dokumen 2 milik siti, budi ditolak.
	kirim(server, "GET", "/dokumen/2", "budi", "tok-budi-7f3a")

	// Izin granular: admin boleh membaca dokumen milik siapa pun.
	kirim(server, "GET", "/dokumen/2", "admin", "tok-siti-9c2e")

	// Tabel izin: DELETE hanya untuk admin, budi ditolak 403.
	kirim(server, "DELETE", "/dokumen/1", "budi", "tok-budi-7f3a")

	// Tabel izin: admin berhasil menghapus dokumen 1.
	kirim(server, "DELETE", "/dokumen/1", "admin", "tok-siti-9c2e")

	// Efek penghapusan: dokumen 1 sudah tidak ada.
	kirim(server, "GET", "/dokumen/1", "admin", "tok-siti-9c2e")
}

// kirim mengeksekusi satu permintaan atas nama satu pemohon,
// lalu mencetak status dan body tanpa jaringan sungguhan.
func kirim(server http.Handler, metode, target, pemohon, token string) {
	req := httptest.NewRequest(metode, target, nil)
	req.Header.Set("Authorization", "Bearer "+token)
	rec := httptest.NewRecorder()
	server.ServeHTTP(rec, req)
	fmt.Printf("%-7s %-13s %-6s -> %d\n", metode, target, pemohon, rec.Code)
	if rec.Body.Len() > 0 {
		fmt.Printf("   %s", rec.Body.String())
	}
}
