//
//  tank
//
//  Import and Playlist Daemon for autoradio project
//
//
//  Copyright (C) 2017-2019 Christian Pointner <equinox@helsinki.at>
//
//  This file is part of tank.
//
//  tank is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  any later version.
//
//  tank is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with tank. If not, see <http://www.gnu.org/licenses/>.
//

package store

import (
	"errors"
	"time"

	"github.com/jinzhu/gorm"
	"gopkg.in/gormigrate.v1"
)

var (
	dbMigrations = []*gormigrate.Migration{
		{
			ID:       "201903131716",
			Migrate:  func(tx *gorm.DB) error { return nil },
			Rollback: func(tx *gorm.DB) error { return nil },
		},
		{
			ID: "201905160033",
			Migrate: func(tx *gorm.DB) error {
				type PlaylistEntry struct {
					ID         uint64  `json:"-" gorm:"primary_key"`
					PlaylistID uint64  `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint    `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string  `json:"uri" gorm:"size:1024"`
					File       *File   `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64 `json:"-" gorm:"index"`
				}

				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primary_key"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					ShowName    string          `json:"show" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"association_foreignkey:Name"`
					Entries     []PlaylistEntry `json:"entries,omitempty"`
				}
				return tx.AutoMigrate(&Playlist{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("playlists").DropColumn("description").Error
			},
		},
		{
			ID: "201905291602",
			Migrate: func(tx *gorm.DB) error {
				type Import struct {
					State ImportState `json:"state"`
					Error string      `json:"error,omitempty"`
				}

				type FileSource struct {
					URI    string `json:"uri" gorm:"size:1024"`
					Hash   string `json:"hash"`
					Import Import `json:"import" gorm:"embedded;embedded_prefix:import__"`
				}

				type FileMetadata struct {
					Artist string `json:"artist,omitempty" gorm:"index"`
					Title  string `json:"title,omitempty" gorm:"index"`
					Album  string `json:"album,omitempty" gorm:"index"`
				}

				type File struct {
					ID        uint64        `json:"id" gorm:"primary_key"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"association_foreignkey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				return tx.AutoMigrate(&File{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("files").DropColumn("source__import__error").Error
			},
		},
		{
			ID: "201906010144",
			Migrate: func(tx *gorm.DB) error {
				type ImportLog struct {
					ID         uint64 `gorm:"primary_key"`
					File       File   `gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     uint64 `gorm:"not null;index;unique_index:unique_import_log_step"`
					ImportStep string `gorm:"not null;index;unique_index:unique_import_log_step"`
					Encoded    []byte
				}

				if err := tx.AutoMigrate(&ImportLog{}).Error; err != nil {
					return err
				}
				if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil {
					return err
				}
				return nil
			},
			Rollback: func(tx *gorm.DB) error {
				if err := tx.Model(ImportLog{}).RemoveForeignKey("file_id", "files (id)").Error; err != nil {
					return err
				}
				return tx.DropTable("import_logs").Error
			},
		},
		{
			ID: "201906051520",
			Migrate: func(tx *gorm.DB) error {
				if tx.Dialect().GetName() == "mysql" {
					tx.Model(ImportLog{}).ModifyColumn("encoded", "longblob")
				}
				return nil
			},
			Rollback: func(tx *gorm.DB) error {
				// to be exact we would need to chang `encoded` back to varbinary(255) for mysql
				// however this will open a box of worms....
				return nil
			},
		},
		{
			ID: "201908110945",
			Migrate: func(tx *gorm.DB) error {

				type PlaylistEntry struct {
					ID         uint64  `json:"-" gorm:"primary_key"`
					PlaylistID uint64  `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint    `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string  `json:"uri" gorm:"size:1024"`
					File       *File   `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64 `json:"-" gorm:"index"`
				}

				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primary_key"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					PlayoutMode string          `json:"playout-mode"`
					ShowName    string          `json:"show" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"association_foreignkey:Name"`
					Entries     []PlaylistEntry `json:"entries,omitempty"`
				}

				return tx.AutoMigrate(&Playlist{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("playlists").DropColumn("playout_mode").Error
			},
		},
	}
)

func initialMigration(tx *gorm.DB) (err error) {
	err = tx.AutoMigrate(
		&Show{},
		&File{},
		&ImportLog{},
		&Playlist{},
		&PlaylistEntry{},
	).Error

	if err != nil {
		return
	}

	// TODO: sadly this does not work on sqlite because constraints can only be added with create table...
	//       unfortunately gorm does not create foreign key contstraints in AutoMigrate(), see: https://github.com/jinzhu/gorm/issues/450
	if err := tx.Model(File{}).AddForeignKey("show_name", "shows (name)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(Playlist{}).AddForeignKey("show_name", "shows (name)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(PlaylistEntry{}).AddForeignKey("playlist_id", "playlists (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(PlaylistEntry{}).AddForeignKey("file_id", "files (id)", "RESTRICT", "CASCADE").Error; err != nil {
		return err
	}

	return nil
}

func (st *Store) initDBModel(cfg DBConfig) (err error) {
	opts := gormigrate.DefaultOptions
	opts.TableName = migrationsTn
	opts.IDColumnSize = 64
	opts.UseTransaction = true
	if cfg.Type == "mysql" {
		// mySQl does not support DDL commands inside transactions
		opts.UseTransaction = false
	}

	m := gormigrate.New(st.db, opts, dbMigrations)
	m.InitSchema(initialMigration)
	if err = m.Migrate(); err != nil {
		return errors.New("running database migrations failed: " + err.Error())
	}

	if err = st.db.Table(migrationsTn).Select("id").Not("id = ?", "SCHEMA_INIT").Order("id DESC").Limit(1).Row().Scan(&st.revision); err != nil {
		return errors.New("fetching current database revision failed: " + err.Error())
	}
	return
}