// Copyright 2019 Yunion
//
// 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 db

import (
	"fmt"
	"os"
	"reflect"
	"strings"

	"yunion.io/x/log"
	"yunion.io/x/pkg/utils"
	"yunion.io/x/sqlchemy"

	"yunion.io/x/onecloud/pkg/appsrv"
	common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
)

var globalTables map[string]IModelManager

func GlobalModelManagerTables() map[string]IModelManager {
	return globalTables
}

func RegisterModelManager(modelMan IModelManager) {
	RegisterModelManagerWithKeyword(modelMan, "")
}

func RegisterModelManagerWithKeyword(modelMan IModelManager, keyword string) {
	if globalTables == nil {
		globalTables = make(map[string]IModelManager)
	}
	if len(keyword) == 0 {
		keyword = modelMan.Keyword()
	}
	mustCheckModelManager(modelMan)
	if _, ok := globalTables[keyword]; ok {
		log.Fatalf("keyword %s exists in globalTables!", keyword)
	}
	globalTables[keyword] = modelMan
}

func mustCheckModelManager(modelMan IModelManager) {
	allowedTags := map[string][]string{
		"create": {
			"required",
			"optional",
			"domain",
			"domain_required",
			"domain_optional",
			"admin_required",
			"admin_optional",
		},
		"search": {"user", "domain", "admin"},
		"get":    {"user", "domain", "admin"},
		"list":   {"user", "domain", "admin"},
		"update": {"user", "domain", "admin"},
	}
	for _, col := range modelMan.TableSpec().Columns() {
		tags := col.Tags()
		for tagName, allowedValues := range allowedTags {
			v, ok := tags[tagName]
			if !ok {
				continue
			}
			if len(v) > 0 && !utils.IsInStringArray(v, allowedValues) {
				msg := fmt.Sprintf("model manager %s: column %s has invalid tag %s:\"%s\", expecting %v",
					modelMan.KeywordPlural(), col.Name(), tagName, v, allowedValues)
				panic(msg)
			}
		}
	}

	if false {
		requiredManagerFuncNames := []string{
			"ListItemFilter",
			"OrderByExtraFields",
			"FetchCustomizeColumns",
		}
		for _, name := range requiredManagerFuncNames {
			manV := reflect.ValueOf(modelMan)
			methV := manV.MethodByName(name)
			if !methV.IsValid() {
				msg := fmt.Sprintf("model manager %T: has no valid %s, likely caused by ambiguity",
					modelMan, name)
				panic(msg)
			}
		}
	}
}

func tableSpecId(tableSpec ITableSpec) string {
	keys := []string{
		string(tableSpec.GetDBName()),
		tableSpec.Name(),
	}
	for _, c := range tableSpec.Columns() {
		keys = append(keys, c.Name())
	}
	return strings.Join(keys, "-")
}

func CheckSync(autoSync bool, enableChecksumTables bool, skipInitChecksum bool) bool {
	log.Infof("Start check database schema: autoSync(%v), enableChecksumTables(%v), skipInitChecksum(%v)", autoSync, enableChecksumTables, skipInitChecksum)
	inSync := true

	var err error
	foreignProcessedTbl := make(map[string]bool)
	for modelName, modelMan := range globalTables {
		tableSpec := modelMan.TableSpec()
		tableKey := tableSpecId(tableSpec)
		if _, ok := foreignProcessedTbl[tableKey]; ok {
			continue
		}
		foreignProcessedTbl[tableKey] = true
		dropFKSqls := tableSpec.DropForeignKeySQL()
		if len(dropFKSqls) > 0 {
			log.Infof("model %s drop foreign key constraints!!!", modelName)
			if autoSync {
				if ts, ok := tableSpec.(*sTableSpec); ok {
					err = commitSqlDiffWithName(dropFKSqls, ts.GetDBName())
				} else {
					err = commitSqlDIffs(dropFKSqls)
				}
				if err != nil {
					log.Errorf("commit sql error %s", err)
					return false
				}
			} else {
				for _, sql := range dropFKSqls {
					log.Infof("%s;", sql)
				}
				inSync = false
			}
		}
	}

	processedTbl := make(map[string]bool)
	for modelName, modelMan := range globalTables {
		tableSpec := modelMan.TableSpec()
		tableKey := tableSpecId(tableSpec)
		if _, ok := processedTbl[tableKey]; ok {
			log.Warningf("table %s has been synced!", tableKey)
			continue
		}
		processedTbl[tableKey] = true
		sqls := tableSpec.SyncSQL()
		if len(sqls) > 0 {
			log.Infof("model %s is not in SYNC!!!", modelName)
			if autoSync {
				if ts, ok := tableSpec.(*sTableSpec); ok {
					err = commitSqlDiffWithName(sqls, ts.GetDBName())
				} else {
					err = commitSqlDIffs(sqls)
				}
				if err != nil {
					log.Errorf("commit sql error %s", err)
					return false
				}
			} else {
				for _, sql := range sqls {
					log.Infof("%s;", sql)
				}
				inSync = false
			}
		}

		recordMan, ok := modelMan.(IRecordChecksumModelManager)
		if ok {
			recordMan.SetEnableRecordChecksum(enableChecksumTables)
			if recordMan.EnableRecordChecksum() {
				if len(sqls) > 0 || !skipInitChecksum {
					if err := InjectModelsChecksum(recordMan); err != nil {
						log.Errorf("InjectModelsChecksum for %q error: %v", modelMan.TableSpec().Name(), err)
						return false
					}
				}
			}
		}
	}
	return inSync
}

func EnsureAppSyncDB(app *appsrv.Application, opt *common_options.DBOptions, modelInitDBFunc func() error) {
	// cloudcommon.InitDB(opt)

	if !CheckSync(opt.AutoSyncTable, opt.EnableDBChecksumTables, opt.DBChecksumSkipInit) {
		log.Fatalf("database schema not in sync!")
	}

	if modelInitDBFunc != nil {
		if err := modelInitDBFunc(); err != nil {
			log.Fatalf("model init db: %v", err)
		}
	}

	if opt.ExitAfterDBInit {
		log.Infof("Exiting after db initialization ...")
		os.Exit(0)
	}

	AppDBInit(app)
}

func GetModelManager(keyword string) IModelManager {
	modelMan, ok := globalTables[keyword]
	if ok {
		return modelMan
	} else {
		return nil
	}
}

func commitSqlDIffs(sqls []string) error {
	db := sqlchemy.GetDB()

	for _, sql := range sqls {
		log.Infof("Exec %s", sql)
		_, err := db.Exec(sql)
		if err != nil {
			log.Errorf("Exec sql failed %s\n%s", sql, err)
			return err
		}
	}
	return nil
}

func commitSqlDiffWithName(sqls []string, dbName sqlchemy.DBName) error {
	db := sqlchemy.GetDBWithName(dbName)
	return execSqlDiffWithDb(sqls, db)
}

func execSqlDiffWithDb(sqls []string, db *sqlchemy.SDatabase) error {
	for _, sql := range sqls {
		log.Infof("Exec %s", sql)
		_, err := db.Exec(sql)
		if err != nil {
			log.Errorf("Exec sql failed %s\n%s", sql, err)
			return err
		}
	}
	return nil
}
