package infra

import (
	"database/sql"
	"fmt"
	"io/fs"
	"sort"
	"strings"
)

// ApplyMigrations reads SQL files matching glob from fsys, sorts them
// lexicographically (NNN_name.sql order), and executes every statement
// found in each file against conn.
//
// If glob is empty, it defaults to "migrations/*.sql".
//
// Each statement is executed individually. Errors that look like
// idempotent SQLite errors (duplicate column, already exists) are
// silently ignored so that migrations can be replayed safely against
// a database that was partially migrated.
//
// NOTE: the internal statement parser splits on ";" at the end of a
// trimmed line. This is intentionally simple and will break if SQL
// strings contain a literal semicolon at end-of-line. Avoid using
// such patterns in migration files.
func ApplyMigrations(conn *sql.DB, fsys fs.FS, glob string) error {
	if glob == "" {
		glob = "migrations/*.sql"
	}
	files, err := fs.Glob(fsys, glob)
	if err != nil {
		return err
	}
	sort.Strings(files)
	for _, f := range files {
		b, err := fs.ReadFile(fsys, f)
		if err != nil {
			return err
		}
		for _, stmt := range splitSQLStatements(string(b)) {
			s := strings.TrimSpace(stmt)
			if s == "" {
				continue
			}
			if _, err := conn.Exec(s); err != nil {
				if isIdempotentMigrationError(err) {
					continue
				}
				return fmt.Errorf("%s: %w", f, err)
			}
		}
	}
	return nil
}

// splitSQLStatements splits a SQL script into individual statements.
// Lines starting with "--" (comments) and blank lines are skipped.
// A statement ends when a trimmed line ends with ";".
func splitSQLStatements(s string) []string {
	out := []string{}
	cur := strings.Builder{}
	for _, line := range strings.Split(s, "\n") {
		trim := strings.TrimSpace(line)
		if strings.HasPrefix(trim, "--") || trim == "" {
			continue
		}
		cur.WriteString(line)
		cur.WriteString("\n")
		if strings.HasSuffix(trim, ";") {
			out = append(out, cur.String())
			cur.Reset()
		}
	}
	if cur.Len() > 0 {
		out = append(out, cur.String())
	}
	return out
}

// isIdempotentMigrationError returns true for SQLite errors that arise
// from re-applying a migration that was already applied (duplicate
// column, table/index already exists).
func isIdempotentMigrationError(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "duplicate column") ||
		strings.Contains(msg, "already exists")
}

