package migrations

import (
	"context"
	"database/sql"
	"embed"
	"fmt"
	"log/slog"
	"os"

	_ "github.com/jackc/pgx/v5/stdlib"
	"github.com/pressly/goose/v3"
)

//go:embed sql/*.sql
var FS embed.FS

const DefaultURL = "postgres:///lookbook?sslmode=disable"

// Up applies all available migrations using the provided database URL.
func Up(ctx context.Context, dbURL string, logger *slog.Logger) error {
	url := dbURL
	if url == "" {
		url = DefaultURL
	}

	db, err := openDB(url, logger)
	if err != nil {
		return err
	}
	defer db.Close()

	if err := goose.UpContext(ctx, db, "sql"); err != nil {
		return fmt.Errorf("apply migrations: %w", err)
	}

	logger.Info("database migrated")
	return nil
}

// Down rolls back migrations. If targetVersion < 0, it steps back one migration; otherwise it migrates down to the target version.
func Down(ctx context.Context, dbURL string, targetVersion int64, logger *slog.Logger) error {
	url := dbURL
	if url == "" {
		url = DefaultURL
	}

	db, err := openDB(url, logger)
	if err != nil {
		return err
	}
	defer db.Close()

	if targetVersion < 0 {
		if err := goose.DownContext(ctx, db, "sql"); err != nil {
			return fmt.Errorf("rollback one: %w", err)
		}
		logger.Info("rolled back one migration")
		return nil
	}

	if err := goose.DownToContext(ctx, db, "sql", targetVersion); err != nil {
		return fmt.Errorf("rollback to version %d: %w", targetVersion, err)
	}
	logger.Info("rolled back to version", slog.Int64("version", targetVersion))
	return nil
}

// CheckPending returns the number of pending migrations without applying them.
func CheckPending(ctx context.Context, dbURL string, logger *slog.Logger) (int, error) {
	url := dbURL
	if url == "" {
		url = DefaultURL
	}

	db, err := openDB(url, logger)
	if err != nil {
		return 0, err
	}
	defer db.Close()

	current, err := goose.GetDBVersionContext(ctx, db)
	if err != nil {
		return 0, fmt.Errorf("get db version: %w", err)
	}

	migrations, err := goose.CollectMigrations("sql", 0, goose.MaxVersion)
	if err != nil {
		return 0, fmt.Errorf("collect migrations: %w", err)
	}

	pending := 0
	for _, m := range migrations {
		if m.Version > current {
			pending++
		}
	}

	return pending, nil
}

// slogLogger adapts slog to goose's minimal logging interface.
type slogLogger struct {
	logger *slog.Logger
}

func (l slogLogger) Printf(format string, v ...any) {
	l.logger.Info(fmt.Sprintf(format, v...))
}

func (l slogLogger) Fatalf(format string, v ...any) {
	l.logger.Error(fmt.Sprintf(format, v...))
	os.Exit(1)
}

func openDB(url string, logger *slog.Logger) (*sql.DB, error) {
	goose.SetBaseFS(FS)
	goose.SetLogger(slogLogger{logger: logger})

	db, err := sql.Open("pgx", url)
	if err != nil {
		return nil, fmt.Errorf("open db: %w", err)
	}

	if err := db.Ping(); err != nil {
		db.Close()
		return nil, fmt.Errorf("ping db: %w", err)
	}

	return db, nil
}
