package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"slices"
	"sync"
)

// ItemRepository adalah kontrak penyimpanan item. PostgresItemRepository
// mengimplementasikannya di atas PostgreSQL, sedangkan
// MemoryItemRepository menyimpan data di map untuk pengujian tanpa
// database. Handler dan service hanya mengenal interface ini.
type ItemRepository interface {
	Create(ctx context.Context, item Item) (Item, error)
	GetByID(ctx context.Context, id int64) (Item, error)
	List(ctx context.Context) ([]Item, error)
	Update(ctx context.Context, item Item) (Item, error)
	Delete(ctx context.Context, id int64) error
}

// PostgresItemRepository menyimpan item di PostgreSQL melalui
// database/sql dengan driver pgx. Seluruh nilai dikirim sebagai
// parameter $1, $2, ... sehingga bebas SQL injection.
type PostgresItemRepository struct {
	db *sql.DB
}

// NewPostgresItemRepository membangun repository di atas pool *sql.DB
// yang sudah terbuka.
func NewPostgresItemRepository(db *sql.DB) *PostgresItemRepository {
	return &PostgresItemRepository{db: db}
}

// Create menyimpan item baru; id dibuat oleh sequence PostgreSQL lalu
// dikembalikan lewat RETURNING tanpa query kedua.
func (r *PostgresItemRepository) Create(ctx context.Context, item Item) (Item, error) {
	err := r.db.QueryRowContext(ctx,
		`INSERT INTO items (name, price, created_at) VALUES ($1, $2, $3) RETURNING id`,
		item.Name, item.Price, item.CreatedAt,
	).Scan(&item.ID)
	if err != nil {
		return Item{}, fmt.Errorf("menyimpan item: %w", err)
	}
	return item, nil
}

// GetByID membaca satu item; sql.ErrNoRows diterjemahkan menjadi
// ErrNotFound supaya handler cukup memeriksa sentinel error.
func (r *PostgresItemRepository) GetByID(ctx context.Context, id int64) (Item, error) {
	var item Item
	err := r.db.QueryRowContext(ctx,
		`SELECT id, name, price, created_at FROM items WHERE id = $1`, id,
	).Scan(&item.ID, &item.Name, &item.Price, &item.CreatedAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return Item{}, fmt.Errorf("item %d: %w", id, ErrNotFound)
		}
		return Item{}, fmt.Errorf("membaca item %d: %w", id, err)
	}
	return item, nil
}

// List membaca seluruh item terurut naik berdasarkan id. rows ditutup
// dengan defer agar koneksi kembali ke pool meski terjadi galat.
func (r *PostgresItemRepository) List(ctx context.Context) ([]Item, error) {
	rows, err := r.db.QueryContext(ctx,
		`SELECT id, name, price, created_at FROM items ORDER BY id`)
	if err != nil {
		return nil, fmt.Errorf("membaca daftar item: %w", err)
	}
	defer rows.Close()

	items := []Item{}
	for rows.Next() {
		var item Item
		if err := rows.Scan(&item.ID, &item.Name, &item.Price, &item.CreatedAt); err != nil {
			return nil, fmt.Errorf("memindai baris item: %w", err)
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterasi baris item: %w", err)
	}
	return items, nil
}

// Update mengganti name dan price item. Jumlah baris yang berubah
// diperiksa untuk membedakan "tidak ada baris" dari keberhasilan.
func (r *PostgresItemRepository) Update(ctx context.Context, item Item) (Item, error) {
	res, err := r.db.ExecContext(ctx,
		`UPDATE items SET name = $1, price = $2 WHERE id = $3`,
		item.Name, item.Price, item.ID,
	)
	if err != nil {
		return Item{}, fmt.Errorf("mengubah item %d: %w", item.ID, err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return Item{}, fmt.Errorf("menghitung baris item %d: %w", item.ID, err)
	}
	if n == 0 {
		return Item{}, fmt.Errorf("item %d: %w", item.ID, ErrNotFound)
	}
	return item, nil
}

// Delete menghapus satu item dan memetakan "tidak ada baris terhapus"
// menjadi ErrNotFound.
func (r *PostgresItemRepository) Delete(ctx context.Context, id int64) error {
	res, err := r.db.ExecContext(ctx, `DELETE FROM items WHERE id = $1`, id)
	if err != nil {
		return fmt.Errorf("menghapus item %d: %w", id, err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("menghitung baris item %d: %w", id, err)
	}
	if n == 0 {
		return fmt.Errorf("item %d: %w", id, ErrNotFound)
	}
	return nil
}

// MemoryItemRepository adalah penyimpanan palsu (fake) untuk unit test:
// map dilindungi mutex dan ID bertambah dari penghitung internal.
// Perilakunya meniru PostgresItemRepository, termasuk pemetaan
// ErrNotFound, sehingga service bisa diuji tanpa database.
type MemoryItemRepository struct {
	mu     sync.Mutex
	items  map[int64]Item
	nextID int64
}

// NewMemoryItemRepository membuat penyimpanan kosong dengan penghitung
// ID mulai dari 1, meniru sequence PostgreSQL.
func NewMemoryItemRepository() *MemoryItemRepository {
	return &MemoryItemRepository{items: make(map[int64]Item), nextID: 1}
}

func (r *MemoryItemRepository) Create(ctx context.Context, item Item) (Item, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	item.ID = r.nextID
	r.nextID++
	r.items[item.ID] = item
	return item, nil
}

func (r *MemoryItemRepository) GetByID(ctx context.Context, id int64) (Item, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	item, ada := r.items[id]
	if !ada {
		return Item{}, fmt.Errorf("item %d: %w", id, ErrNotFound)
	}
	return item, nil
}

// List mengembalikan salinan item terurut naik berdasarkan ID agar
// urutannya sama dengan ORDER BY id di PostgreSQL.
func (r *MemoryItemRepository) List(ctx context.Context) ([]Item, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	ids := make([]int64, 0, len(r.items))
	for id := range r.items {
		ids = append(ids, id)
	}
	slices.Sort(ids)
	items := make([]Item, 0, len(ids))
	for _, id := range ids {
		items = append(items, r.items[id])
	}
	return items, nil
}

func (r *MemoryItemRepository) Update(ctx context.Context, item Item) (Item, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ada := r.items[item.ID]; !ada {
		return Item{}, fmt.Errorf("item %d: %w", item.ID, ErrNotFound)
	}
	r.items[item.ID] = item
	return item, nil
}

func (r *MemoryItemRepository) Delete(ctx context.Context, id int64) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ada := r.items[id]; !ada {
		return fmt.Errorf("item %d: %w", id, ErrNotFound)
	}
	delete(r.items, id)
	return nil
}
