package migrate

import (
	"database/sql"
	"time"

	"github.com/dustin/go-humanize/english"
	"github.com/jinzhu/gorm"
)

// Migrations represents a sorted list of migrations.
type Migrations []Migration

// MigrationMap represents a map of migrations.
type MigrationMap map[string]Migration

// Existing finds and returns previously executed database schema migrations.
func Existing(db *gorm.DB) MigrationMap {
	result := make(MigrationMap)

	stmt := db.Model(Migration{})
	stmt = stmt.Select("id, dialect, error, source, started_at, finished_at")

	rows, err := stmt.Rows()

	if err != nil {
		log.Warnf("migrate: %s (find existing)", err)
		return result
	}

	defer func(rows *sql.Rows) {
		err = rows.Close()
	}(rows)

	for rows.Next() {
		m := Migration{}

		if err = rows.Scan(&m.ID, &m.Dialect, &m.Error, &m.Source, &m.StartedAt, &m.FinishedAt); err != nil {
			log.Warnf("migrate: %s (scan existing)", err)
			return result
		}

		result[m.ID] = m
	}

	return result
}

// Start runs all migrations that haven't been executed yet.
func (m *Migrations) Start(db *gorm.DB, runFailed bool) {
	// Find previously executed migrations.
	executed := Existing(db)

	if prev := len(executed); prev == 0 {
		log.Infof("migrate: found no previous migrations")
	} else {
		log.Debugf("migrate: found %s", english.Plural(len(executed), "previous migration", "previous migrations"))
	}

	for _, migration := range *m {
		start := time.Now()
		migration.StartedAt = start.UTC().Round(time.Second)

		// Already executed?
		if done, ok := executed[migration.ID]; ok {
			// Try to run failed migrations again?
			if !runFailed || done.Error == "" {
				log.Debugf("migrate: %s skipped", migration.ID)
				continue
			}
		} else if err := db.Create(migration).Error; err != nil {
			// Should not happen.
			log.Warnf("migrate: creating %s failed with %s [%s]", migration.ID, err, time.Since(start))
			continue
		}

		// Run migration.
		if err := migration.Execute(db); err != nil {
			migration.Fail(err, db)
			log.Errorf("migrate: executing %s failed with %s [%s]", migration.ID, err, time.Since(start))
		} else if err = migration.Finish(db); err != nil {
			log.Warnf("migrate: updating %s failed with %s [%s]", migration.ID, err, time.Since(start))
		} else {
			log.Infof("migrate: %s successful [%s]", migration.ID, time.Since(start))
		}
	}
}
