package main

import (
	"errors"
	"fmt"
	"io/fs"
	"slices"
	"strconv"
	"strings"
	"testing/fstest"
)

// Runner adalah migration runner sederhana yang bekerja atas fs.FS.
// Field applied meniru tabel schema_migrations di database nyata.
type Runner struct {
	applied map[int]bool
}

func NewRunner() *Runner {
	return &Runner{applied: map[int]bool{}}
}

// pisahNama memecah "002_add_harga.up.sql" menjadi versi 2 dan label "add_harga".
func pisahNama(nama string) (int, string, error) {
	nama = strings.TrimSuffix(nama, ".sql")
	nama = strings.TrimSuffix(nama, ".up")
	nama = strings.TrimSuffix(nama, ".down")
	angka, label, ada := strings.Cut(nama, "_")
	if !ada {
		return 0, "", fmt.Errorf("nama migration tidak valid: %s", nama)
	}
	v, err := strconv.Atoi(angka)
	if err != nil {
		return 0, "", fmt.Errorf("nomor versi tidak valid: %s", nama)
	}
	return v, label, nil
}

// Up menerapkan semua migration .up.sql yang belum diterapkan, dari versi terkecil.
// "Eksekusi" SQL di sini hanya dicetak karena tidak ada database aktif.
func (r *Runner) Up(fsys fs.FS) error {
	names, err := daftarBerkas(fsys)
	if err != nil {
		return err
	}
	for _, name := range names {
		if !strings.HasSuffix(name, ".up.sql") {
			continue
		}
		v, label, err := pisahNama(name)
		if err != nil {
			return err
		}
		if r.applied[v] {
			fmt.Printf("lewati     %03d_%s (sudah diterapkan)\n", v, label)
			continue
		}
		sql, err := fs.ReadFile(fsys, name)
		if err != nil {
			return err
		}
		fmt.Printf("terapkan   %03d_%s: %s\n", v, label, sql)
		r.applied[v] = true
	}
	return nil
}

// Down membatalkan migration dengan versi terbesar, satu langkah saja.
func (r *Runner) Down(fsys fs.FS) error {
	terapan := r.terapannya()
	if len(terapan) == 0 {
		return errors.New("tidak ada migration untuk dibatalkan")
	}
	versi := terapan[len(terapan)-1]
	names, err := daftarBerkas(fsys)
	if err != nil {
		return err
	}
	for _, name := range names {
		if !strings.HasSuffix(name, ".down.sql") {
			continue
		}
		v, label, errN := pisahNama(name)
		if errN != nil {
			return errN
		}
		if v != versi {
			continue
		}
		sql, errR := fs.ReadFile(fsys, name)
		if errR != nil {
			return errR
		}
		fmt.Printf("batalkan   %03d_%s: %s\n", v, label, sql)
		delete(r.applied, v)
		return nil
	}
	return fmt.Errorf("berkas down untuk versi %d tidak ditemukan", versi)
}

// terapannya mengembalikan daftar versi yang sudah diterapkan, terurut naik.
func (r *Runner) terapannya() []int {
	hasil := make([]int, 0, len(r.applied))
	for v := range r.applied {
		hasil = append(hasil, v)
	}
	slices.Sort(hasil)
	return hasil
}

func daftarBerkas(fsys fs.FS) ([]string, error) {
	entries, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return nil, err
	}
	names := make([]string, 0, len(entries))
	for _, e := range entries {
		names = append(names, e.Name())
	}
	slices.Sort(names)
	return names, nil
}

func main() {
	// MapFS menggantikan folder migrations/ di proyek nyata.
	// Di aplikasi sungguhan, gunakan embed.FS atau folder di disk.
	migrationsFS := fstest.MapFS{
		"001_create_items.up.sql":   &fstest.MapFile{Data: []byte("CREATE TABLE items (id SERIAL PRIMARY KEY, nama TEXT)")},
		"001_create_items.down.sql": &fstest.MapFile{Data: []byte("DROP TABLE items")},
		"002_add_harga.up.sql":      &fstest.MapFile{Data: []byte("ALTER TABLE items ADD COLUMN harga INTEGER")},
		"002_add_harga.down.sql":    &fstest.MapFile{Data: []byte("ALTER TABLE items DROP COLUMN harga")},
		"003_add_stok.up.sql":       &fstest.MapFile{Data: []byte("ALTER TABLE items ADD COLUMN stok INTEGER NOT NULL DEFAULT 0")},
		"003_add_stok.down.sql":     &fstest.MapFile{Data: []byte("ALTER TABLE items DROP COLUMN stok")},
	}

	runner := NewRunner()

	fmt.Println("== up pertama: semua migration baru diterapkan ==")
	if err := runner.Up(migrationsFS); err != nil {
		fmt.Println("galat:", err)
		return
	}
	fmt.Println("versi terapannya:", runner.terapannya())

	fmt.Println("== up kedua: migration lama dilewati ==")
	if err := runner.Up(migrationsFS); err != nil {
		fmt.Println("galat:", err)
		return
	}

	fmt.Println("== down satu langkah: versi terbesar dibatalkan ==")
	if err := runner.Down(migrationsFS); err != nil {
		fmt.Println("galat:", err)
		return
	}
	fmt.Println("versi terapannya:", runner.terapannya())
}
