package services

import (
	"context"
	"fmt"
	"time"
	"unicode"

	"git.solsynth.dev/hypernet/nexus/pkg/nex"
	"git.solsynth.dev/hypernet/nexus/pkg/proto"
	"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
	localCache "git.solsynth.dev/hypernet/passport/pkg/internal/cache"
	"github.com/eko/gocache/lib/v4/cache"
	"github.com/eko/gocache/lib/v4/marshaler"
	"github.com/eko/gocache/lib/v4/store"

	"git.solsynth.dev/hypernet/passport/pkg/internal/gap"

	"gorm.io/gorm/clause"

	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
	"gorm.io/datatypes"

	"git.solsynth.dev/hypernet/passport/pkg/internal/database"
	"github.com/samber/lo"
)

func GetAccountCacheKey(query any) string {
	return fmt.Sprintf("account-query#%v", query)
}

func CacheAccount(account models.Account) {
	cacheManager := cache.New[any](localCache.S)
	marshal := marshaler.New(cacheManager)
	ctx := context.Background()

	_ = marshal.Set(
		ctx,
		GetAccountCacheKey(account.Name),
		account,
		store.WithExpiration(30*time.Minute),
		store.WithTags([]string{"account", fmt.Sprintf("user#%d", account.ID)}),
	)
	_ = marshal.Set(
		ctx,
		GetAccountCacheKey(account.ID),
		account,
		store.WithExpiration(30*time.Minute),
		store.WithTags([]string{"account", fmt.Sprintf("user#%d", account.ID)}),
	)
}

func ValidateAccountName(val string, min, max int) bool {
	actualLength := 0
	for _, r := range val {
		if unicode.Is(unicode.Han, r) || unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) || unicode.Is(unicode.Hangul, r) {
			actualLength += 2
		} else {
			actualLength += 1
		}
	}
	return actualLength >= min && max >= actualLength
}

func GetAccount(id uint) (models.Account, error) {
	var account models.Account
	if err := database.C.Where(models.Account{
		BaseModel: models.BaseModel{ID: id},
	}).First(&account).Error; err != nil {
		return account, err
	}

	return account, nil
}

func GetAccountList(id []uint) ([]models.Account, error) {
	var accounts []models.Account
	if err := database.C.Where("id IN ?", id).Find(&accounts).Error; err != nil {
		return accounts, err
	}

	return accounts, nil
}

func GetAccountWithName(alias string) (models.Account, error) {
	var account models.Account
	if err := database.C.Where(models.Account{
		Name: alias,
	}).First(&account).Error; err != nil {
		return account, err
	}

	return account, nil
}

func LookupAccount(probe string) (models.Account, error) {
	var account models.Account
	if err := database.C.Where(models.Account{Name: probe}).First(&account).Error; err == nil {
		return account, nil
	}

	var contact models.AccountContact
	if err := database.C.Where(models.AccountContact{Content: probe}).First(&contact).Error; err == nil {
		if err := database.C.
			Where(models.Account{
				BaseModel: models.BaseModel{ID: contact.AccountID},
			}).First(&account).Error; err == nil {
			return account, err
		}
	}

	return account, fmt.Errorf("account was not found")
}

func SearchAccount(probe string) ([]models.Account, error) {
	probe = "%" + probe + "%"
	var accounts []models.Account
	if err := database.C.Where("name LIKE ? OR nick LIKE ?", probe, probe).Find(&accounts).Error; err != nil {
		return accounts, err
	}
	return accounts, nil
}

func CreateAccount(name, nick, email, password string) (models.Account, error) {
	user := models.Account{
		Name: name,
		Nick: nick,
		Profile: models.AccountProfile{
			Experience: 100,
		},
		Factors: []models.AuthFactor{
			{
				Type:   models.PasswordAuthFactor,
				Secret: HashPassword(password),
			},
		},
		Contacts: []models.AccountContact{
			{
				Type:       models.EmailAccountContact,
				Content:    email,
				IsPrimary:  true,
				VerifiedAt: nil,
			},
		},
		PermNodes:   datatypes.JSONMap{},
		ConfirmedAt: nil,
	}

	if err := database.C.Create(&user).Error; err != nil {
		return user, err
	} else if viper.GetInt("default_user_group") > 0 {
		database.C.Create(&models.AccountGroupMember{
			AccountID: user.ID,
			GroupID:   uint(viper.GetInt("default_user_group")),
		})
	}

	if tk, err := NewMagicToken(models.ConfirmMagicToken, &user, nil); err != nil {
		return user, err
	} else if err := NotifyMagicToken(tk); err != nil {
		return user, err
	}

	return user, nil
}

func ConfirmAccount(code string) error {
	token, err := ValidateMagicToken(code, models.ConfirmMagicToken)
	if err != nil {
		return err
	} else if token.AccountID == nil {
		return fmt.Errorf("magic token didn't assign a valid account")
	}

	var user models.Account
	if err := database.C.Where(&models.Account{
		BaseModel: models.BaseModel{ID: *token.AccountID},
	}).First(&user).Error; err != nil {
		return err
	}

	if err = ForceConfirmAccount(user); err != nil {
		return err
	} else {
		database.C.Delete(&token)
	}

	return nil
}

