package main

import (
	"context"
	"fmt"
	"maps"
	"slices"
	"strings"
	"sync"
)

// Kode status span, meniru go.opentelemetry.io/otel/codes.
const (
	statusOK    = "ok"
	statusError = "error"
)

// traceIDDemo bernilai tetap agar output deterministik; produksi
// membangkitkan trace ID acak unik per request.
const traceIDDemo = "4bf92f3577b34da6a3ce929d0e0e4736"

// kunciSpan adalah kunci context privat untuk span aktif.
type kunciSpan struct{}

// Span merekam satu unit kerja: dibuat Tracer.Start, ditutup span.End.
type Span struct {
	provider    *TracerProvider
	nama        string
	spanID      string
	indukID     string
	traceID     string
	statusKode  string
	statusPesan string
	attrs       map[string]string
}

// SetAttributes menyimpan pasangan kunci-nilai pada span.
func (s *Span) SetAttributes(pasangan ...string) {
	for i := 0; i+1 < len(pasangan); i += 2 {
		if s.attrs == nil {
			s.attrs = map[string]string{}
		}
		s.attrs[pasangan[i]] = pasangan[i+1]
	}
}

// SetStatus menandai span sukses atau gagal beserta deskripsinya.
func (s *Span) SetStatus(kode, pesan string) {
	s.statusKode = kode
	s.statusPesan = pesan
}

// End menutup span dan menyerahkannya ke TracerProvider — pada SDK
// sungguhan, titik ini memasukkan span ke antrean exporter.
func (s *Span) End() {
	s.provider.catat(s)
}

// Tracer membuat span dalam lingkup satu nama instrumentasi.
type Tracer struct {
	provider *TracerProvider
}

// TracerProvider adalah pusat SDK: sumber tracer dan penampung span.
type TracerProvider struct {
	mu     sync.Mutex
	spans  []*Span
	urutan int
}

// TracerProviderBaru membuat provider siap pakai.
func TracerProviderBaru() *TracerProvider {
	return &TracerProvider{}
}

// Tracer mengembalikan tracer dengan nama instrumentasi tertentu.
func (p *TracerProvider) Tracer(nama string) *Tracer {
	return &Tracer{provider: p}
}

// Start membuat span baru sebagai anak span aktif pada ctx dan
// mengembalikan context yang membawa span tersebut.
func (t *Tracer) Start(ctx context.Context, nama string) (context.Context, *Span) {
	induk := spanDariContext(ctx)

	t.provider.mu.Lock()
	t.provider.urutan++
	nomor := t.provider.urutan
	t.provider.mu.Unlock()

	s := &Span{
		provider:   t.provider,
		nama:       nama,
		spanID:     fmt.Sprintf("%016x", nomor),
		traceID:    traceIDDemo,
		statusKode: statusOK,
	}
	if induk != nil {
		s.indukID = induk.spanID
		s.traceID = induk.traceID
	}
	return context.WithValue(ctx, kunciSpan{}, s), s
}

// catat menyimpan span selesai; menggantikan exporter pada demo ini.
func (p *TracerProvider) catat(s *Span) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.spans = append(p.spans, s)
}

// Cetak menampilkan seluruh span terekam dalam urutan End.
func (p *TracerProvider) Cetak() {
	p.mu.Lock()
	defer p.mu.Unlock()
	for i, s := range p.spans {
		induk := s.indukID
		if induk == "" {
			induk = "(root)"
		}
		status := s.statusKode
		if s.statusPesan != "" {
			status += ": " + s.statusPesan
		}
		var attrs []string
		for _, k := range slices.Sorted(maps.Keys(s.attrs)) {
			attrs = append(attrs, k+"="+s.attrs[k])
		}
		fmt.Printf("%d. %s span=%s induk=%s status=%s attrs=[%s]\n",
			i+1, s.nama, s.spanID, induk, status, strings.Join(attrs, ", "))
	}
}

// spanDariContext mengambil span aktif dari ctx; nil bila belum ada span.
func spanDariContext(ctx context.Context) *Span {
	s, _ := ctx.Value(kunciSpan{}).(*Span)
	return s
}

func main() {
	provider := TracerProviderBaru()
	tracer := provider.Tracer("layanan-pesanan")

	// Pekerjaan berada di fungsi terpisah agar seluruh defer span.End()
	// selesai sebelum provider mencetak hasilnya — pada SDK sungguhan,
	// peran serupa dijalankan provider.Shutdown saat aplikasi berhenti.
	if err := tanganiRequest(tracer); err != nil {
		fmt.Println("request gagal:", err)
	}

	fmt.Println("--- span terekam, urutan End ---")
	provider.Cetak()
}

// tanganiRequest adalah span root: pola Start lalu defer End.
func tanganiRequest(tracer *Tracer) error {
	ctx, span := tracer.Start(context.Background(), "POST /pesanan")
	defer span.End()
	span.SetAttributes("http.method", "POST", "http.rute", "/pesanan")

	if err := validasiPesanan(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// validasiPesanan membuat span anak dan meneruskan ctx-nya.
func validasiPesanan(ctx context.Context, tracer *Tracer) error {
	ctx, span := tracer.Start(ctx, "validasiPesanan")
	defer span.End()

	span.SetAttributes("pesanan.id", "42")

	if err := cekAlamat(ctx, tracer); err != nil {
		span.SetStatus(statusError, err.Error())
		return err
	}
	return nil
}

// cekAlamat adalah span cucu: bukti ctx membawa span induknya.
func cekAlamat(ctx context.Context, tracer *Tracer) error {
	_, span := tracer.Start(ctx, "cekAlamat")
	defer span.End()

	span.SetAttributes("alamat.field", "alamat_kirim")

	err := fmt.Errorf("alamat kosong")
	span.SetStatus(statusError, err.Error())
	return err
}
