package main

import (
	"errors"
	"fmt"
	"strings"
)

// Laporan adalah interface yang diketahui pemanggil.
// Format konkret (csv/json) disembunyikan di balik constructor.
type Laporan interface {
	Bentuk() string
	Render() string
}

// DataPembelian adalah input mentah yang sama untuk semua format.
type DataPembelian struct {
	Pembeli string
	Items   []string
}

// laporanCSV adalah tipe konkret yang tidak diekspor.
type laporanCSV struct {
	data DataPembelian
}

func (l laporanCSV) Bentuk() string { return "csv" }

func (l laporanCSV) Render() string {
	// koma di dalam item diganti titik koma agar tetap satu kolom.
	items := make([]string, len(l.data.Items))
	for i, v := range l.data.Items {
		items[i] = strings.ReplaceAll(v, ",", ";")
	}
	return "pembeli,items\n" + l.data.Pembeli + ",\"" + strings.Join(items, ",") + "\""
}

// laporanJSON adalah tipe konkret kedua dengan API render sendiri.
type laporanJSON struct {
	data DataPembelian
}

func (l laporanJSON) Bentuk() string { return "json" }

func (l laporanJSON) Render() string {
	quoted := make([]string, len(l.data.Items))
	for i, v := range l.data.Items {
		quoted[i] = fmt.Sprintf("%q", v)
	}
	return fmt.Sprintf(`{"pembeli": %q, "items": [%s]}`, l.data.Pembeli, strings.Join(quoted, ", "))
}

// registry memetakan format ke fungsi pabrik.
// Package lain bisa mendaftarkan format baru tanpa menyentuh NewLaporan.
var registry = map[string]func(DataPembelian) Laporan{
	"csv":  func(d DataPembelian) Laporan { return laporanCSV{d} },
	"json": func(d DataPembelian) Laporan { return laporanJSON{d} },
}

// NewLaporan adalah factory: menerima format, mengembalikan interface.
// Kesalahan format terdeteksi di sini, bukan di tangan pemanggil.
func NewLaporan(format string, data DataPembelian) (Laporan, error) {
	pabrik, ok := registry[format]
	if !ok {
		return nil, fmt.Errorf("%q: %w", format, ErrFormat)
	}
	return pabrik(data), nil
}

// ErrFormat adalah sentinel error untuk format yang tidak dikenal.
var ErrFormat = errors.New("format laporan tidak dikenal")

func main() {
	data := DataPembelian{Pembeli: "Budi", Items: []string{"Buku Go", "Pensil 2B"}}

	for _, format := range []string{"csv", "json", "pdf"} {
		lap, err := NewLaporan(format, data)
		if err != nil {
			fmt.Printf("[%s] gagal: %v\n", format, err)
			continue
		}
		fmt.Printf("[%s]\n%s\n", lap.Bentuk(), lap.Render())
	}
}
