// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee 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 Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package migration

import (
	"os"
	"sort"

	"code.vikunja.io/api/pkg/config"
	"code.vikunja.io/api/pkg/db"
	"code.vikunja.io/api/pkg/files"
	"code.vikunja.io/api/pkg/log"
	"code.vikunja.io/api/pkg/models"
	"code.vikunja.io/api/pkg/modules/migration"
	"code.vikunja.io/api/pkg/notifications"
	"code.vikunja.io/api/pkg/user"
	"github.com/olekukonko/tablewriter"
	"src.techknowlogick.com/xormigrate"
	"xorm.io/xorm"
)

// You can get the id string for new migrations by running `date +%Y%m%d%H%M%S` on a unix system.

var migrations []*xormigrate.Migration

// A helper function because we need a migration in various places which we can't really solve with an init() function.
func initMigration(x *xorm.Engine) *xormigrate.Xormigrate {
	// Get our own xorm engine if we don't have one
	if x == nil {
		var err error
		x, err = db.CreateDBEngine()
		if err != nil {
			log.Fatalf("Could not connect to db: %v", err.Error())
			return nil
		}
	}

	// Because init() does not guarantee the order in which these are added to the slice,
	// we need to sort them to ensure that they are in order
	sort.Slice(migrations, func(i, j int) bool {
		return migrations[i].ID < migrations[j].ID
	})

	m := xormigrate.New(x, migrations)
	logger := log.NewXormLogger("")
	m.SetLogger(logger)
	m.InitSchema(initSchema)
	return m
}

// Migrate runs all migrations
func Migrate(x *xorm.Engine) {
	m := initMigration(x)
	err := m.Migrate()
	if err != nil {
		log.Fatalf("Migration failed: %v", err)
	}
	log.Info("Ran all migrations successfully.")
}

// ListMigrations pretty-prints a list with all migrations.
func ListMigrations() {
	x, err := db.CreateDBEngine()
	if err != nil {
		log.Fatalf("Could not connect to db: %v", err.Error())
	}
	ms := []*xormigrate.Migration{}
	err = x.Find(&ms)
	if err != nil {
		log.Fatalf("Error getting migration table: %v", err.Error())
	}

	table := tablewriter.NewWriter(os.Stdout)
	table.SetHeader([]string{"ID", "Description"})
	table.SetAlignment(tablewriter.ALIGN_LEFT)
	table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor},
		tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor})

	for _, m := range ms {
		table.Append([]string{m.ID, m.Description})
	}
	table.Render()
}

// Rollback rolls back all migrations until a certain point.
func Rollback(migrationID string) {
	m := initMigration(nil)
	err := m.RollbackTo(migrationID)
	if err != nil {
		log.Fatalf("Could not rollback: %v", err)
	}
	log.Info("Rolled back successfully.")
}

// MigrateTo executes all migrations up to a certain point
func MigrateTo(migrationID string, x *xorm.Engine) error {
	m := initMigration(x)
	if err := m.MigrateTo(migrationID); err != nil {
		return err
	}
	return nil
}

// Deletes a column from a table. All arguments are strings, to let them be standalone and not depending on any struct.
func dropTableColum(x *xorm.Engine, tableName, col string) error {

	switch config.DatabaseType.GetString() {
	case "sqlite":
		log.Warning("Unable to drop columns in SQLite")
	case "mysql":
		_, err := x.Exec("ALTER TABLE " + tableName + " DROP COLUMN " + col)
		if err != nil {
			return err
		}
	case "postgres":
		_, err := x.Exec("ALTER TABLE " + tableName + " DROP COLUMN " + col)
		if err != nil {
			return err
		}
	default:
		log.Fatal("Unknown db.")
	}
	return nil
}

// Modifies a column definition
func modifyColumn(x *xorm.Engine, tableName, col, newDefinition string) error {
	switch config.DatabaseType.GetString() {
	case "sqlite":
		log.Warning("Unable to modify columns in SQLite")
	case "mysql":
		_, err := x.Exec("ALTER TABLE " + tableName + " MODIFY COLUMN " + col + " " + newDefinition)
		if err != nil {
			return err
		}
	case "postgres":
		_, err := x.Exec("ALTER TABLE " + tableName + " ALTER COLUMN " + col + " " + newDefinition)
		if err != nil {
			return err
		}
	default:
		log.Fatal("Unknown db.")
	}
	return nil
}

func renameTable(x *xorm.Engine, oldName, newName string) error {
	switch config.DatabaseType.GetString() {
	case "sqlite":
		_, err := x.Exec("ALTER TABLE `" + oldName + "` RENAME TO `" + newName + "`")
		if err != nil {
			return err
		}
	case "mysql":
		_, err := x.Exec("RENAME TABLE `" + oldName + "` TO `" + newName + "`")
		if err != nil {
			return err
		}
	case "postgres":
		_, err := x.Exec("ALTER TABLE `" + oldName + "` RENAME TO `" + newName + "`")
		if err != nil {
			return err
		}
	default:
		log.Fatal("Unknown db.")
	}
	return nil
}

func initSchema(tx *xorm.Engine) error {
	schemeBeans := []interface{}{}
	schemeBeans = append(schemeBeans, models.GetTables()...)
	schemeBeans = append(schemeBeans, files.GetTables()...)
	schemeBeans = append(schemeBeans, migration.GetTables()...)
	schemeBeans = append(schemeBeans, user.GetTables()...)
	schemeBeans = append(schemeBeans, notifications.GetTables()...)
	return tx.Sync2(schemeBeans...)
}