package hmndata

import (
	"context"
	"strings"

	"git.handmade.network/hmn/hmn/src/db"
	"git.handmade.network/hmn/hmn/src/models"
	"git.handmade.network/hmn/hmn/src/oops"
	"git.handmade.network/hmn/hmn/src/perf"
)

type UsersQuery struct {
	// Ignored when using FetchUser
	UserIDs        []int    // if empty, all users
	Usernames      []string // if empty, all users
	DiscordUserIDs []string // if empty, no Discord filtering

	// Flags to modify behavior
	AnyStatus bool // Bypasses shadowban system
}

/*
Fetches users and related models from the database according to all the given
query params. For the most correct results, provide as much information as you have
on hand.
*/
func FetchUsers(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	q UsersQuery,
) ([]*models.User, error) {
	perf := perf.ExtractPerf(ctx)
	perf.StartBlock("SQL", "Fetch users")
	defer perf.EndBlock()

	var currentUserID *int
	if currentUser != nil {
		currentUserID = &currentUser.ID
	}

	for i := range q.Usernames {
		q.Usernames[i] = strings.ToLower(q.Usernames[i])
	}

	type userRow struct {
		User        models.User         `db:"hmn_user"`
		AvatarAsset *models.Asset       `db:"avatar"`
		DiscordUser *models.DiscordUser `db:"discord_user"`
	}

	var qb db.QueryBuilder
	qb.Add(`
		SELECT $columns
		FROM
			hmn_user
			LEFT JOIN asset AS avatar ON avatar.id = hmn_user.avatar_asset_id
			LEFT JOIN discord_user ON discord_user.hmn_user_id = hmn_user.id
		WHERE
			TRUE
	`)
	if len(q.UserIDs) > 0 {
		qb.Add(`AND hmn_user.id = ANY($?)`, q.UserIDs)
	}
	if len(q.Usernames) > 0 {
		qb.Add(`AND LOWER(hmn_user.username) = ANY($?)`, q.Usernames)
	}
	if len(q.DiscordUserIDs) > 0 {
		qb.Add(`AND discord_user.userid = ANY($?)`, q.DiscordUserIDs)
	}
	if !q.AnyStatus {
		if currentUser == nil {
			qb.Add(`AND hmn_user.status = $?`, models.UserStatusApproved)
		} else if !currentUser.IsStaff {
			qb.Add(
				`
				AND (
					hmn_user.status = $? -- user is Approved
					OR hmn_user.id = $? -- getting self
				)
				`,
				models.UserStatusApproved,
				currentUserID,
			)
		}
	}

	userRows, err := db.Query[userRow](ctx, dbConn, qb.String(), qb.Args()...)
	if err != nil {
		return nil, oops.New(err, "failed to fetch users")
	}

	result := make([]*models.User, len(userRows))
	for i, row := range userRows {
		user := row.User
		user.AvatarAsset = row.AvatarAsset
		user.DiscordUser = row.DiscordUser
		result[i] = &user
	}

	return result, nil
}

/*
Fetches a single user and related data. A wrapper around FetchUsers.
As with FetchUsers, provide as much information as you know to get the
most correct results.

Returns db.NotFound if no result is found.
*/
func FetchUser(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	userID int,
	q UsersQuery,
) (*models.User, error) {
	q.UserIDs = []int{userID}

	res, err := FetchUsers(ctx, dbConn, currentUser, q)
	if err != nil {
		return nil, oops.New(err, "failed to fetch user")
	}

	if len(res) == 0 {
		return nil, db.NotFound
	}

	return res[0], nil
}

/*
Fetches a single user and related data. A wrapper around FetchUsers.
As with FetchUsers, provide as much information as you know to get the
most correct results.

Returns db.NotFound if no result is found.
*/
func FetchUserByUsername(
	ctx context.Context,
	dbConn db.ConnOrTx,
	currentUser *models.User,
	username string,
	q UsersQuery,
) (*models.User, error) {
	q.Usernames = []string{username}

	res, err := FetchUsers(ctx, dbConn, currentUser, q)
	if err != nil {
		return nil, oops.New(err, "failed to fetch user")
	}

	if len(res) == 0 {
		return nil, db.NotFound
	}

	return res[0], nil
}

// NOTE(ben): Someday we can add CountUsers...I don't have a need for it right now.
