// Copyright 2021 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package migration

import (
	"fmt"
	"reflect"

	"github.com/rs/zerolog/log"
	"xorm.io/xorm"

	"github.com/woodpecker-ci/woodpecker/server/model"
)

// APPEND NEW MIGRATIONS
// they are executed in order and if one fail woodpecker try to rollback and quit
var migrationTasks = []*task{
	&legacy2Xorm,
	&alterTableReposDropFallback,
	&alterTableReposDropAllowDeploysAllowTags,
	&fixPRSecretEventName,
	&alterTableReposDropCounter,
	&dropSenders,
	&alterTableLogUpdateColumnLogDataType,
	&alterTableSecretsAddUserCol,
	&lowercaseSecretNames,
	&renameBuildsToPipeline,
	&renameColumnsBuildsToPipeline,
	&renameTableProcsToSteps,
}

var allBeans = []interface{}{
	new(model.Agent),
	new(model.Pipeline),
	new(model.PipelineConfig),
	new(model.Config),
	new(model.File),
	new(model.Logs),
	new(model.Perm),
	new(model.Step),
	new(model.Registry),
	new(model.Repo),
	new(model.Secret),
	new(model.Task),
	new(model.User),
	new(model.ServerConfig),
	new(model.Cron),
	new(model.Redirection),
}

type migrations struct {
	Name string `xorm:"UNIQUE"`
}

type task struct {
	name     string
	required bool
	fn       func(sess *xorm.Session) error
}

// initNew create tables for new instance
func initNew(sess *xorm.Session) error {
	if err := syncAll(sess); err != nil {
		return err
	}

	// dummy run migrations
	for _, task := range migrationTasks {
		if _, err := sess.Insert(&migrations{task.name}); err != nil {
			return err
		}
	}

	return nil
}

func Migrate(e *xorm.Engine) error {
	if err := e.Sync2(new(migrations)); err != nil {
		return err
	}

	sess := e.NewSession()
	defer sess.Close()
	if err := sess.Begin(); err != nil {
		return err
	}

	// check if we have a fresh installation or need to check for migrations
	c, err := sess.Count(new(migrations))
	if err != nil {
		return err
	}

	if c == 0 {
		if err := initNew(sess); err != nil {
			return err
		}
		return sess.Commit()
	}

	if err := runTasks(sess, migrationTasks); err != nil {
		return err
	}

	if err := sess.Commit(); err != nil {
		return err
	}

	if err := e.ClearCache(allBeans...); err != nil {
		return err
	}

	return syncAll(e)
}

func runTasks(sess *xorm.Session, tasks []*task) error {
	// cache migrations in db
	migCache := make(map[string]bool)
	var migList []*migrations
	if err := sess.Find(&migList); err != nil {
		return err
	}
	for i := range migList {
		migCache[migList[i].Name] = true
	}

	for _, task := range tasks {
		if migCache[task.name] {
			log.Trace().Msgf("migration task '%s' already applied", task.name)
			continue
		}

		log.Trace().Msgf("start migration task '%s'", task.name)

		if task.fn != nil {
			if err := task.fn(sess); err != nil {
				if task.required {
					return err
				}
				log.Error().Err(err).Msgf("migration task '%s' failed but is not required", task.name)
				continue
			}
			log.Debug().Msgf("migration task '%s' done", task.name)
		} else {
			log.Trace().Msgf("skip migration task '%s'", task.name)
		}

		if _, err := sess.Insert(&migrations{task.name}); err != nil {
			return err
		}
		migCache[task.name] = true
	}
	return nil
}

type syncEngine interface {
	Sync2(beans ...interface{}) error
}

func syncAll(sess syncEngine) error {
	for _, bean := range allBeans {
		if err := sess.Sync2(bean); err != nil {
			return fmt.Errorf("sync2 error '%s': %v", reflect.TypeOf(bean), err)
		}
	}
	return nil
}
