package migrate

import (
	"fmt"
	"time"

	"github.com/jinzhu/gorm"
)

// Migration represents a database schema migration.
type Migration struct {
	ID         string     `gorm:"size:16;primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
	Dialect    string     `gorm:"size:16;" json:"Dialect" yaml:"Dialect,omitempty"`
	Stage      string     `gorm:"size:16;" json:"Stage" yaml:"Stage,omitempty"`
	Error      string     `gorm:"size:255;" json:"Error" yaml:"Error,omitempty"`
	Source     string     `gorm:"size:16;" json:"Source" yaml:"Source,omitempty"`
	Statements []string   `gorm:"-" json:"Statements" yaml:"Statements,omitempty"`
	StartedAt  time.Time  `json:"StartedAt" yaml:"StartedAt,omitempty"`
	FinishedAt *time.Time `json:"FinishedAt" yaml:"FinishedAt,omitempty"`
}

// TableName returns the entity database table name.
func (Migration) TableName() string {
	return "migrations"
}

// StageName returns the stage name.
func (m *Migration) StageName() string {
	if m.Stage == "" {
		return StageMain
	} else {
		return m.Stage
	}
}

// RunStage checks if the run stage matches.
func (m *Migration) RunStage(name string) bool {
	return m.StageName() == name
}

// Skip checks if the migration should be skipped based on the options.
func (m *Migration) Skip(opt Options) bool {
	return !m.RunStage(opt.StageName())
}

// Finished tests if the migration has been finished yet.
func (m *Migration) Finished() bool {
	if m.FinishedAt == nil {
		return false
	}

	return !m.FinishedAt.IsZero()
}

// RunDuration returns the run duration of started migrations.
func (m *Migration) RunDuration() time.Duration {
	if m.Error != "" || m.StartedAt.IsZero() {
		return time.Duration(0)
	}

	if m.Finished() {
		return m.FinishedAt.UTC().Sub(m.StartedAt.UTC())
	}

	return time.Now().UTC().Sub(m.StartedAt.UTC())
}

// Repeat tests if the migration should be repeated.
func (m *Migration) Repeat(runFailed bool) bool {
	if runFailed && m.Error != "" {
		return true
	}

	// Don't repeat if finished.
	if m.Finished() {
		return false
	}

	// Repeat not started yet.
	if m.StartedAt.IsZero() {
		return true
	}

	// Repeat if "running" for more than 60 minutes.
	return m.RunDuration().Minutes() >= 60
}

// Fail marks the migration as failed by adding an error message and removing the FinishedAt timestamp.
func (m *Migration) Fail(err error, db *gorm.DB) {
	if err == nil || db == nil {
		return
	}

	m.FinishedAt = nil

	if err.Error() == "" {
		m.Error = "unknown error"
	} else {
		m.Error = err.Error()
	}

	db.Model(m).Updates(Values{"FinishedAt": m.FinishedAt, "Error": m.Error})
}

// Finish updates the FinishedAt timestamp and removes the error message when the migration was successful.
func (m *Migration) Finish(db *gorm.DB) error {
	if db == nil {
		return fmt.Errorf("db is nil")
	}

	finished := time.Now().UTC().Truncate(time.Second)
	m.FinishedAt = &finished
	m.Error = ""
	return db.Model(m).Updates(Values{"FinishedAt": m.FinishedAt, "Error": m.Error}).Error
}

// Execute runs the migration.
func (m *Migration) Execute(db *gorm.DB) error {
	if db == nil {
		return fmt.Errorf("db is nil")
	}

	for _, s := range m.Statements { //  ADD
		if err := db.Exec(s).Error; err != nil {
			// Log the errors triggered by ALTER and DROP statements
			// and otherwise ignore them, since some databases do not
			// support "IF EXISTS".
			if IgnoreErr.Matches(s, err.Error()) {
				log.Tracef("migrate: ignored %s", err)
			} else {
				return err
			}
		}
	}

	return nil
}