func ForceConfirmAccount(user models.Account) error {
	user.ConfirmedAt = lo.ToPtr(time.Now())

	for k, v := range viper.GetStringMap("permissions.verified") {
		if val, ok := user.PermNodes[k]; !ok {
			user.PermNodes[k] = v
		} else {
			user.PermNodes[k] = val
		}
	}

	if err := database.C.Save(&user).Error; err != nil {
		return err
	}

	InvalidAuthCacheWithUser(user.ID)

	return nil
}

func CheckAbleToDeleteAccount(user models.Account) error {
	if user.AutomatedID != nil {
		return fmt.Errorf("bot cannot request delete account, head to developer portal and dispose bot")
	}

	var count int64
	if err := database.C.
		Where("account_id = ?", user.ID).
		Where("expired_at < ?", time.Now()).
		Where("type = ?", models.DeleteAccountMagicToken).
		Model(&models.MagicToken{}).
		Count(&count).Error; err != nil {
		return fmt.Errorf("unable to check delete account ability: %v", err)
	} else if count > 0 {
		return fmt.Errorf("you requested delete account recently")
	}

	return nil
}

func RequestDeleteAccount(user models.Account) error {
	if tk, err := NewMagicToken(
		models.DeleteAccountMagicToken,
		&user,
		lo.ToPtr(time.Now().Add(24*time.Hour)),
	); err != nil {
		return err
	} else if err := NotifyMagicToken(tk); err != nil {
		log.Error().
			Err(err).
			Str("code", tk.Code).
			Uint("user", user.ID).
			Msg("Failed to notify delete account magic token...")
	}

	return nil
}

func ConfirmDeleteAccount(code string) error {
	token, err := ValidateMagicToken(code, models.DeleteAccountMagicToken)
	if err != nil {
		return err
	} else if token.AccountID == nil {
		return fmt.Errorf("magic token didn't assign a valid account")
	}

	if err := DeleteAccount(*token.AccountID); err != nil {
		return err
	} else {
		database.C.Delete(&token)
	}

	return nil
}

func CheckAbleToResetPassword(user models.Account) error {
	var count int64
	if err := database.C.
		Where("account_id = ?", user.ID).
		Where("expired_at < ?", time.Now()).
		Where("type = ?", models.ResetPasswordMagicToken).
		Model(&models.MagicToken{}).
		Count(&count).Error; err != nil {
		return fmt.Errorf("unable to check reset password ability: %v", err)
	} else if count > 0 {
		return fmt.Errorf("you requested reset password recently")
	}

	return nil
}

func RequestResetPassword(user models.Account) error {
	if tk, err := NewMagicToken(
		models.ResetPasswordMagicToken,
		&user,
		lo.ToPtr(time.Now().Add(24*time.Hour)),
	); err != nil {
		return err
	} else if err := NotifyMagicToken(tk); err != nil {
		log.Error().
			Err(err).
			Str("code", tk.Code).
			Uint("user", user.ID).
			Msg("Failed to notify password reset magic token...")
	}

	return nil
}

func ConfirmResetPassword(code, newPassword string) error {
	token, err := ValidateMagicToken(code, models.ResetPasswordMagicToken)
	if err != nil {
		return err
	} else if token.AccountID == nil {
		return fmt.Errorf("magic token didn't assign a valid account")
	}

	factor, err := GetPasswordTypeFactor(*token.AccountID)
	if err != nil {
		factor = models.AuthFactor{
			Type:      models.PasswordAuthFactor,
			Secret:    HashPassword(newPassword),
			AccountID: *token.AccountID,
		}
	} else {
		factor.Secret = HashPassword(newPassword)
	}

	if err = database.C.Save(&factor).Error; err != nil {
		return err
	} else {
		database.C.Delete(&token)
	}

	return nil
}

func DeleteAccount(id uint) error {
	tx := database.C.Begin()

	if err := tx.Select(clause.Associations).Delete(&models.Account{}, "id = ?", id).Error; err != nil {
		tx.Rollback()
		return err
	}

	if err := tx.Commit().Error; err != nil {
		return err
	} else {
		InvalidAuthCacheWithUser(id)
		_, _ = proto.NewDirectoryServiceClient(gap.Nx.GetNexusGrpcConn()).BroadcastEvent(context.Background(), &proto.EventInfo{
			Event: "deletion",
			Data: nex.EncodeMap(map[string]any{
				"type": "account",
				"id":   fmt.Sprintf("%d", id),
			}),
		})
	}

	return nil
}

func RecycleUnConfirmAccount() {
	deadline := time.Now().Add(-24 * time.Hour)

	var hitList []models.Account
	if err := database.C.Where("confirmed_at IS NULL AND created_at <= ?", deadline).Find(&hitList).Error; err != nil {
		log.Error().Err(err).Msg("An error occurred while recycling accounts...")
		return
	}

	if len(hitList) > 0 {
		log.Info().Int("count", len(hitList)).Msg("Going to recycle those un-confirmed accounts...")
		for _, entry := range hitList {
			if err := DeleteAccount(entry.ID); err != nil {
				log.Error().Err(err).Msg("An error occurred while recycling accounts...")
			}
		}
	}
}

func SetAccountLastSeen(uid uint) error {
	var profile models.AccountProfile
	if err := database.C.Where("account_id = ?", uid).First(&profile).Error; err != nil {
		return err
	}

	profile.LastSeenAt = lo.ToPtr(time.Now())

	return database.C.Save(&profile).Error
}