package main

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

// maxBodyBytes membatasi ukuran body JSON supaya satu permintaan tidak
// bisa menghabiskan memori server.
const maxBodyBytes = 1 << 20 // 1 MiB

// dataEnvelope membungkus data sukses dalam field "data" sehingga
// bentuk respons konsisten dengan konvensi kursus ini.
type dataEnvelope struct {
	Data any `json:"data"`
}

// listEnvelope membungkus hasil List dengan meta jumlah data.
type listEnvelope struct {
	Data []Item   `json:"data"`
	Meta listMeta `json:"meta"`
}

type listMeta struct {
	Count int `json:"count"`
}

// ItemHandler menerjemahkan permintaan HTTP menjadi pemanggilan
// ItemService, lalu menerjemahkan hasilnya kembali menjadi respons.
// Handler tidak memuat aturan bisnis apa pun.
type ItemHandler struct {
	service *ItemService
}

// Healthz melaporkan bahwa proses API hidup dan siap menerima
// permintaan; dipakai monitoring dan compose healthcheck.
func (h *ItemHandler) Healthz(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

// List menangani GET /items dan mengembalikan seluruh item.
func (h *ItemHandler) List(w http.ResponseWriter, r *http.Request) {
	items, err := h.service.List(r.Context())
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, listEnvelope{Data: items, Meta: listMeta{Count: len(items)}})
}

// Create menangani POST /items. Item baru dibalas 201 Created dengan
// header Location yang menunjuk ke URL item.
func (h *ItemHandler) Create(w http.ResponseWriter, r *http.Request) {
	var input ItemInput
	if err := bacaInput(w, r, &input); err != nil {
		tulisGalatBody(w, err)
		return
	}
	item, err := h.service.Create(r.Context(), input)
	if err != nil {
		writeError(w, err)
		return
	}
	w.Header().Set("Location", fmt.Sprintf("/items/%d", item.ID))
	writeJSON(w, http.StatusCreated, dataEnvelope{Data: item})
}

// Get menangani GET /items/{id}.
func (h *ItemHandler) Get(w http.ResponseWriter, r *http.Request) {
	id, err := bacaID(r)
	if err != nil {
		tulisGalatBody(w, err)
		return
	}
	item, err := h.service.GetByID(r.Context(), id)
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dataEnvelope{Data: item})
}

// Update menangani PUT /items/{id} dengan semantik penggantian penuh.
func (h *ItemHandler) Update(w http.ResponseWriter, r *http.Request) {
	id, err := bacaID(r)
	if err != nil {
		tulisGalatBody(w, err)
		return
	}
	var input ItemInput
	if err := bacaInput(w, r, &input); err != nil {
		tulisGalatBody(w, err)
		return
	}
	item, err := h.service.Update(r.Context(), id, input)
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dataEnvelope{Data: item})
}

// Delete menangani DELETE /items/{id} dan membalas 204 No Content
// tanpa body.
func (h *ItemHandler) Delete(w http.ResponseWriter, r *http.Request) {
	id, err := bacaID(r)
	if err != nil {
		tulisGalatBody(w, err)
		return
	}
	if err := h.service.Delete(r.Context(), id); err != nil {
		writeError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// bacaID membaca segmen {id} dari path lalu mengubahnya ke int64.
// Segmen bukan bilangan bulat dibalas galat 400 INVALID_ARGUMENT.
func bacaID(r *http.Request) (int64, error) {
	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
	if err != nil {
		return 0, &galatBody{
			status:  http.StatusBadRequest,
			code:    codeArgumenInvalid,
			message: "id harus bilangan bulat",
		}
	}
	return id, nil
}

// bacaInput memeriksa Content-Type, membatasi ukuran body, lalu
// mendekode JSON ke input sambil menolak field yang tidak dikenal.
// Galat transport dibungkus galatBody agar handler tinggal
// meneruskannya ke tulisGalatBody.
func bacaInput(w http.ResponseWriter, r *http.Request, input *ItemInput) error {
	ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
	if err != nil || ct != "application/json" {
		return &galatBody{
			status:  http.StatusUnsupportedMediaType,
			code:    codeMediaType,
			message: "Content-Type harus application/json",
		}
	}
	r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(input); err != nil {
		var maxErr *http.MaxBytesError
		switch {
		case errors.As(err, &maxErr):
			return &galatBody{
				status:  http.StatusRequestEntityTooLarge,
				code:    codeBodyKebesaran,
				message: fmt.Sprintf("body melebihi batas %d byte", maxErr.Limit),
			}
		case errors.Is(err, io.EOF):
			return &galatBody{
				status:  http.StatusBadRequest,
				code:    codeJSONInvalid,
				message: "body kosong; kirim JSON seperti {\"name\":\"Buku Go\",\"price\":75000}",
			}
		default:
			return &galatBody{
				status:  http.StatusBadRequest,
				code:    codeJSONInvalid,
				message: "body bukan JSON valid: " + err.Error(),
			}
		}
	}
	return nil
}
