package main

import (
	"bytes"
	"cmp"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"slices"
	"strconv"
	"strings"
)

// Item adalah satu baris data katalog tugas.
type Item struct {
	ID       int    `json:"id"`
	Title    string `json:"title"`
	Priority int    `json:"priority"`
}

// metaHalaman merangkum informasi pagination pada respons.
type metaHalaman struct {
	Page  int `json:"page"`
	Limit int `json:"limit"`
	Total int `json:"total"`
	Pages int `json:"pages"`
}

type responsList struct {
	Data []Item      `json:"data"`
	Meta metaHalaman `json:"meta"`
}

// kolomDiizinkan adalah whitelist kolom sorting.
// Kolom di luar daftar ini ditolak dan diganti kolom default.
var kolomDiizinkan = map[string]bool{
	"id":       true,
	"title":    true,
	"priority": true,
}

// Handler melayani GET /items dengan filter, sorting, dan pagination.
type Handler struct {
	items []Item
}

func (h *Handler) daftar(w http.ResponseWriter, r *http.Request) {
	// Langkah 1: filter judul memakai substring (case-insensitive).
	filter := strings.ToLower(r.URL.Query().Get("q"))
	hasil := make([]Item, 0, len(h.items))
	for _, it := range h.items {
		if filter == "" || strings.Contains(strings.ToLower(it.Title), filter) {
			hasil = append(hasil, it)
		}
	}

	// Langkah 2: sorting — kolom harus lolos whitelist, arah hanya asc/desc.
	kolom := r.URL.Query().Get("sort")
	if !kolomDiizinkan[kolom] {
		kolom = "id"
	}
	arah := 1
	if r.URL.Query().Get("order") == "desc" {
		arah = -1
	}
	slices.SortStableFunc(hasil, func(a, b Item) int {
		var banding int
		switch kolom {
		case "title":
			banding = strings.Compare(a.Title, b.Title)
		case "priority":
			banding = cmp.Compare(a.Priority, b.Priority)
		default:
			banding = cmp.Compare(a.ID, b.ID)
		}
		return arah * banding
	})

	// Langkah 3: pagination — nilai tidak valid jatuh ke default.
	halaman := parseAngka(r.URL.Query().Get("page"), 1)
	batas := parseAngka(r.URL.Query().Get("limit"), 10)
	total := len(hasil)
	jumlahHalaman := (total + batas - 1) / batas
	if jumlahHalaman < 1 {
		jumlahHalaman = 1
	}
	awal := (halaman - 1) * batas
	if awal > total {
		awal = total
	}
	akhir := awal + batas
	if akhir > total {
		akhir = total
	}

	// Langkah 4: Link header memberi tahu halaman tetangga,
	// disusun dari parameter yang sudah tervalidasi.
	if halaman < jumlahHalaman {
		w.Header().Add("Link", fmt.Sprintf(`<%s>; rel="next"`, alamat(r, halaman+1, batas, kolom, arah)))
	}
	if halaman > 1 {
		w.Header().Add("Link", fmt.Sprintf(`<%s>; rel="prev"`, alamat(r, halaman-1, batas, kolom, arah)))
	}

	tulisJSON(w, http.StatusOK, responsList{
		Data: hasil[awal:akhir],
		Meta: metaHalaman{Page: halaman, Limit: batas, Total: total, Pages: jumlahHalaman},
	})
}

// parseAngka mengubah parameter query menjadi angka; input tidak
// valid atau di bawah satu menghasilkan nilai default.
func parseAngka(s string, standar int) int {
	n, err := strconv.Atoi(s)
	if err != nil || n < 1 {
		return standar
	}
	return n
}

// alamat menyusun URL halaman tetangga dari parameter tervalidasi.
func alamat(r *http.Request, halaman, batas int, kolom string, arah int) string {
	v := url.Values{}
	v.Set("page", strconv.Itoa(halaman))
	v.Set("limit", strconv.Itoa(batas))
	v.Set("sort", kolom)
	if arah < 0 {
		v.Set("order", "desc")
	}
	if q := r.URL.Query().Get("q"); q != "" {
		v.Set("q", q)
	}
	return r.URL.Path + "?" + v.Encode()
}

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{items: buatItem(25)}
	mux := http.NewServeMux()
	mux.HandleFunc("GET /items", h.daftar)

	fmt.Println("GET /items (tanpa query, default page=1 limit=10 sort=id asc):")
	ringkas(mux, "/items")

	fmt.Println()
	fmt.Println(`GET /items?page=2&limit=10&sort=priority&order=desc (halaman 2 dari 25 item, urut prioritas menurun):`)
	ringkas(mux, "/items?page=2&limit=10&sort=priority&order=desc")

	fmt.Println()
	fmt.Println(`GET /items?q=tugas%202&limit=5&sort=title (filter judul, body respons utuh):`)
	mentah(mux, "/items?q=tugas%202&limit=5&sort=title")

	fmt.Println()
	fmt.Println(`GET /items?page=abc&sort=nama (query tidak valid jatuh ke default):`)
	mentah(mux, "/items?page=abc&sort=nama")
}

// buatItem menyiapkan 25 item dengan prioritas unik dan deterministik.
func buatItem(n int) []Item {
	items := make([]Item, 0, n)
	for i := 1; i <= n; i++ {
		items = append(items, Item{
			ID:       i,
			Title:    fmt.Sprintf("Tugas %02d", i),
			Priority: (i*7)%n + 1,
		})
	}
	return items
}

// ringkas mengeksekusi permintaan lalu mencetak meta, Link header,
// dan urutan item tanpa mencetak body JSON mentah.
func ringkas(mux *http.ServeMux, target string) {
	req := httptest.NewRequest(http.MethodGet, target, nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	var out responsList
	if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
		fmt.Println("decode gagal:", err)
		return
	}
	fmt.Printf("status=%d link=%q\n", rec.Code, rec.Header().Get("Link"))
	fmt.Printf("meta : page=%d limit=%d total=%d pages=%d\n",
		out.Meta.Page, out.Meta.Limit, out.Meta.Total, out.Meta.Pages)
	bagian := make([]string, 0, len(out.Data))
	for _, it := range out.Data {
		bagian = append(bagian, fmt.Sprintf("#%02d(p%d)", it.ID, it.Priority))
	}
	fmt.Println("item : " + strings.Join(bagian, " "))
}

// mentah mengeksekusi permintaan lalu mencetak body respons utuh
// dengan indentasi hasil json.Indent.
func mentah(mux *http.ServeMux, target string) {
	req := httptest.NewRequest(http.MethodGet, target, nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	fmt.Printf("status=%d link=%q\n", rec.Code, rec.Header().Get("Link"))
	var rapi bytes.Buffer
	if err := json.Indent(&rapi, rec.Body.Bytes(), "", "  "); err != nil {
		fmt.Println("indent gagal:", err)
		return
	}
	fmt.Print(rapi.String())
}
