package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
)

// ErrNotFound adalah error domain: entitas dengan ID tersebut tidak ada.
// Pemanggil memeriksa error ini dengan errors.Is, bukan mengenali storage.
var ErrNotFound = errors.New("entitas tidak ditemukan")

// Repository adalah kontrak penyimpanan generik.
// Type parameter T membuat satu interface dipakai untuk semua entitas.
type Repository[T any] interface {
	Save(id string, entitas T) error
	Find(id string) (T, error)
	Delete(id string) error
}

// memoryRepo menyimpan entitas di map dalam memori.
type memoryRepo[T any] struct {
	mu   sync.Mutex
	data map[string]T
}

func NewMemoryRepo[T any]() *memoryRepo[T] {
	return &memoryRepo[T]{data: map[string]T{}}
}

func (r *memoryRepo[T]) Save(id string, entitas T) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.data[id] = entitas
	return nil
}

func (r *memoryRepo[T]) Find(id string) (T, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	v, ok := r.data[id]
	if !ok {
		var zero T
		return zero, fmt.Errorf("cari %q: %w", id, ErrNotFound)
	}
	return v, nil
}

func (r *memoryRepo[T]) Delete(id string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.data[id]; !ok {
		return fmt.Errorf("hapus %q: %w", id, ErrNotFound)
	}
	delete(r.data, id)
	return nil
}

// fileRepo menyimpan seluruh map sebagai satu berkas JSON.
type fileRepo[T any] struct {
	nama string
	mu   sync.Mutex
}

func NewFileRepo[T any](nama string) *fileRepo[T] {
	return &fileRepo[T]{nama: nama}
}

// muat membaca isi berkas; berkas yang belum ada dianggap kosong.
func (r *fileRepo[T]) muat() (map[string]T, error) {
	isi, err := os.ReadFile(r.nama)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return map[string]T{}, nil
		}
		return nil, err
	}
	var data map[string]T
	if err := json.Unmarshal(isi, &data); err != nil {
		return nil, err
	}
	return data, nil
}

func (r *fileRepo[T]) simpan(data map[string]T) error {
	isi, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(r.nama, isi, 0o600)
}

func (r *fileRepo[T]) Save(id string, entitas T) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	data, err := r.muat()
	if err != nil {
		return err
	}
	data[id] = entitas
	return r.simpan(data)
}

func (r *fileRepo[T]) Find(id string) (T, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	data, err := r.muat()
	if err != nil {
		var zero T
		return zero, err
	}
	v, ok := data[id]
	if !ok {
		var zero T
		return zero, fmt.Errorf("cari %q: %w", id, ErrNotFound)
	}
	return v, nil
}

func (r *fileRepo[T]) Delete(id string) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	data, err := r.muat()
	if err != nil {
		return err
	}
	if _, ok := data[id]; !ok {
		return fmt.Errorf("hapus %q: %w", id, ErrNotFound)
	}
	delete(data, id)
	return r.simpan(data)
}

// Pengguna adalah struct domain murni, tidak mengimport storage.
type Pengguna struct {
	Nama  string
	Email string
}

// LayananPengguna hanya mengenal interface Repository.
// Implementasi bisa ditukar tanpa menyentuh baris pun di sini.
type LayananPengguna struct {
	repo Repository[Pengguna]
}

func NewLayananPengguna(repo Repository[Pengguna]) *LayananPengguna {
	return &LayananPengguna{repo: repo}
}

func (l *LayananPengguna) Daftar(id, nama, email string) error {
	return l.repo.Save(id, Pengguna{Nama: nama, Email: email})
}

func (l *LayananPengguna) Cari(id string) (Pengguna, error) {
	return l.repo.Find(id)
}

func main() {
	// Implementasi pertama: in-memory.
	mem := NewLayananPengguna(NewMemoryRepo[Pengguna]())
	_ = mem.Daftar("u1", "Budi", "budi@example.com")
	_ = mem.Daftar("u2", "Siti", "siti@example.com")

	p, err := mem.Cari("u1")
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Printf("memory  : u1 = %s <%s>\n", p.Nama, p.Email)

	_, err = mem.Cari("u9")
	fmt.Println("memory  : cari u9 ->", err)
	fmt.Println("memory  : errors.Is(ErrNotFound) =", errors.Is(err, ErrNotFound))

	// Implementasi kedua: file JSON, layanan yang sama tanpa perubahan.
	berkas := filepath.Join(os.TempDir(), "repo-demo-pengguna.json")
	os.Remove(berkas) // mulai dari kondisi bersih agar demo dapat diulang

	file := NewLayananPengguna(NewFileRepo[Pengguna](berkas))
	_ = file.Daftar("u1", "Budi", "budi@example.com")

	q, err := file.Cari("u1")
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Printf("file    : u1 = %s <%s>\n", q.Nama, q.Email)

	isi, err := os.ReadFile(berkas)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("file    : isi %s:\n%s\n", filepath.Base(berkas), string(isi))
}
