package main

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

// masukanPesanan adalah payload pembuatan pesanan.
type masukanPesanan struct {
	Email  string `json:"email"`
	Jumlah *int   `json:"jumlah"`
}

// Pesanan adalah representasi resource yang dikirim ke klien.
type Pesanan struct {
	ID     int    `json:"id"`
	Email  string `json:"email"`
	Jumlah int    `json:"jumlah"`
	Status string `json:"status"`
}

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

// Handler menyimpan pesanan dan status pembayarannya.
type Handler struct {
	pesanan []Pesanan
}

func (h *Handler) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /pesanan", h.buat)
	mux.HandleFunc("GET /pesanan/{id}", h.detail)
	mux.HandleFunc("POST /pesanan/{id}/bayar", h.bayar)
	mux.HandleFunc("GET /laporan-penjualan", h.laporan)
	return mux
}

// buat memvalidasi payload; seluruh pelanggaran field dilaporkan sekaligus
// di dalam details sebagai peta field ke alasan penolakan.
func (h *Handler) buat(w http.ResponseWriter, r *http.Request) {
	var in masukanPesanan
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		tulisGalat(w, http.StatusBadRequest, "INVALID_JSON", "body bukan JSON yang valid")
		return
	}
	details := map[string]string{}
	if !strings.Contains(in.Email, "@") {
		details["email"] = "harus berisi alamat email yang valid"
	}
	if in.Jumlah == nil || *in.Jumlah <= 0 {
		details["jumlah"] = "harus berupa angka lebih besar dari 0"
	}
	if len(details) > 0 {
		tulisJSON(w, http.StatusBadRequest, galatBody{Error: galatRincian{
			Code:    "VALIDATION_ERROR",
			Message: "payload tidak lolos validasi",
			Details: details,
		}})
		return
	}
	p := Pesanan{ID: len(h.pesanan) + 1, Email: in.Email, Jumlah: *in.Jumlah, Status: "MENUNGGU"}
	h.pesanan = append(h.pesanan, p)
	w.Header().Set("Location", fmt.Sprintf("/pesanan/%d", p.ID))
	tulisJSON(w, http.StatusCreated, amplopSatu{Data: p})
}

// detail mengirim satu pesanan berdasarkan ID.
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.pesanan {
		if p.ID == nomor {
			tulisJSON(w, http.StatusOK, amplopSatu{Data: p})
			return
		}
	}
	tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
		fmt.Sprintf("pesanan %d tidak ditemukan", nomor))
}

// bayar menolak pembayaran kedua dengan 409 dan status saat ini di details.
func (h *Handler) bayar(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 := range h.pesanan {
		if h.pesanan[i].ID != nomor {
			continue
		}
		if h.pesanan[i].Status == "LUNAS" {
			tulisJSON(w, http.StatusConflict, galatBody{Error: galatRincian{
				Code:    "SUDAH_DIBAYAR",
				Message: "pesanan sudah lunas, tidak bisa dibayar dua kali",
				Details: map[string]string{"status": "LUNAS"},
			}})
			return
		}
		h.pesanan[i].Status = "LUNAS"
		tulisJSON(w, http.StatusOK, amplopSatu{Data: h.pesanan[i]})
		return
	}
	tulisGalat(w, http.StatusNotFound, "NOT_FOUND",
		fmt.Sprintf("pesanan %d tidak ditemukan", nomor))
}

// bacaLaporan mensimulasikan kegagalan infrastruktur di balik endpoint.
func bacaLaporan() error {
	return errors.New("koneksi database terputus (dial tcp 10.0.0.5:5432: i/o timeout)")
}

// catatInternal mencatat detail kegagalan hanya di sisi server.
func catatInternal(galat error) {
	fmt.Printf("[log internal] %v\n", galat)
}

// laporan mengirim pesan publik yang aman; detail galat hanya masuk log.
func (h *Handler) laporan(w http.ResponseWriter, r *http.Request) {
	if err := bacaLaporan(); err != nil {
		catatInternal(err)
		tulisGalat(w, http.StatusInternalServerError, "INTERNAL_ERROR",
			"terjadi kesalahan internal, coba beberapa saat lagi")
		return
	}
	tulisJSON(w, http.StatusOK, map[string]string{"data": "laporan penjualan"})
}

type galatBody struct {
	Error galatRincian `json:"error"`
}

type galatRincian struct {
	Code    string            `json:"code"`
	Message string            `json:"message"`
	Details map[string]string `json:"details,omitempty"`
}

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{}
	mux := h.routes()

	// 400: dua field gagal validasi sekaligus, tercantum di details.
	kirim(mux, "POST", "/pesanan", `{"email":"budi","jumlah":0}`)

	// 201: payload valid, Location menunjuk pesanan baru.
	kirim(mux, "POST", "/pesanan", `{"email":"budi@example.com","jumlah":2}`)

	// 200: pembayaran pertama berhasil.
	kirim(mux, "POST", "/pesanan/1/bayar", "")

	// 409: pembayaran kedua ditolak, details memberi konteks state.
	kirim(mux, "POST", "/pesanan/1/bayar", "")

	// 404: pesanan tidak ada.
	kirim(mux, "GET", "/pesanan/99", "")

	// 500: kegagalan internal — klien hanya menerima pesan generik,
	// detail lengkap tercatat di log server.
	kirim(mux, "GET", "/laporan-penjualan", "")
}

// kirim mengeksekusi satu permintaan lalu mencetak status, header Location,
// dan body — memakai httptest agar hasil selalu dapat direproduksi.
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("%-5s %-20s -> %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())
	}
}
