package migrate

import (
	"fmt"
	"sync"
	"time"

	"github.com/jinzhu/gorm"

	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/txt"
)

var versionOnce = sync.Once{}
var versionMutex = sync.Mutex{}

// Version represents the application version.
type Version struct {
	ID         uint       `gorm:"primary_key" yaml:"-"`
	Version    string     `gorm:"size:255;unique_index:idx_version_edition;" json:"Version" yaml:"Version,omitempty"`
	Edition    string     `gorm:"size:255;unique_index:idx_version_edition;" json:"Edition" yaml:"Edition,omitempty"`
	Error      string     `gorm:"size:255;" json:"Error" yaml:"Error,omitempty"`
	CreatedAt  time.Time  `yaml:"CreatedAt,omitempty"`
	UpdatedAt  time.Time  `yaml:"UpdatedAt,omitempty"`
	MigratedAt *time.Time `json:"MigratedAt" yaml:"MigratedAt,omitempty"`
}

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

var UnknownVersion = Version{
	Version: "0.0.0",
	Edition: "dev",
}

// NeedsMigration tests if the Version is not yet installed.
func (m *Version) NeedsMigration() bool {
	if m == nil {
		return true
	} else if m.MigratedAt == nil || m.CreatedAt.IsZero() {
		return true
	} else if m.Unknown() {
		return true
	}

	return m.MigratedAt.IsZero()
}

// Migrated flags the version as installed and migrated.
func (m *Version) Migrated(db *gorm.DB) error {
	if err := m.CreateTable(db); err != nil {
		return err
	} else if m.Unknown() {
		return nil
	}

	timeStamp := time.Now().UTC().Truncate(time.Second)
	m.MigratedAt = &timeStamp
	m.Error = ""

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

// NewVersion creates a Version entity from a model name and a make name.
func NewVersion(version, edition string) *Version {
	result := &Version{
		Version: txt.Clip(version, txt.ClipLongName),
		Edition: txt.Clip(edition, txt.ClipLongName),
	}

	return result
}

// Create inserts a new row to the database.
func (m *Version) Create(db *gorm.DB) error {
	if err := m.CreateTable(db); err != nil {
		return err
	}

	versionMutex.Lock()
	defer versionMutex.Unlock()

	return db.Create(m).Error
}

// Save saved the record in the database.
func (m *Version) Save(db *gorm.DB) error {
	if err := m.CreateTable(db); err != nil {
		return err
	}

	versionMutex.Lock()
	defer versionMutex.Unlock()

	return db.Save(m).Error
}

// Find fetches an existing record from the database.
func (m *Version) Find(db *gorm.DB) *Version {
	if err := m.CreateTable(db); err != nil {
		return nil
	} else if m.Version == "" {
		return nil
	}

	result := Version{}

	if err := db.Where("version = ? AND edition = ?", m.Version, m.Edition).First(&result).Error; err != nil {
		return nil
	}

	return &result
}

// FirstOrCreateVersion returns the existing row, inserts a new row or nil in case of errors.
func FirstOrCreateVersion(db *gorm.DB, m *Version) *Version {
	if m == nil {
		return &UnknownVersion
	} else if err := m.CreateTable(db); err != nil {
		return &UnknownVersion
	} else if m.Version == "" {
		return &UnknownVersion
	}

	// Find existing version or create new record.
	if found := m.Find(db); found != nil {
		return found
	} else if err := m.Create(db); err != nil {
		log.Errorf("version: %s (create)", err)
	} else {
		return m
	}

	return &UnknownVersion
}

// String returns an identifier that can be used in logs.
func (m *Version) String() string {
	return clean.Log(m.Version + "-" + m.Edition)
}

// Unknown checks if the version is unknown.
func (m *Version) Unknown() bool {
	if m == nil {
		return true
	} else if m.Version == "" {
		return true
	}

	return m.Version == UnknownVersion.Version
}

// CreateTable creates the versions database table if needed.
func (m *Version) CreateTable(db *gorm.DB) (err error) {
	if db == nil {
		return fmt.Errorf("db is nil")
	} else if m == nil {
		return fmt.Errorf("version is nil")
	}

	versionOnce.Do(func() {
		err = db.AutoMigrate(&Version{}).Error
	})

	return err
}
