package ludusapi

import (
	"fmt"
	"log"
	"os"
	"sync"
	"time"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

var (
	once     sync.Once
	db       *gorm.DB
	database string = fmt.Sprintf("%s/ludus.db?cache=shared&_journal_mode=WAL", ludusInstallPath)
)

func InitDb() *gorm.DB {
	// Only initialize and open the DB once per run
	once.Do(func() {
		var err error

		db, err = gorm.Open(sqlite.Open(database), &gorm.Config{
			SkipDefaultTransaction: true,
		})
		if err != nil {
			log.Fatalf("error opening db: %v", err)
		}

		// Error
		if err != nil {
			panic(err)
		}
		// Create the tables if they don't exist and we are root
		if !db.Migrator().HasTable(&UserObject{}) && os.Geteuid() == 0 {
			db.Set("gorm:table_options", "ENGINE=InnoDB")
			db.Raw("PRAGMA journal_mode = WAL")
			db.Migrator().CreateTable(&UserObject{})
			db.Migrator().CreateTable(&RangeObject{})
			db.Migrator().CreateTable(&VmObject{})
			db.Migrator().CreateTable(&RangeAccessObject{})

			// Create a root user
			var user UserObject
			user.Name = "root"
			user.ProxmoxUsername = "root"
			user.UserID = "ROOT"
			user.DateCreated = time.Now()
			user.DateLastActive = time.Now()
			user.IsAdmin = true
			apiKey := GenerateAPIKey(&user)
			err := os.WriteFile(fmt.Sprintf("%s/install/root-api-key", ludusInstallPath), []byte(apiKey), 0400)
			if err != nil {
				log.Fatal(err.Error())
			}
			os.Chown(fmt.Sprintf("%s/install/root-api-key", ludusInstallPath), 0, 0)

			os.MkdirAll(fmt.Sprintf("%s/users/root", ludusInstallPath), 0700)

			user.HashedAPIKey, err = HashString(apiKey)
			if err != nil {
				log.Fatal("error hashing API Key for root user")
			}
			db.Create(&user)

			// Create a dummy range for ROOT to take up the range_id of 1 (since the server has the .1 address)
			var usersRange RangeObject
			usersRange.UserID = user.UserID
			usersRange.NumberOfVMs = 0
			usersRange.TestingEnabled = false
			db.Create(&usersRange)
		}
		// Only do migrations as ludus-admin service to prevent a race condition when starting services
		// that leads to the ludus service creating the tables via migration before the root api key
		// has been written
		if os.Geteuid() == 0 {
			// Migrate any updates from the models to an existing DB
			db.AutoMigrate(&UserObject{}, &RangeObject{}, &VmObject{}, &RangeAccessObject{})
			checkDBOwnership()
		}
	})

	return db
}

// findNextAvailableRangeNumber finds the smallest positive integer that is not
// present in the RangeNumber column of the RangeObject table. This function
// assumes that the RangeNumber values are positive integers and that there can
// be gaps or non-sequential values in the column. It will also skip any values
// in the reserved_range_numbers array in the config.
//
// The function takes a *gorm.DB as an argument, which should be a valid GORM
// database connection.
//
// Returns:
//
//	int32 - The smallest positive integer that is not present in the
//	        RangeNumber column of the RangeObject table.
func findNextAvailableRangeNumber(db *gorm.DB, reservedRangeNumbers []int32) int32 {
	var rangeNumbers []int32
	db.Model(&RangeObject{}).Select("range_number").Order("range_number").Scan(&rangeNumbers)

	for i := int32(1); ; i++ {
		found := false
		for _, num := range rangeNumbers {
			if num == i {
				found = true
				break
			}
		}
		if !found {
			// Check if this is a reserved range number
			for _, num := range reservedRangeNumbers {
				if num == i {
					found = true
					break
				}
			}
			// The number is not in the DB and not reserved, return it
			if !found {
				return i
			}
		}
	}
}

// checkDBOwnership makes sure the ludus.db is owned by ludus and not root
// In rare cases the DB can become owned by root if it is removed and the services
// are started out of order
func checkDBOwnership() {
	filename := fmt.Sprintf("%s/ludus.db", ludusInstallPath)
	desiredOwner := "ludus:ludus"

	// Check file ownership
	owner, err := getFileOwner(filename)
	if err != nil {
		log.Println("Error getting ownership of ludus.db:", err)
		return
	}

	// If file is not owned by ludus:ludus, change ownership
	if owner != desiredOwner {
		err := changeFileOwner(filename, desiredOwner)
		if err != nil {
			log.Println("Error changing ownership of ludus.db:", err)
			return
		}
		log.Printf("%s ownership changed to %s\n", filename, desiredOwner)
	} else {
		log.Printf("%s is owned by %s\n", filename, desiredOwner)
	}
}
