package main

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

// Produk adalah data internal; representasi tiap versi dipilih saat encoding.
type Produk struct {
	ID    int
	Nama  string
	Harga int
}

// produkV1 mempertahankan kontrak lama: tanpa field harga.
type produkV1 struct {
	ID   int    `json:"id"`
	Nama string `json:"nama"`
}

// produkV2 menambah field harga — perubahan additive di dalam versi baru.
type produkV2 struct {
	ID    int    `json:"id"`
	Nama  string `json:"nama"`
	Harga int    `json:"harga"`
}

// amplopV2 membungkus daftar versi 2 dengan informasi meta.
type amplopV2 struct {
	Data []produkV2 `json:"data"`
	Meta metaV2     `json:"meta"`
}

type metaV2 struct {
	Versi  string `json:"versi"`
	Jumlah int    `json:"jumlah"`
}

// Handler menyimpan data dan tanggal kedaluwarsa versi 1.
type Handler struct {
	produk []Produk
	sunset time.Time
}

func (h *Handler) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /v1/produk", h.daftarV1)
	mux.HandleFunc("GET /v2/produk", h.daftarV2)
	mux.HandleFunc("GET /produk", h.daftarNegosiasi)
	return mux
}

// daftarV1 mengirim array datar — bentuk yang dijanjikan sejak awal.
func (h *Handler) daftarV1(w http.ResponseWriter, r *http.Request) {
	daftar := make([]produkV1, 0, len(h.produk))
	for _, p := range h.produk {
		daftar = append(daftar, produkV1{ID: p.ID, Nama: p.Nama})
	}
	// Sunset memberi tahu klien kapan versi ini akan dihentikan.
	w.Header().Set("Sunset", h.sunset.Format(http.TimeFormat))
	tulisJSON(w, http.StatusOK, daftar)
}

// daftarV2 memakai envelope dan field harga: kontrak baru yang terpisah.
func (h *Handler) daftarV2(w http.ResponseWriter, r *http.Request) {
	daftar := make([]produkV2, 0, len(h.produk))
	for _, p := range h.produk {
		daftar = append(daftar, produkV2{ID: p.ID, Nama: p.Nama, Harga: p.Harga})
	}
	tulisJSON(w, http.StatusOK,
		amplopV2{Data: daftar, Meta: metaV2{Versi: "2", Jumlah: len(daftar)}})
}

// daftarNegosiasi memilih versi dari parameter version pada header Accept.
func (h *Handler) daftarNegosiasi(w http.ResponseWriter, r *http.Request) {
	if versiDariAccept(r.Header.Get("Accept")) == "2" {
		h.daftarV2(w, r)
		return
	}
	h.daftarV1(w, r)
}

// versiDariAccept membaca "version=" dari header Accept; default "1".
func versiDariAccept(accept string) string {
	_, sisa, ada := strings.Cut(accept, "version=")
	if !ada {
		return "1"
	}
	if titikKoma := strings.IndexByte(sisa, ';'); titikKoma >= 0 {
		sisa = sisa[:titikKoma]
	}
	return strings.TrimSpace(sisa)
}

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{
		produk: []Produk{
			{ID: 1, Nama: "Buku Tulis", Harga: 4500},
			{ID: 2, Nama: "Pensil", Harga: 2500},
		},
		sunset: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC),
	}
	mux := h.routes()

	// Versi di URL path: eksplisit, terlihat di log, mudah di-cache per versi.
	kirim(mux, "GET", "/v1/produk", "")
	kirim(mux, "GET", "/v2/produk", "")

	// Versi di header Accept: URL tetap sama, versi dinegosiasikan.
	kirim(mux, "GET", "/produk", "application/json;version=1")
	kirim(mux, "GET", "/produk", "application/json;version=2")

	// Tanpa parameter versi: default konservatif ke versi terlama.
	kirim(mux, "GET", "/produk", "")
}

// kirim mengeksekusi satu permintaan lalu mencetak status, header Sunset,
// dan body; nilai Accept ditampilkan agar strategi tiap baris terlihat.
func kirim(mux *http.ServeMux, metode, target, accept string) {
	req := httptest.NewRequest(metode, target, nil)
	if accept != "" {
		req.Header.Set("Accept", accept)
	}
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	label := accept
	if label == "" {
		label = "-"
	}
	fmt.Printf("%-5s %-12s %-28s -> %d", metode, target, label, rec.Code)
	if sunset := rec.Header().Get("Sunset"); sunset != "" {
		fmt.Printf(" (Sunset: %s)", sunset)
	}
	fmt.Println()
	if rec.Body.Len() > 0 {
		fmt.Printf("   %s", rec.Body.String())
	}
}
