// handler HTTP untuk API katalog.
//
// Semua respons JSON dikirim lewat writeJSON agar Content-Type konsisten.
// Validasi input dilakukan sebelum menyentuh database: name wajib terisi,
// note opsional dengan batas 200 karakter.
package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

// item adalah satu baris tabel items; tag json menentukan bentuk respons.
type item struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Note      string    `json:"note"`
	CreatedAt time.Time `json:"created_at"`
}

// itemBaru adalah bentuk body request POST /api/items.
type itemBaru struct {
	Name string `json:"name"`
	Note string `json:"note"`
}

// batasCatatan membatasi panjang field note agar data tetap wajar.
const batasCatatan = 200

// writeJSON menulis respons JSON dengan status HTTP yang diberikan.
func writeJSON(w http.ResponseWriter, status int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(data)
}

// tulisError mengirim pesan error standar berbentuk {"error": "..."}.
func tulisError(w http.ResponseWriter, status int, pesan string) {
	writeJSON(w, status, map[string]string{"error": pesan})
}

// handleHealthz memastikan aplikasi DAN database sama-sama hidup.
func handleHealthz(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var satu int
		if err := db.QueryRowContext(r.Context(), `SELECT 1`).Scan(&satu); err != nil {
			tulisError(w, http.StatusServiceUnavailable, "database tidak merespons")
			return
		}
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	}
}

// handleListItems mengembalikan seluruh item sebagai array JSON.
func handleListItems(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.QueryContext(r.Context(),
			`SELECT id, name, note, created_at FROM items ORDER BY id`)
		if err != nil {
			tulisError(w, http.StatusInternalServerError, "gagal membaca items")
			return
		}
		defer rows.Close()

		items := []item{}
		for rows.Next() {
			var it item
			if err := rows.Scan(&it.ID, &it.Name, &it.Note, &it.CreatedAt); err != nil {
				tulisError(w, http.StatusInternalServerError, "gagal membaca baris item")
				return
			}
			items = append(items, it)
		}
		writeJSON(w, http.StatusOK, items)
	}
}

// handleCreateItem memvalidasi body lalu menyimpan item baru.
func handleCreateItem(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Body dibatasi 1 MB agar request besar tidak menghabiskan memori.
		var body itemBaru
		if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
			tulisError(w, http.StatusBadRequest, "body JSON tidak valid")
			return
		}

		name := strings.TrimSpace(body.Name)
		if name == "" {
			tulisError(w, http.StatusBadRequest, "name wajib diisi")
			return
		}
		if utf8.RuneCountInString(body.Note) > batasCatatan {
			tulisError(w, http.StatusBadRequest, "note maksimal 200 karakter")
			return
		}

		var it item
		err := db.QueryRowContext(r.Context(),
			`INSERT INTO items (name, note) VALUES ($1, $2)
             RETURNING id, name, note, created_at`,
			name, body.Note,
		).Scan(&it.ID, &it.Name, &it.Note, &it.CreatedAt)
		if err != nil {
			tulisError(w, http.StatusInternalServerError, "gagal menyimpan item")
			return
		}
		writeJSON(w, http.StatusCreated, it)
	}
}

// handleGetItem mengembalikan satu item berdasarkan {id} di path.
func handleGetItem(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
		if err != nil {
			tulisError(w, http.StatusBadRequest, "id tidak valid")
			return
		}

		var it item
		err = db.QueryRowContext(r.Context(),
			`SELECT id, name, note, created_at FROM items WHERE id = $1`, id,
		).Scan(&it.ID, &it.Name, &it.Note, &it.CreatedAt)
		if errors.Is(err, sql.ErrNoRows) {
			tulisError(w, http.StatusNotFound, "item tidak ditemukan")
			return
		}
		if err != nil {
			tulisError(w, http.StatusInternalServerError, "gagal membaca item")
			return
		}
		writeJSON(w, http.StatusOK, it)
	}
}
