package main

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

// TugasInput memakai pointer field agar kehadiran tiap field
// bisa dibedakan dari nilai kosong setelah decode.
type TugasInput struct {
	Title    *string `json:"title"`
	Priority *int    `json:"priority"`
	DueDate  *string `json:"due_date"`
}

// FieldError membawa satu pelanggaran validasi: field mana dan alasannya.
type FieldError struct {
	Field string
	Pesan string
}

func (e FieldError) Error() string { return e.Field + " " + e.Pesan }

// Validate memeriksa seluruh aturan sekaligus lalu menggabungkan
// semua pelanggaran memakai errors.Join.
func (in TugasInput) Validate() error {
	var galat []error
	if in.Title == nil {
		galat = append(galat, FieldError{Field: "title", Pesan: "wajib diisi"})
	} else if len(*in.Title) < 3 {
		galat = append(galat, FieldError{
			Field: "title",
			Pesan: fmt.Sprintf("minimal 3 huruf, dapat %d", len(*in.Title)),
		})
	}
	if in.Priority == nil {
		galat = append(galat, FieldError{Field: "priority", Pesan: "wajib diisi"})
	} else if *in.Priority < 1 || *in.Priority > 5 {
		galat = append(galat, FieldError{
			Field: "priority",
			Pesan: fmt.Sprintf("harus 1-5, dapat %d", *in.Priority),
		})
	}
	if in.DueDate != nil {
		if _, err := time.Parse("2006-01-02", *in.DueDate); err != nil {
			galat = append(galat, FieldError{Field: "due_date", Pesan: "harus format YYYY-MM-DD"})
		}
	}
	return errors.Join(galat...)
}

// Handler menyimpan jumlah tugas yang berhasil dibuat.
type Handler struct {
	jumlah int
}

func (h *Handler) buat(w http.ResponseWriter, r *http.Request) {
	// Langkah 1: pastikan klien benar-benar mengirim JSON.
	if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
		tulisGalat(w, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE",
			"Content-Type harus application/json, dapat "+ct)
		return
	}

	// Langkah 2: batasi ukuran body sebelum didecode.
	r.Body = http.MaxBytesReader(w, r.Body, 1024)

	// Langkah 3: decode payload.
	var in TugasInput
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
		var terlaluBesar *http.MaxBytesError
		if errors.As(err, &terlaluBesar) {
			tulisGalat(w, http.StatusRequestEntityTooLarge, "BODY_TOO_LARGE",
				fmt.Sprintf("body melebihi batas %d byte", terlaluBesar.Limit))
			return
		}
		tulisGalat(w, http.StatusBadRequest, "INVALID_JSON", "body bukan JSON yang valid")
		return
	}

	// Langkah 4: validasi aturan bisnis secara menyeluruh.
	if err := in.Validate(); err != nil {
		tulisJSON(w, http.StatusBadRequest, galatBody{Error: galatRincian{
			Code:    "VALIDATION_ERROR",
			Message: "payload tidak lolos validasi",
			Fields:  petakanGalat(err),
		}})
		return
	}

	// Langkah 5: data sah — simpan lalu balas 201.
	h.jumlah++
	tulisJSON(w, http.StatusCreated, tugas{ID: h.jumlah, Title: *in.Title, Priority: *in.Priority})
}

type tugas struct {
	ID       int    `json:"id"`
	Title    string `json:"title"`
	Priority int    `json:"priority"`
}

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

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

// petakanGalat mengubah hasil errors.Join menjadi peta field -> pesan
// sehingga klien bisa memperbaiki seluruh field sekaligus.
func petakanGalat(err error) map[string]string {
	hasil := make(map[string]string)
	if gabungan, ok := err.(interface{ Unwrap() []error }); ok {
		for _, anak := range gabungan.Unwrap() {
			if fe, ok := anak.(FieldError); ok {
				hasil[fe.Field] = fe.Pesan
			}
		}
	}
	return hasil
}

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 := http.NewServeMux()
	mux.HandleFunc("POST /todos", h.buat)

	// Kasus 1: payload sah, respons 201.
	kirim(mux, "application/json", `{"title":"Belajar REST API","priority":2,"due_date":"2026-01-20"}`)

	// Kasus 2: body JSON tapi Content-Type salah, respons 415.
	kirim(mux, "text/plain", `{"title":"Belajar REST API","priority":2}`)

	// Kasus 3: JSON rusak, respons 400.
	kirim(mux, "application/json", `{"title":`)

	// Kasus 4: tiga field melanggar aturan sekaligus, respons 400 dengan detail.
	kirim(mux, "application/json", `{"title":"Bo","priority":9,"due_date":"besok"}`)

	// Kasus 5: body melebihi 1024 byte, respons 413.
	besar := `{"title":"` + strings.Repeat("a", 2000) + `"}`
	kirim(mux, "application/json", besar)
}

// kirim mengeksekusi satu POST lalu mencetak dump status dan body respons.
func kirim(mux *http.ServeMux, ct, body string) {
	req := httptest.NewRequest(http.MethodPost, "/todos", strings.NewReader(body))
	req.Header.Set("Content-Type", ct)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	fmt.Printf("POST /todos (Content-Type: %s) -> %d\n", ct, rec.Code)
	fmt.Printf("   %s", rec.Body.String())
}
