//
//  tank, Import and Playlist Daemon for Aura project
//  Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at>
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
//
//  This program 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 Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

package store

import (
	"errors"
	"time"

	"github.com/go-gormigrate/gormigrate/v2"
	"gorm.io/gorm"
)

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:"primaryKey"`
					PlaylistID uint64  `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"`
					LineNum    uint    `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"`
					URI        string  `json:"uri" gorm:"size:1024"`
					File       *File   `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"`
					FileID     *uint64 `json:"-" gorm:"index"`
				}

				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primaryKey"`
					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:"associationForeignKey:Name"`
					Entries     []PlaylistEntry `json:"entries,omitempty"`
				}
				return tx.AutoMigrate(&Playlist{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropColumn(&Playlist{}, "description")
			},
		},
		{
			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;embeddedPrefix: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:"primaryKey"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"associationForeignKey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embeddedPrefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				return tx.AutoMigrate(&File{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropColumn(&File{}, "source__import__error")
			},
		},
		{
			ID: "201906010144",
			Migrate: func(tx *gorm.DB) error {
				type ImportLog struct {
					ID         uint64 `gorm:"primaryKey"`
					File       File   `gorm:"associationAutoUpdate:false;associationAutoCreate:false"`
					FileID     uint64 `gorm:"not null;index;uniqueIndex:unique_import_log_step"`
					ImportStep string `gorm:"not null;index;uniqueIndex:unique_import_log_step"`
					Encoded    []byte
				}

				if err := tx.AutoMigrate(&ImportLog{}); err != nil {
					return err
				}

				if err := tx.Migrator().CreateConstraint(&ImportLog{}, "file_id"); err != nil {
					return err
				}

				return nil
			},
			Rollback: func(tx *gorm.DB) error {
				if err := tx.Migrator().DropConstraint(&ImportLog{}, "file_id"); err != nil {
					return err
				}
				return nil
			},
		},
		{
			ID: "201908110945",
			Migrate: func(tx *gorm.DB) error {

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

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

				return tx.AutoMigrate(&Playlist{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropColumn(&Playlist{}, "playout_mode")
			},
		},
		{
			ID: "201908150104",
			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;embeddedPrefix: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"`
					Organization string `json:"organization,omitempty" gorm:"index"`
					ISRC         string `json:"isrc,omitempty" gorm:"index"`
				}

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

				return tx.AutoMigrate(&File{})
			},
			Rollback: func(tx *gorm.DB) error {
				if err := tx.Migrator().DropColumn(&File{}, "metadata__organization"); err != nil {
					return err
				}
				return tx.Migrator().DropColumn(&File{}, "metadata__isrc")
			},
		},
		{
			ID: "202006130203",
			Migrate: func(tx *gorm.DB) error {

				type PlaylistEntry struct {
					ID         uint64         `json:"-" gorm:"primaryKey"`
					PlaylistID uint64         `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"`
					LineNum    uint           `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"`
					URI        string         `json:"uri" gorm:"size:1024"`
					Duration   *time.Duration `json:"duration,omitempty"`
					File       *File          `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"`
					FileID     *uint64        `json:"-" gorm:"index"`
				}

				// actually all playlists would need to be verified if they still fit the new constraint
				// that only allows a single non-file entry with duration == NULL per playlist.
				// However, we are still pre-first-release and all migrations will likely be squashed before that
				// release anyway...
				return tx.AutoMigrate(&PlaylistEntry{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropColumn(&PlaylistEntry{}, "duration")
			},
		},
		{
			ID: "202309141500",
			Migrate: func(tx *gorm.DB) error {
				type File struct {
					ID        uint64       `json:"id" gorm:"primaryKey"`
					CreatedAt time.Time    `json:"created"`
					UpdatedAt time.Time    `json:"updated"`
					ShowName  string       `json:"show" gorm:"not null;index"`
					Show      Show         `json:"-" gorm:"associationForeignKey:Name"`
					Source    FileSource   `json:"source" gorm:"embedded;embeddedPrefix:source__"`
					Metadata  FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"`
					Size      uint64       `json:"size"`
					Duration  float64      `json:"duration"`
				}

				if err := tx.Migrator().AlterColumn(&File{}, "duration"); err != nil {
					return err
				}

				type PlaylistEntry struct {
					ID         uint64   `json:"-" gorm:"primaryKey"`
					PlaylistID uint64   `json:"-" gorm:"not null;index;uniqueIndex:unique_playlist_line_numbers"`
					LineNum    uint     `json:"-" gorm:"not null;uniqueIndex:unique_playlist_line_numbers"`
					URI        string   `json:"uri" gorm:"size:1024"`
					Duration   *float64 `json:"duration,omitempty"`
					File       *File    `json:"file,omitempty" gorm:"associationAutoUpdate:false;associationAutoCreate:false"`
					FileID     *uint64  `json:"-" gorm:"index"`
				}

				return tx.Migrator().AlterColumn(&PlaylistEntry{}, "duration")
			},
			Rollback: func(tx *gorm.DB) error {
				type File struct {
					ID        uint64        `json:"id" gorm:"primaryKey"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"associationForeignKey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embeddedPrefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				if err := tx.Migrator().AlterColumn(&File{}, "duration"); err != nil {
					return err
				}

				return tx.Migrator().AlterColumn(&Playlist{}, "duration")
			},
		},
		{
			ID: "202312011500",
			Migrate: func(tx *gorm.DB) error {
				type Show struct {
					ID        uint64    `json:"id" gorm:"primaryKey"`
					CreatedAt time.Time `json:"created"`
					UpdatedAt time.Time `json:"updated"`
				}
				type File struct {
					ID        uint64       `json:"id" gorm:"primaryKey"`
					CreatedAt time.Time    `json:"created"`
					UpdatedAt time.Time    `json:"updated"`
					ShowID    uint64       `json:"showId" gorm:"not null;index"`
					Show      Show         `json:"-" gorm:"associationForeignKey:ID"`
					Source    FileSource   `json:"source" gorm:"embedded;embeddedPrefix:source__"`
					Metadata  FileMetadata `json:"metadata" gorm:"embedded;embeddedPrefix:metadata__"`
					Size      uint64       `json:"size"`
					Duration  float64      `json:"duration"`
				}
				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primaryKey"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					PlayoutMode string          `json:"playoutMode" gorm:"not null;default:'linear'"`
					ShowID      uint64          `json:"showId" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"associationForeignKey:ID"`
					Entries     []PlaylistEntry `json:"entries"`
				}

				if err := tx.Migrator().DropConstraint(&File{}, "show_name"); err != nil {
					return err
				}
				if err := tx.Migrator().DropConstraint(&Playlist{}, "show_name"); err != nil {
					return err
				}
				return tx.AutoMigrate(&Show{}, &File{}, &Playlist{})
			},

			Rollback: func(tx *gorm.DB) error {
				if err := tx.Migrator().DropColumn(&Show{}, "id"); err != nil {
					return err
				}
				if err := tx.Migrator().DropColumn(&File{}, "show_id"); err != nil {
					return err
				}
				return tx.Migrator().DropColumn(&Playlist{}, "show_id")
			},
		},
	}
)

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

	if err != nil {
		return
	}

	return nil
}

func (st *Store) initDBModel(cfg DBConfig) (err error) {
	opts := gormigrate.DefaultOptions
	opts.TableName = migrationsTn
	opts.IDColumnSize = 64
	opts.UseTransaction = true

	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
}