package main

import (
	"encoding/json"
	"fmt"
	"maps"
	"net/http"
	"net/http/httptest"
	"slices"
	"strconv"
	"strings"
)

// skemaProduk menyalin components.schemas.Produk dari dokumen OpenAPI:
// peta field ke tipe JSON yang dijanjikan kepada seluruh klien.
var skemaProduk = map[string]string{
	"id":    "integer",
	"nama":  "string",
	"harga": "integer",
	"stok":  "integer",
}

// Produk adalah data yang disimpan server.
type Produk struct {
	ID    int
	Nama  string
	Harga int
	Stok  int
}

// produkTampil adalah representasi JSON satu produk.
type produkTampil struct {
	ID    int    `json:"id"`
	Nama  string `json:"nama"`
	Harga int    `json:"harga"`
	Stok  int    `json:"stok"`
}

// Handler melayani daftar dan detail produk dari data di memori.
type Handler struct {
	data []Produk
}

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

// daftar mengirim seluruh produk sebagai array datar.
func (h *Handler) daftar(w http.ResponseWriter, r *http.Request) {
	daftar := make([]produkTampil, 0, len(h.data))
	for _, p := range h.data {
		daftar = append(daftar, produkTampil{ID: p.ID, Nama: p.Nama, Harga: p.Harga, Stok: p.Stok})
	}
	tulisJSON(w, http.StatusOK, daftar)
}

// detail mengirim satu produk sesuai skema terdokumentasi.
func (h *Handler) detail(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 _, p := range h.data {
		if p.ID == nomor {
			tulisJSON(w, http.StatusOK,
				produkTampil{ID: p.ID, Nama: p.Nama, Harga: p.Harga, Stok: p.Stok})
			return
		}
	}
	tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
		fmt.Sprintf("produk %d tidak ditemukan", nomor))
}

// detailLama mensimulasikan server versi lama yang masih beredar:
// harga dikirim sebagai string dan field stok belum ada.
func detailLama(w http.ResponseWriter, r *http.Request) {
	tulisJSON(w, http.StatusOK, map[string]any{
		"id":    1,
		"nama":  "Buku Tulis",
		"harga": "4500",
	})
}

// cocokSkema memeriksa satu objek respons terhadap skema dokumen.
// Field diperiksa dalam urutan abjad agar laporan selalu sama urutannya.
func cocokSkema(objek map[string]any, skema map[string]string) []string {
	var selisih []string
	for _, nama := range slices.Sorted(maps.Keys(skema)) {
		nilai, ada := objek[nama]
		if !ada {
			selisih = append(selisih, fmt.Sprintf("field %s: hilang dari respons", nama))
			continue
		}
		if tipeJSON(nilai) != skema[nama] {
			selisih = append(selisih, fmt.Sprintf("field %s: skema %s, respons %s",
				nama, skema[nama], tipeJSON(nilai)))
		}
	}
	return selisih
}

// tipeJSON menyimpulkan nama tipe dari nilai hasil decode memakai json.Number
// agar tipe angka tidak lenyap menjadi float64.
func tipeJSON(nilai any) string {
	switch v := nilai.(type) {
	case json.Number:
		if _, err := v.Int64(); err == nil {
			return "integer"
		}
		return "number"
	case string:
		return "string"
	case bool:
		return "boolean"
	case []any:
		return "array"
	case map[string]any:
		return "object"
	case nil:
		return "null"
	}
	return "unknown"
}

// kirim mengeksekusi permintaan, mencetak respons, lalu membandingkan
// respons dengan skema — validator mini antara dokumen dan perilaku nyata.
func kirim(server http.Handler, metode, target string) {
	req := httptest.NewRequest(metode, target, nil)
	rec := httptest.NewRecorder()
	server.ServeHTTP(rec, req)
	fmt.Printf("%-5s %-12s -> %d\n", metode, target, rec.Code)
	fmt.Printf("   %s", rec.Body.String())
	switch v := decodeJSON(rec.Body.String()).(type) {
	case []any:
		for i, item := range v {
			objek, ok := item.(map[string]any)
			if !ok {
				fmt.Printf("   item %d: bukan objek\n", i+1)
				continue
			}
			laporkan(fmt.Sprintf("item %d", i+1), cocokSkema(objek, skemaProduk))
		}
	case map[string]any:
		laporkan("objek", cocokSkema(v, skemaProduk))
	}
}

// decodeJSON mengubah body respons menjadi nilai Go memakai json.Number.
func decodeJSON(body string) any {
	dec := json.NewDecoder(strings.NewReader(body))
	dec.UseNumber()
	var v any
	if err := dec.Decode(&v); err != nil {
		return nil
	}
	return v
}

// laporkan mencetak hasil pemeriksaan satu objek atau item.
func laporkan(nama string, selisih []string) {
	if len(selisih) == 0 {
		fmt.Printf("   %s: cocok dengan skema\n", nama)
		return
	}
	fmt.Printf("   %s: %d selisih dengan skema\n", nama, len(selisih))
	for _, s := range selisih {
		fmt.Printf("   - %s\n", s)
	}
}

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{data: []Produk{
		{ID: 1, Nama: "Buku Tulis", Harga: 4500, Stok: 12},
		{ID: 2, Nama: "Pensil", Harga: 2500, Stok: 40},
		{ID: 3, Nama: "Penghapus", Harga: 1500, Stok: 25},
	}}
	muxBaru := h.routes()

	// Server lama: deployment yang responsnya melenceng dari dokumentasi.
	muxLama := http.NewServeMux()
	muxLama.HandleFunc("GET /produk/{id}", detailLama)

	fmt.Println("== server baru: respons dibandingkan dengan skema ==")
	kirim(muxBaru, "GET", "/produk")
	kirim(muxBaru, "GET", "/produk/1")

	fmt.Println("== server lama: dokumentasi dan perilaku tidak lagi cocok ==")
	kirim(muxLama, "GET", "/produk/1")
}
