package federation

import (
	"context"
	"strings"
	"time"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	"xorm.io/builder"
)

// HookTask represents a hook task.
// exact copy of models/webhook/hooktask.go when this migration was created
//   - xorm:"-" fields deleted
type FederatedHost struct {
	ID        int64 `xorm:"pk autoincr"`
	isBlocked bool
	HostFqdn  string `xorm:"UNIQUE(s) INDEX"`
}

func GetFederatdHost(ctx context.Context, hostFqdn string) (*FederatedHost, error) {
	rec := new(FederatedHost)
	_, err := db.GetEngine(ctx).
		Table("federated_host").Where("host_fqdn = ?", hostFqdn).Get(rec)
	if err != nil {
		return nil, err
	}
	return rec, nil
}

func FederatedHostExists(ctx context.Context, hostFqdn string) (bool, error) {
	rec := new(FederatedHost)
	exists, err := db.GetEngine(ctx).
		Table("federated_host").Where("host_fqdn = ?", hostFqdn).Get(rec)
	if err != nil {
		return false, err
	}
	return exists, nil
}

func (host *FederatedHost) Save(ctx context.Context) error {
	_, err := db.GetEngine(ctx).
		Insert(host)
	return err
}

type FederatedUser struct {
	ID               int64  `xorm:"pk autoincr"`
	UserID           int64  `xorm:"INDEX"`
	ExternalID       string `xorm:"UNIQUE(s) INDEX"`
	FederationHostID int64  `xorm:"INDEX"`
}

func CreateFederatedUser(ctx context.Context, alias string, website string, hostname string) (*user.User, error) {
	engine := db.GetEngine(ctx)

	// create FederatedHost
	exists, err := FederatedHostExists(ctx, hostname)
	if err != nil {
		return nil, err
	}

	var federatedHost FederatedHost
	if exists {
		x, err := GetFederatdHost(ctx, hostname)
		federatedHost = *x
		if err != nil {
			return nil, err
		}
	} else {
		federatedHost := new(FederatedHost)
		federatedHost.HostFqdn = hostname
		if err = federatedHost.Save(ctx); err != nil {
			return nil, err
		}
	}

	// create user.User
	u := new(user.User)
	u.Name = "@" + alias + "@" + hostname
	//panic(u.Name)
	u.Email = alias + "@" + hostname
	u.Website = website
	u.KeepEmailPrivate = true

	exist, err := user.GetUser(ctx, u)
	if err != nil {
		return nil, err
	}
	if exist {
		return u, nil // TODO: must also check for federatedUser existence
	}

	if err = createUser(ctx, u); err != nil {
		return nil, err
	}

	federatedUser := new(FederatedUser)
	federatedUser.ExternalID = u.Name
	federatedUser.UserID = u.ID
	federatedUser.FederationHostID = federatedHost.ID
	exist, err = engine.Get(federatedUser)
	if err != nil {
		return nil, err
	}

	if !exist {
		_, err = engine.Insert(federatedUser)
		if err != nil {
			return nil, err
		}
	}

	return u, nil
}

func createUser(ctx context.Context, u *user.User) error {
	// set system defaults
	u.Visibility = setting.Service.DefaultUserVisibilityMode
	u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
	u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
	u.MaxRepoCreation = -1
	u.Theme = setting.UI.DefaultTheme
	u.IsRestricted = setting.Service.DefaultUserIsRestricted
	u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)

	// Ensure consistency of the dates.
	if u.UpdatedUnix < u.CreatedUnix {
		u.UpdatedUnix = u.CreatedUnix
	}

	// validate data
	if err := user.ValidateUser(u); err != nil {
		return err
	}

	if err := user.ValidateEmail(u.Email); err != nil {
		return err
	}

	ctx, committer, err := db.TxContext(ctx)
	if err != nil {
		return err
	}
	defer committer.Close()

	isExist, err := user.IsUserExist(ctx, 0, u.Name)
	if err != nil {
		return err
	} else if isExist {
		return user.ErrUserAlreadyExist{u.Name}
	}

	isExist, err = user.IsEmailUsed(ctx, u.Email)
	if err != nil {
		return err
	} else if isExist {
		return user.ErrEmailAlreadyUsed{
			Email: u.Email,
		}
	}

	// prepare for database

	u.LowerName = strings.ToLower(u.Name)
	u.AvatarEmail = u.Email
	if u.Rands, err = user.GetUserSalt(); err != nil {
		return err
	}
	if u.Passwd != "" {
		if err = u.SetPassword(u.Passwd); err != nil {
			return err
		}
	} else {
		u.Salt = ""
		u.PasswdHashAlgo = ""
	}

	// save changes to database

	if err = user.DeleteUserRedirect(ctx, u.Name); err != nil {
		return err
	}

	if u.CreatedUnix == 0 {
		// Caller expects auto-time for creation & update timestamps.
		err = db.Insert(ctx, u)
	} else {
		// Caller sets the timestamps themselves. They are responsible for ensuring
		// both `CreatedUnix` and `UpdatedUnix` are set appropriately.
		_, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
	}
	if err != nil {
		return err
	}

	// insert email address
	if err := db.Insert(ctx, &user.EmailAddress{
		UID:         u.ID,
		Email:       u.Email,
		LowerEmail:  strings.ToLower(u.Email),
		IsActivated: u.IsActive,
		IsPrimary:   true,
	}); err != nil {
		return err
	}

	return committer.Commit()

}

func GetRemoteUsersWithNoLocalFollowers(ctx context.Context, olderThan time.Duration, page int) ([]user.User, error) {
	limit := 40
	offset := page * limit
	var users []user.User

	err := db.GetEngine(ctx).
		Table("user").
		Where("num_followers = 0").
		And(builder.Lt{"user.created_unix": time.Now().Add(-olderThan).Unix()}).
		Join("inner", "federated_user", "federated_user.user_id = user.id").
		Limit(limit, offset).
		Find(&users)

	if err != nil {
		log.Trace("Error: GetRemoteUserWithNoLocalFollowers: %w", err)
		return nil, err
	}
	return users, nil

}

func GetRemotePersons(ctx context.Context, page int) ([]FederatedUser, error) {
	limit := 1
	offset := page * limit
	var federatedUsers []FederatedUser

	err := db.GetEngine(ctx).
		Table("federated_user").
		Limit(limit, offset).
		Find(&federatedUsers)

		//	TODO: this doesn't work, so fetching federated_user and then getting user. How to make this work?
		//	var users []user.User
		//	err := db.GetEngine(ctx).
		//		Table("user").
		//		Join("inner", "federated_user", "federated_user.user_id = user.id").
		//		Limit(limit, offset).
		//		Find(&users)

	if err != nil {
		log.Trace("Error: GetRemotePersons: %w", err)
		return nil, err
	}
	return federatedUsers, nil
}
