package daos

import (
	"database/sql"
	"errors"
	"fmt"
	"log"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/models/schema"
	"github.com/pocketbase/pocketbase/tools/list"
	"github.com/pocketbase/pocketbase/tools/security"
)

// UserQuery returns a new User model select query.
func (dao *Dao) UserQuery() *dbx.SelectQuery {
	return dao.ModelQuery(&models.User{})
}

// LoadProfile loads the profile record associated to the provided user.
func (dao *Dao) LoadProfile(user *models.User) error {
	collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
	if err != nil {
		return err
	}

	profile, err := dao.FindFirstRecordByData(collection, models.ProfileCollectionUserFieldName, user.Id)
	if err != nil && err != sql.ErrNoRows {
		return err
	}

	user.Profile = profile

	return nil
}

// LoadProfiles loads the profile records associated to the provied users list.
func (dao *Dao) LoadProfiles(users []*models.User) error {
	collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
	if err != nil {
		return err
	}

	// extract user ids
	ids := make([]string, len(users))
	usersMap := map[string]*models.User{}
	for i, user := range users {
		ids[i] = user.Id
		usersMap[user.Id] = user
	}

	profiles, err := dao.FindRecordsByExpr(collection, dbx.HashExp{
		models.ProfileCollectionUserFieldName: list.ToInterfaceSlice(ids),
	})
	if err != nil {
		return err
	}

	// populate each user.Profile member
	for _, profile := range profiles {
		userId := profile.GetStringDataValue(models.ProfileCollectionUserFieldName)
		user, ok := usersMap[userId]
		if !ok {
			continue
		}
		user.Profile = profile
	}

	return nil
}

// FindUserById finds a single User model by its id.
//
// This method also auto loads the related user profile record
// into the found model.
func (dao *Dao) FindUserById(id string) (*models.User, error) {
	model := &models.User{}

	err := dao.UserQuery().
		AndWhere(dbx.HashExp{"id": id}).
		Limit(1).
		One(model)

	if err != nil {
		return nil, err
	}

	// try to load the user profile (if exist)
	if err := dao.LoadProfile(model); err != nil {
		log.Println(err)
	}

	return model, nil
}

// FindUserByEmail finds a single User model by its email address.
//
// This method also auto loads the related user profile record
// into the found model.
func (dao *Dao) FindUserByEmail(email string) (*models.User, error) {
	model := &models.User{}

	err := dao.UserQuery().
		AndWhere(dbx.HashExp{"email": email}).
		Limit(1).
		One(model)

	if err != nil {
		return nil, err
	}

	// try to load the user profile (if exist)
	if err := dao.LoadProfile(model); err != nil {
		log.Println(err)
	}

	return model, nil
}

// FindUserByToken finds the user associated with the provided JWT token.
// Returns an error if the JWT token is invalid or expired.
//
// This method also auto loads the related user profile record
// into the found model.
func (dao *Dao) FindUserByToken(token string, baseTokenKey string) (*models.User, error) {
	unverifiedClaims, err := security.ParseUnverifiedJWT(token)
	if err != nil {
		return nil, err
	}

	// check required claims
	id, _ := unverifiedClaims["id"].(string)
	if id == "" {
		return nil, errors.New("Missing or invalid token claims.")
	}

	user, err := dao.FindUserById(id)
	if err != nil || user == nil {
		return nil, err
	}

	verificationKey := user.TokenKey + baseTokenKey

	// verify token signature
	if _, err := security.ParseJWT(token, verificationKey); err != nil {
		return nil, err
	}

	return user, nil
}

// IsUserEmailUnique checks if the provided email address is not
// already in use by other users.
func (dao *Dao) IsUserEmailUnique(email string, excludeId string) bool {
	if email == "" {
		return false
	}

	var exists bool
	err := dao.UserQuery().
		Select("count(*)").
		AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})).
		AndWhere(dbx.HashExp{"email": email}).
		Limit(1).
		Row(&exists)

	return err == nil && !exists
}

// DeleteUser deletes the provided User model.
//
// This method will also cascade the delete operation to all
// Record models that references the provided User model
// (delete or set to NULL, depending on the related user shema field settings).
//
// The delete operation may fail if the user is part of a required
// reference in another Record model (aka. cannot be deleted or set to NULL).
func (dao *Dao) DeleteUser(user *models.User) error {
	// fetch related records
	// note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
	relatedRecords, err := dao.FindUserRelatedRecords(user)
	if err != nil {
		return err
	}

	return dao.RunInTransaction(func(txDao *Dao) error {
		// check if related records has to be deleted (if `CascadeDelete` is set)
		// OR
		// just unset the user related fields (if they are not required)
		// -----------------------------------------------------------
	recordsLoop:
		for _, record := range relatedRecords {
			var needSave bool

			for _, field := range record.Collection().Schema.Fields() {
				if field.Type != schema.FieldTypeUser {
					continue // not a user field
				}

				ids := record.GetStringSliceDataValue(field.Name)

				// unset the user id
				for i := len(ids) - 1; i >= 0; i-- {
					if ids[i] == user.Id {
						ids = append(ids[:i], ids[i+1:]...)
						break
					}
				}

				options, _ := field.Options.(*schema.UserOptions)

				// cascade delete
				// (only if there are no other user references in case of multiple select)
				if options.CascadeDelete && len(ids) == 0 {
					if err := txDao.DeleteRecord(record); err != nil {
						return err
					}
					// no need to further iterate the user fields (the record is deleted)
					continue recordsLoop
				}

				if field.Required && len(ids) == 0 {
					return fmt.Errorf("Failed delete the user because a record exist with required user reference to the current model (%q, %q).", record.Id, record.Collection().Name)
				}

				// apply the reference changes
				record.SetDataValue(field.Name, field.PrepareValue(ids))
				needSave = true
			}

			if needSave {
				if err := txDao.SaveRecord(record); err != nil {
					return err
				}
			}
		}
		// -----------------------------------------------------------

		return txDao.Delete(user)
	})
}

// SaveUser upserts the provided User model.
//
// An empty profile record will be created if the user
// doesn't have a profile record set yet.
func (dao *Dao) SaveUser(user *models.User) error {
	profileCollection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName)
	if err != nil {
		return err
	}

	// fetch the related user profile record (if exist)
	var userProfile *models.Record
	if user.HasId() {
		userProfile, _ = dao.FindFirstRecordByData(
			profileCollection,
			models.ProfileCollectionUserFieldName,
			user.Id,
		)
	}

	return dao.RunInTransaction(func(txDao *Dao) error {
		if err := txDao.Save(user); err != nil {
			return err
		}

		// create default/empty profile record if doesn't exist
		if userProfile == nil {
			userProfile = models.NewRecord(profileCollection)
			userProfile.SetDataValue(models.ProfileCollectionUserFieldName, user.Id)
			if err := txDao.Save(userProfile); err != nil {
				return err
			}
			user.Profile = userProfile
		}

		return nil
	})
}
