package migrations

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Result struct {
	Applied int
	Skipped int
}

type PlanEntry struct {
	Version           string `json:"version"`
	UpFile            string `json:"up_file"`
	DownFile          string `json:"down_file,omitempty"`
	Checksum          string `json:"checksum"`
	RollbackAvailable bool   `json:"rollback_available"`
}

func Run(ctx context.Context, db *pgxpool.Pool, dir string, log *slog.Logger) (Result, error) {
	dir = strings.TrimSpace(dir)
	if dir == "" {
		dir = "migrations"
	}

	files, err := migrationFiles(dir)
	if err != nil {
		return Result{}, err
	}
	if len(files) == 0 {
		return Result{}, fmt.Errorf("no .up.sql migrations found in %s", dir)
	}

	if _, err := db.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS schema_migrations (
			version text PRIMARY KEY,
			checksum text NOT NULL,
			applied_at timestamptz NOT NULL DEFAULT now()
		)
	`); err != nil {
		return Result{}, fmt.Errorf("ensure schema_migrations: %w", err)
	}

	var result Result
	for _, file := range files {
		version := strings.TrimSuffix(filepath.Base(file), ".up.sql")
		sql, err := os.ReadFile(file)
		if err != nil {
			return result, fmt.Errorf("read migration %s: %w", file, err)
		}

		checksum := sha256Hex(sql)
		var existingChecksum string
		err = db.QueryRow(ctx, `
			SELECT checksum
			FROM schema_migrations
			WHERE version = $1
		`, version).Scan(&existingChecksum)
		if err == nil {
			if existingChecksum != checksum {
				return result, fmt.Errorf("migration %s checksum mismatch", version)
			}
			result.Skipped++
			continue
		}
		if !errors.Is(err, pgx.ErrNoRows) {
			return result, fmt.Errorf("check migration %s: %w", version, err)
		}

		if err := apply(ctx, db, version, checksum, string(sql)); err != nil {
			return result, err
		}
		result.Applied++
		if log != nil {
			log.Info("database migration applied", "version", version)
		}
	}

	return result, nil
}

func Plan(dir string) ([]PlanEntry, error) {
	dir = strings.TrimSpace(dir)
	if dir == "" {
		dir = "migrations"
	}

	files, err := migrationFiles(dir)
	if err != nil {
		return nil, err
	}
	if len(files) == 0 {
		return nil, fmt.Errorf("no .up.sql migrations found in %s", dir)
	}

	entries := make([]PlanEntry, 0, len(files))
	for _, file := range files {
		sql, err := os.ReadFile(file)
		if err != nil {
			return nil, fmt.Errorf("read migration %s: %w", file, err)
		}
		version := strings.TrimSuffix(filepath.Base(file), ".up.sql")
		downFile := filepath.Join(dir, version+".down.sql")
		_, statErr := os.Stat(downFile)
		entry := PlanEntry{
			Version:           version,
			UpFile:            filepath.Clean(file),
			Checksum:          sha256Hex(sql),
			RollbackAvailable: statErr == nil,
		}
		if entry.RollbackAvailable {
			entry.DownFile = filepath.Clean(downFile)
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

func migrationFiles(dir string) ([]string, error) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, fmt.Errorf("read migrations directory %s: %w", dir, err)
	}

	files := []string{}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".up.sql") {
			continue
		}
		files = append(files, filepath.Join(dir, entry.Name()))
	}
	sort.Strings(files)
	return files, nil
}

func apply(ctx context.Context, db *pgxpool.Pool, version, checksum, sql string) error {
	tx, err := db.Begin(ctx)
	if err != nil {
		return fmt.Errorf("begin migration %s: %w", version, err)
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, sql); err != nil {
		return fmt.Errorf("apply migration %s: %w", version, err)
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO schema_migrations (version, checksum)
		VALUES ($1, $2)
	`, version, checksum); err != nil {
		return fmt.Errorf("record migration %s: %w", version, err)
	}
	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("commit migration %s: %w", version, err)
	}
	return nil
}

func sha256Hex(input []byte) string {
	sum := sha256.Sum256(input)
	return hex.EncodeToString(sum[:])
}
