package main

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

// Pengguna menyimpan data internal lengkap, termasuk field yang tidak
// boleh pernah muncul di respons.
type Pengguna struct {
	ID           int       `json:"id"`
	Name         string    `json:"name"`
	Email        string    `json:"email"`
	PasswordHash string    `json:"-"` // tidak pernah dikirim ke klien
	CreatedAt    time.Time `json:"created_at"`
}

// metaPaging menempelkan informasi halaman pada respons daftar.
type metaPaging struct {
	Page  int `json:"page"`
	Total int `json:"total"`
}

// amplopDaftar adalah respons ber-envelope untuk daftar pengguna.
type amplopDaftar struct {
	Data []Pengguna `json:"data"`
	Meta metaPaging `json:"meta"`
}

// amplopSatu adalah respons ber-envelope untuk satu pengguna.
type amplopSatu struct {
	Data Pengguna `json:"data"`
}

// amplopGalat adalah respons ber-envelope untuk kegagalan.
type amplopGalat struct {
	Error galatRincian `json:"error"`
}

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

// Handler menyimpan data pengguna di memori.
type Handler struct {
	data  []Pengguna
	waktu time.Time
}

func (h *Handler) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /users", h.daftar)
	mux.HandleFunc("POST /users", h.buat)
	mux.HandleFunc("GET /users/{id}", h.detail)
	return mux
}

func (h *Handler) daftar(w http.ResponseWriter, r *http.Request) {
	tulisJSON(w, http.StatusOK, amplopDaftar{
		Data: h.data,
		Meta: metaPaging{Page: 1, Total: len(h.data)},
	})
}

func (h *Handler) buat(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Name     string `json:"name"`
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		tulisJSON(w, http.StatusBadRequest, amplopGalat{Error: galatRincian{
			Code: "INVALID_JSON", Message: "body bukan JSON yang valid",
		}})
		return
	}
	u := Pengguna{
		ID:           len(h.data) + 1,
		Name:         in.Name,
		Email:        in.Email,
		PasswordHash: "hash:" + in.Password, // disimulasikan; hashing nyata dibahas di section Authentication dan Security
		CreatedAt:    h.waktu,
	}
	h.data = append(h.data, u)
	w.Header().Set("Location", fmt.Sprintf("/users/%d", u.ID))
	tulisJSON(w, http.StatusCreated, amplopSatu{Data: u})
}

func (h *Handler) detail(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		tulisJSON(w, http.StatusBadRequest, amplopGalat{Error: galatRincian{
			Code: "INVALID_ID", Message: "id harus angka",
		}})
		return
	}
	for _, u := range h.data {
		if u.ID == id {
			tulisJSON(w, http.StatusOK, amplopSatu{Data: u})
			return
		}
	}
	tulisJSON(w, http.StatusNotFound, amplopGalat{Error: galatRincian{
		Code: "NOT_FOUND", Message: fmt.Sprintf("pengguna dengan id %d tidak ditemukan", id),
	}})
}

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{waktu: time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)}
	h.data = append(h.data, Pengguna{
		ID: 1, Name: "Budi", Email: "budi@example.com",
		PasswordHash: "hash:rahasia-budi", CreatedAt: h.waktu,
	})
	mux := h.routes()

	// Perbandingan dua bentuk respons untuk data yang sama.
	contoh := []Pengguna{h.data[0]}
	cetakBentuk("flat", contoh)
	cetakBentuk("envelope", amplopDaftar{Data: contoh, Meta: metaPaging{Page: 1, Total: 1}})

	kirim(mux, "POST", "/users", `{"name":"Siti","email":"siti@example.com","password":"rahasia-siti"}`)
	kirim(mux, "GET", "/users", "")
	kirim(mux, "GET", "/users/1", "")
	kirim(mux, "GET", "/users/9", "")
}

// cetakBentuk menulis satu nilai lewat json.NewEncoder ke recorder
// lalu mencetak header dan bodynya, sama seperti respons HTTP sungguhan.
func cetakBentuk(label string, v any) {
	rec := httptest.NewRecorder()
	tulisJSON(rec, http.StatusOK, v)
	fmt.Printf("bentuk %s (Content-Type: %s):\n%s", label, rec.Header().Get("Content-Type"), rec.Body.String())
}

// kirim mengeksekusi satu permintaan lalu mencetak dump status, Location, dan body.
func kirim(mux *http.ServeMux, metode, target, body string) {
	var pembaca io.Reader
	if body != "" {
		pembaca = strings.NewReader(body)
	}
	req := httptest.NewRequest(metode, target, pembaca)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	fmt.Printf("%s %s -> %d", metode, target, rec.Code)
	if loc := rec.Header().Get("Location"); loc != "" {
		fmt.Printf(" (Location: %s)", loc)
	}
	fmt.Println()
	if rec.Body.Len() > 0 {
		fmt.Printf("%s", rec.Body.String())
	}
}
