package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"sync/atomic"
)

// ctxKunciRequestID adalah tipe kunci context yang tidak diekspor sehingga
// hanya helper di berkas ini yang dapat membaca request ID dari context.
type ctxKunciRequestID struct{}

// PembuatID adalah pabrik request ID; bertipe fungsi agar mudah diganti
// generator deterministik saat pengujian.
type PembuatID func() string

// buatIDAcak adalah versi produksi: 8 byte acak dari crypto/rand, dienkode
// hexadecimal. Demo memakai penghitung berurutan agar output deterministik.
func buatIDAcak() (string, error) {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

// penghitungID menghasilkan req-001, req-002, ... untuk keperluan demo.
func penghitungID() PembuatID {
	var n atomic.Int64
	return func() string {
		return fmt.Sprintf("req-%03d", n.Add(1))
	}
}

// requestIDDariContext mengambil request ID yang disimpan middleware.
func requestIDDariContext(ctx context.Context) string {
	id, _ := ctx.Value(ctxKunciRequestID{}).(string)
	return id
}

// middlewareRequestID membaca header X-Request-ID (atau membuat ID baru),
// menyimpannya di context, dan menyalinnya ke header respons.
func middlewareRequestID(pembuat PembuatID, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if id == "" {
			id = pembuat()
		}
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), ctxKunciRequestID{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey && len(groups) == 0 {
				return slog.String(slog.TimeKey, "2026-01-15T08:00:00Z")
			}
			return a
		},
	}))
	klien := &http.Client{}

	// Layanan pesanan: menerima header X-Request-ID hasil propagasi gateway.
	layananPesanan := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		logger.Info("pesanan diproses", "request_id", id, "layanan", "pesanan")
		fmt.Fprintln(w, "pesanan siap")
	}))
	defer layananPesanan.Close()

	// Gateway: middleware membuat/mewarisi request ID, lalu meneruskannya.
	pembuat := penghitungID()
	gateway := httptest.NewServer(middlewareRequestID(pembuat,
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			id := requestIDDariContext(r.Context())
			logger.Info("request diterima", "request_id", id, "layanan", "gateway")

			req, err := http.NewRequestWithContext(r.Context(), http.MethodGet,
				layananPesanan.URL+"/proses", nil)
			if err != nil {
				panic(err)
			}
			req.Header.Set("X-Request-ID", id) // propagasi lintas layanan

			resp, err := klien.Do(req)
			if err != nil {
				logger.Error("panggilan layanan gagal", "request_id", id, "err", err)
				http.Error(w, "layanan pesanan tak tersedia", http.StatusBadGateway)
				return
			}
			defer resp.Body.Close()
			isi, _ := io.ReadAll(resp.Body)

			logger.Info("respons layanan pesanan", "request_id", id,
				"status", resp.StatusCode, "isi", string(isi))
			fmt.Fprintln(w, "OK")
		})))
	defer gateway.Close()

	fmt.Println("--- request pertama: gateway membuat request ID ---")
	resp, err := klien.Get(gateway.URL)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))

	fmt.Println("--- request kedua: header X-Request-ID dari pemanggil ---")
	req, err := http.NewRequest(http.MethodGet, gateway.URL, nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("X-Request-ID", "ext-9f2c")
	resp, err = klien.Do(req)
	if err != nil {
		panic(err)
	}
	resp.Body.Close()
	fmt.Printf("respons klien: status=%d X-Request-ID=%s\n",
		resp.StatusCode, resp.Header.Get("X-Request-ID"))
}
