package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
)

// ErrNotFound adalah sentinel error untuk data yang tidak ditemukan di
// penyimpanan. Handler memeriksa galat ini lewat errors.Is untuk
// memilih status 404.
var ErrNotFound = errors.New("data tidak ditemukan")

// ValidationError menjelaskan satu field yang melanggar aturan domain.
// Handler mengumpulkan galat ini menjadi body 400 yang rinci.
type ValidationError struct {
	Field   string
	Message string
}

// Error memenuhi kontrak interface error.
func (e ValidationError) Error() string {
	return fmt.Sprintf("field %s: %s", e.Field, e.Message)
}

// Kode galat yang stabil untuk konsumen API. Kode ini dirancang tidak
// berubah ketika implementasi internal berganti.
const (
	codeNotFound       = "NOT_FOUND"
	codeValidasi       = "VALIDATION_ERROR"
	codeJSONInvalid    = "INVALID_JSON"
	codeArgumenInvalid = "INVALID_ARGUMENT"
	codeBodyKebesaran  = "REQUEST_TOO_LARGE"
	codeMediaType      = "UNSUPPORTED_MEDIA_TYPE"
	codeInternal       = "INTERNAL_ERROR"
)

// errorEnvelope adalah bentuk body galat yang konsisten di seluruh
// endpoint: {"error":{"code":"...","message":"..."}}.
type errorEnvelope struct {
	Error errorBody `json:"error"`
}

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

// galatBody adalah galat transport HTTP yang sudah membawa status dan
// kode galatnya sendiri, misalnya body JSON yang rusak.
type galatBody struct {
	status  int
	code    string
	message string
}

func (e *galatBody) Error() string { return e.message }

// writeJSON menuliskan v sebagai respons JSON dengan status code yang
// diberikan. Content-Type diatur sebelum WriteHeader supaya header
// benar-benar terkirim ke klien.
func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

// writeError menerjemahkan error domain menjadi respons JSON konsisten.
// ValidationError menjadi 400 dengan peta fields; ErrNotFound menjadi
// 404; galat lain menjadi 500 tanpa membocorkan detail internal.
func writeError(w http.ResponseWriter, err error) {
	if fields := petaGalatValidasi(err); len(fields) > 0 {
		writeJSON(w, http.StatusBadRequest, errorEnvelope{Error: errorBody{
			Code:    codeValidasi,
			Message: "masukan tidak lolos validasi",
			Fields:  fields,
		}})
		return
	}
	status, code, message := terjemahkanGalat(err)
	writeJSON(w, status, errorEnvelope{Error: errorBody{Code: code, Message: message}})
}

// terjemahkanGalat memetakan error domain ke status HTTP, kode galat,
// dan pesan yang aman untuk klien.
func terjemahkanGalat(err error) (status int, code, message string) {
	if errors.Is(err, ErrNotFound) {
		return http.StatusNotFound, codeNotFound, err.Error()
	}
	// Pesan galat internal (koneksi, SQL, dsb.) tidak dibocorkan;
	// klien hanya menerima kode INTERNAL_ERROR yang stabil.
	return http.StatusInternalServerError, codeInternal, "galat internal server"
}

// petaGalatValidasi mengumpulkan seluruh ValidationError dari err,
// termasuk yang digabung lewat errors.Join, menjadi peta field→pesan.
func petaGalatValidasi(err error) map[string]string {
	var hasil map[string]string
	kumpulkanValidasi(err, &hasil)
	return hasil
}

// kumpulkanValidasi menelusuri err secara rekursif. errors.Join
// menghasilkan error dengan method Unwrap() []error, sehingga setiap
// anggota gabungan diperiksa satu per satu.
func kumpulkanValidasi(err error, hasil *map[string]string) {
	if err == nil {
		return
	}
	if joined, ok := err.(interface{ Unwrap() []error }); ok {
		for _, e := range joined.Unwrap() {
			kumpulkanValidasi(e, hasil)
		}
		return
	}
	var ve ValidationError
	if errors.As(err, &ve) {
		if *hasil == nil {
			*hasil = make(map[string]string)
		}
		(*hasil)[ve.Field] = ve.Message
	}
}

// tulisGalatBody menuliskan galat transport (galatBody) atau galat
// domain sebagai respons JSON. Handler cukup memanggil fungsi ini
// untuk semua jenis galat.
func tulisGalatBody(w http.ResponseWriter, err error) {
	var gb *galatBody
	if errors.As(err, &gb) {
		writeJSON(w, gb.status, errorEnvelope{Error: errorBody{
			Code:    gb.code,
			Message: gb.message,
		}})
		return
	}
	writeError(w, err)
}
