package usersclient_internal

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"time"

	"code.justin.tv/foundation/twitchclient"
	util "code.justin.tv/web/users-service/client"
	"code.justin.tv/web/users-service/models"
)

const (
	batchURLPath = "/users"
)

// InternalClient is an interface that exposes methods to fetch data from the users service.
//go:generate retool do mockery -name InternalClient
type InternalClient interface {
	GetUserByID(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetUserByIDAndParams(ctx context.Context, userID string, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetUserByLogin(ctx context.Context, login string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error)
	GetIDsForUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.IDsResult, error)
	GetUserByEmail(ctx context.Context, email string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetUserByPhoneNumber(ctx context.Context, phoneNumber string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetBannedUsers(ctx context.Context, until time.Time, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error)
	GetUsersByLoginLike(ctx context.Context, pattern string, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error)
	GetRenameEligibility(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.RenameProperties, error)
	BanUserByID(ctx context.Context, properties *models.BanUserProperties, reqOpts *twitchclient.ReqOpts) error
	UnbanUserByID(ctx context.Context, userID string, unbanProp *models.UnbanUserProperties, reqOpts *twitchclient.ReqOpts) error
	AddDMCAStrike(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error
	RemoveDMCAStrike(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error
	SetUser(ctx context.Context, userID string, uup *models.UpdateableProperties, reqOpts *twitchclient.ReqOpts) error
	SetUserSudo(ctx context.Context, userID string, uup *models.UpdateableProperties, sudoLoginAt time.Time, reqOpts *twitchclient.ReqOpts) error
	CreateUser(ctx context.Context, cup *models.CreateUserProperties, reqOpts *twitchclient.ReqOpts) (*models.Properties, error)
	GetGlobalPrivilegedUsers(ctx context.Context, roles []string, reqOpts *twitchclient.ReqOpts) (*models.GlobalPrivilegedUsers, error)
	VerifyUserPhoneNumber(ctx context.Context, userID, code string, reqOpts *twitchclient.ReqOpts) error
	ExpireUserByID(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error
	GetTypeForLogins(ctx context.Context, logins []string, isRename bool, reqOpts *twitchclient.ReqOpts) (*models.LoginTypePropertiesResult, error)
	GetTypeForLogin(ctx context.Context, login string, isRename bool, reqOpts *twitchclient.ReqOpts) (*models.LoginTypeProperties, error)
	UploadUserImage(ctx context.Context, userID string, uup models.UploadableImage, reqOpts *twitchclient.ReqOpts) (*models.UploadInfo, error)
	SetUserImageMetadata(ctx context.Context, uup models.ImageProperties, reqOpts *twitchclient.ReqOpts) error
	SetUserImageMetadataAuthed(ctx context.Context, editor string, uup models.ImageProperties, reqOpts *twitchclient.ReqOpts) error
	HardDeleteUser(ctx context.Context, userID string, adminLogin string, skipBlock bool, reqOpts *twitchclient.ReqOpts) error
	SoftDeleteUser(ctx context.Context, userID string, adminLogin string, isUserRequestedDestroy bool, isRename bool, flaggedToDelete bool, description string, reqOpts *twitchclient.ReqOpts) error
	SoftDeleteUsers(ctx context.Context, userIDs []string, adminLogin string, isUserRequestedDestroy bool, isRename bool, flaggedToDelete bool, description string, reqOpts *twitchclient.ReqOpts) error
	UndeleteUser(ctx context.Context, userID string, adminLogin string, reqOpts *twitchclient.ReqOpts) error
	RestoreEmail(ctx context.Context, token string, reqOpts *twitchclient.ReqOpts) error
	GeneratePrivacyData(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error
	UsernameReset(ctx context.Context, ur *models.UsernameResetProperties, reqOpts *twitchclient.ReqOpts) error
	CreateUsernameResetToken(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.UsernameResetTokenResult, error)
}

type clientImpl struct {
	twitchclient.Client
}

// NewClient creates a client for the users service.
func NewClient(conf twitchclient.ClientConf) (InternalClient, error) {
	if conf.TimingXactName == "" {
		conf.TimingXactName = util.DefaultTimingXactName
	}

	twitchClient, err := twitchclient.NewClient(conf)
	if err != nil {
		return nil, err
	}

	return &clientImpl{twitchClient}, nil
}

func (c *clientImpl) GetUserByID(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	return c.GetUserByIDAndParams(ctx, userID, nil, reqOpts)
}

func (c *clientImpl) GetUserByIDAndParams(ctx context.Context, userID string, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	query := url.Values{}
	util.ModifyQuery(&query, params)
	query.Add("return_id_as_string", "true")

	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.Properties
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	case http.StatusNotFound:
		return nil, &util.UserNotFoundError{}

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GetUserByLogin(ctx context.Context, login string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	params := &models.FilterParams{Logins: []string{login}}
	users, err := c.GetUsers(ctx, params, reqOpts)
	if err != nil {
		return nil, err
	}
	if users == nil || len(users.Results) < 1 {
		return nil, &util.UserNotFoundError{}
	}
	return users.Results[0], nil
}

func (c *clientImpl) GetUserByEmail(ctx context.Context, email string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	params := &models.FilterParams{Emails: []string{email}}
	users, err := c.GetUsers(ctx, params, reqOpts)
	if err != nil {
		return nil, err
	}
	if users == nil || len(users.Results) < 1 {
		return nil, &util.UserNotFoundError{}
	}
	return users.Results[0], nil
}

func (c *clientImpl) GetUserByPhoneNumber(ctx context.Context, phoneNumber string, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	params := &models.FilterParams{PhoneNumbers: []string{phoneNumber}}
	users, err := c.GetUsers(ctx, params, reqOpts)
	if err != nil {
		return nil, err
	}
	if users == nil || len(users.Results) < 1 {
		return nil, &util.UserNotFoundError{}
	}
	return users.Results[0], nil
}

func (c *clientImpl) GetUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error) {
	batches := util.BatchParams(params)
	ch := make(chan *util.BatchResult, len(batches))
	for _, batch := range batches {
		subCtx, cancel := context.WithCancel(ctx)
		defer cancel()
		go util.GetAsync(subCtx, c, batchURLPath, batch, ch, reqOpts)
	}

	return util.CombineBatches(ch, batches)
}

func (c *clientImpl) GetIDsForUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchclient.ReqOpts) (*models.IDsResult, error) {
	batches := util.BatchParams(params)
	ch := make(chan *util.BatchIDResult, len(batches))
	for _, batch := range batches {
		subCtx, cancel := context.WithCancel(ctx)
		defer cancel()
		go util.GetAsyncIDOnly(subCtx, c, batchURLPath, batch, ch, reqOpts)
	}

	return util.CombineBatchIDs(ch, batches)
}

func (c *clientImpl) GetUsersByLoginLike(ctx context.Context, pattern string, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error) {
	query := url.Values{}
	util.ModifyQueryFieldValue(&query, "login_like", pattern)

	query.Add("return_id_as_string", "true")
	path := (&url.URL{
		Path:     "/users",
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_users_like",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.PropertiesResult
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GetTypeForLogins(ctx context.Context, logins []string, isRename bool, reqOpts *twitchclient.ReqOpts) (*models.LoginTypePropertiesResult, error) {
	query := url.Values{}
	params := &models.FilterParams{Logins: logins}
	util.ModifyQuery(&query, params)

	if isRename {
		query.Add("is_rename", "true")
	}

	path := (&url.URL{
		Path:     "/logins",
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_type_for_logins",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.LoginTypePropertiesResult
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GetTypeForLogin(ctx context.Context, login string, isRename bool, reqOpts *twitchclient.ReqOpts) (*models.LoginTypeProperties, error) {
	logins, err := c.GetTypeForLogins(ctx, []string{login}, isRename, reqOpts)
	if err != nil {
		return nil, err
	}
	if logins == nil || len(logins.Results) < 1 {
		return nil, &util.UserNotFoundError{}
	}
	return logins.Results[0], nil
}

func (c *clientImpl) GetBannedUsers(ctx context.Context, until time.Time, reqOpts *twitchclient.ReqOpts) (*models.PropertiesResult, error) {
	query := url.Values{}

	query.Add("until", until.Format(time.RFC3339))
	query.Add("return_id_as_string", "true")

	path := (&url.URL{
		Path:     "/banned_users",
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_banned_users",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.PropertiesResult
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GetRenameEligibility(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.RenameProperties, error) {
	path := fmt.Sprintf("/users/%s/rename_eligible", url.PathEscape(userID))
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_rename_eligibility",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.RenameProperties
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	case http.StatusNotFound:
		return nil, &util.UserNotFoundError{}

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) BanUserByID(ctx context.Context, properties *models.BanUserProperties, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/ban", url.PathEscape(properties.TargetUserID))

	bodyJson, err := json.Marshal(properties)
	if err != nil {
		return err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("PUT", path, body)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.ban_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) UnbanUserByID(ctx context.Context, userID string, unbanProp *models.UnbanUserProperties, reqOpts *twitchclient.ReqOpts) error {
	query := url.Values{}

	query.Add("decrement", strconv.Itoa(unbanProp.Decrement))

	if unbanProp.ResetCount {
		query.Add("reset_violation_counts", "true")
	}

	if unbanProp.DecrementTos {
		query.Add("ban_type", "tos")
	}

	if unbanProp.DecrementDmca {
		query.Add("ban_type", "dmca")
	}

	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s/ban", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	req, err := c.NewRequest("DELETE", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) AddDMCAStrike(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/dmca_strike", url.PathEscape(userID))
	req, err := c.NewRequest("PUT", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) RemoveDMCAStrike(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/dmca_strike", url.PathEscape(userID))
	req, err := c.NewRequest("DELETE", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) ExpireUserByID(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/cache", url.PathEscape(userID))
	req, err := c.NewRequest("DELETE", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.expire_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) setUser(ctx context.Context, urlPath string, uup *models.UpdateableProperties, reqOpts *twitchclient.ReqOpts) error {
	bodyJson, err := json.Marshal(uup)
	if err != nil {
		return err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("PATCH", urlPath, body)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.update_user_properties",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) SetUser(ctx context.Context, userID string, uup *models.UpdateableProperties, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s", url.PathEscape(userID))
	return c.setUser(ctx, path, uup, reqOpts)
}

// SetUserSudo calls SetUser but with the loginAt timestamp from cartman
func (c *clientImpl) SetUserSudo(ctx context.Context, userID string, uup *models.UpdateableProperties, sudoLoginAt time.Time, reqOpts *twitchclient.ReqOpts) error {
	query := url.Values{}
	query.Add("login_at", sudoLoginAt.Format(time.RFC3339))

	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s/sudo", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	return c.setUser(ctx, path, uup, reqOpts)
}

func (c *clientImpl) VerifyUserPhoneNumber(ctx context.Context, userID, code string, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/verify_phone_number", url.PathEscape(userID))

	bodyJSON, err := json.Marshal(models.PhoneNumberCodeProperties{
		Code: code,
	})
	if err != nil {
		return err
	}

	body := bytes.NewBuffer(bodyJSON)

	req, err := c.NewRequest("POST", path, body)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.verify_phone_number",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	case http.StatusNotFound:
		return &util.UserNotFoundError{}

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) CreateUser(ctx context.Context, cup *models.CreateUserProperties, reqOpts *twitchclient.ReqOpts) (*models.Properties, error) {
	path := "/users"

	bodyJson, err := json.Marshal(cup)
	if err != nil {
		return nil, err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("POST", path, body)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.create_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.Properties
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GetGlobalPrivilegedUsers(ctx context.Context, roles []string, reqOpts *twitchclient.ReqOpts) (*models.GlobalPrivilegedUsers, error) {
	query := url.Values{}

	if len(roles) > 0 {
		for _, role := range roles {
			query.Add("role", role)
		}
	}
	path := (&url.URL{
		Path:     "/global_privileged_users",
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_global_privileged_users",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.GlobalPrivilegedUsers
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) SetUserImageMetadata(ctx context.Context, uup models.ImageProperties, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/images/metadata", url.PathEscape(uup.ID))
	bodyJson, err := json.Marshal(uup)
	if err != nil {
		return err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("PATCH", path, body)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.set_image_metadata",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) HardDeleteUser(ctx context.Context, userID string, adminLogin string, skipBlock bool, reqOpts *twitchclient.ReqOpts) error {
	if userID == "" || adminLogin == "" {
		return models.ErrNoIdentifiers
	}

	query := url.Values{}
	query.Add("destroy", "true")
	if skipBlock {
		query.Add("skip_block", "true")
	}

	return c.deleteUser(ctx, userID, adminLogin, query, reqOpts)
}

func (c *clientImpl) SoftDeleteUsers(ctx context.Context, userIDs []string, adminLogin string, isUserRequestedDestroy bool, isRename bool, flaggedToDelete bool, description string, reqOpts *twitchclient.ReqOpts) error {
	if len(userIDs) == 0 {
		return models.ErrNoIdentifiers
	}

	if adminLogin == "" {
		return models.ErrAdminLoginRequired
	}

	if len(userIDs) > 10 {
		return models.ErrTooManyIdentifiers
	}

	if len(description) >= 1024 {
		return models.ErrDescriptionTooLong
	}
	// Clean string to prevent code injection
	reg, err := regexp.Compile(`[~^&*|\\{}\[\];:}]`)
	if err != nil {
		return models.ErrClientUnexpected
	}
	cleanDescription := reg.ReplaceAllString(description, "_")

	query := url.Values{}
	query.Add("isUserRequestedDestroy", strconv.FormatBool(isUserRequestedDestroy))
	query.Add("is_rename", strconv.FormatBool(isRename))
	if flaggedToDelete {
		query.Add("flagged_to_delete", "true")
	}
	query.Add("description", url.QueryEscape(cleanDescription))
	for _, id := range userIDs {
		query.Add("id", id)
	}
	query.Add("admin", adminLogin)
	path := (&url.URL{
		Path:     "/users",
		RawQuery: query.Encode(),
	}).String()

	req, err := c.NewRequest("DELETE", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.soft_delete_user_bulk",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusNotFound:
		return &util.UserNotFoundError{}
	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

// swagger:route DELETE /delete/:id?admin=adminLogin
// Disable a user account abd request that the user's account be destroyed after a period of time.
// Inputs:
// 		userID: description:Twitch User ID
// 		adminLogin: description:Ldap login or name of user performing the delete. The name is stored in the audit log.
// 		isUserRequestedDestroy: description:In addition to disabling the user, have the user account destroyed after a period of time.
// 		flaggedToDelete: will flag the account so that it cannot be reactivated using self service reactivation.
//						 This is mainly to indicate that the owner of the email on the account did not signup for this account and has requested that the account be deleted.
// 		description: description:Reason for performing the soft delete action (can be user provided)
func (c *clientImpl) SoftDeleteUser(ctx context.Context, userID string, adminLogin string, isUserRequestedDestroy bool, isRename bool, flaggedToDelete bool, description string, reqOpts *twitchclient.ReqOpts) error {
	if userID == "" || adminLogin == "" {
		return models.ErrNoIdentifiers
	}

	query := url.Values{}

	if len(description) >= 1024 {
		return models.ErrDescriptionTooLong
	}

	// Clean string to prevent code injection
	reg, err := regexp.Compile(`[~^&*|\\{}\[\];:}]`)
	if err != nil {
		return models.ErrClientUnexpected
	}
	cleanDescription := reg.ReplaceAllString(description, "_")

	query.Add("isUserRequestedDestroy", strconv.FormatBool(isUserRequestedDestroy))
	query.Add("is_rename", strconv.FormatBool(isRename))
	if flaggedToDelete {
		query.Add("flagged_to_delete", "true")
	}
	query.Add("description", url.QueryEscape(cleanDescription))
	return c.deleteUser(ctx, userID, adminLogin, query, reqOpts)
}

func (c *clientImpl) deleteUser(ctx context.Context, userID string, adminLogin string, query url.Values, reqOpts *twitchclient.ReqOpts) error {
	query.Add("admin", adminLogin)
	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	req, err := c.NewRequest("DELETE", path, nil)
	if err != nil {
		return err
	}

	statName := "soft"
	if query.Get("destroy") == "true" {
		statName = "hard"
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       fmt.Sprintf("service.users_service.%s_delete_user", statName),
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusNotFound:
		return &util.UserNotFoundError{}
	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) UndeleteUser(ctx context.Context, userID string, adminLogin string, reqOpts *twitchclient.ReqOpts) error {
	query := url.Values{}
	query.Add("admin", adminLogin)
	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s/undelete", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	req, err := c.NewRequest("PATCH", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.undelete_user",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusNotFound:
		return &util.UserNotFoundError{}
	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) UploadUserImage(ctx context.Context, userID string, uup models.UploadableImage, reqOpts *twitchclient.ReqOpts) (*models.UploadInfo, error) {
	query := url.Values{}
	query.Add("return_upload_info_as_struct", "true")

	path := (&url.URL{
		Path:     fmt.Sprintf("/users/%s/images", url.PathEscape(userID)),
		RawQuery: query.Encode(),
	}).String()

	bodyJson, err := json.Marshal(uup)
	if err != nil {
		return nil, err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("PATCH", path, body)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.upload_user_image",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var uploadInfo models.UploadInfo
		if err := json.NewDecoder(httpResp.Body).Decode(&uploadInfo); err != nil {
			return nil, err
		}
		return &uploadInfo, nil

	default:
		// Unexpected result
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) SetUserImageMetadataAuthed(ctx context.Context, editor string, uup models.ImageProperties, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("/users/editor/%s/images", url.PathEscape(editor))
	bodyJson, err := json.Marshal(uup)
	if err != nil {
		return err
	}

	body := bytes.NewBuffer(bodyJson)

	req, err := c.NewRequest("PATCH", path, body)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.set_image_metadata_authed",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil

	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) RestoreEmail(ctx context.Context, token string, reqOpts *twitchclient.ReqOpts) error {
	if token == "" {
		return models.ErrEmptyToken
	}
	path := fmt.Sprintf("/users/restore_email/%s", token)
	req, err := c.NewRequest("PATCH", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.restore_email",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil
	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) GeneratePrivacyData(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) error {
	path := fmt.Sprintf("users/%s/generate_privacy_data", url.PathEscape(userID))
	req, err := c.NewRequest("POST", path, nil)
	if err != nil {
		return err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.generate_privacy_data",
		StatSampleRate: util.DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK, http.StatusNoContent:
		return nil
	case http.StatusNotFound:
		return &util.UserNotFoundError{}
	default:
		// Unexpected result
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) UsernameReset(ctx context.Context, ur *models.UsernameResetProperties, reqOpts *twitchclient.ReqOpts) error {
	path := "/username_reset"

	bodyJson, err := json.Marshal(ur)
	if err != nil {
		return err
	}
	body := bytes.NewBuffer(bodyJson)

	var req *http.Request
	req, err = c.NewRequest("PATCH", path, body)

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.username_reset",
		StatSampleRate: util.DefaultStatSampleRate,
	})

	var httpResp *http.Response
	httpResp, err = c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusNoContent:
		return nil
	default:
		return util.HandleUnexpectedResult(httpResp)
	}
}

func (c *clientImpl) CreateUsernameResetToken(ctx context.Context, userID string, reqOpts *twitchclient.ReqOpts) (*models.UsernameResetTokenResult, error) {
	if userID == "" {
		return nil, models.ErrNoIdentifiers
	}

	path := fmt.Sprintf("users/%s/username_reset_token", url.PathEscape(userID))
	req, err := c.NewRequest("POST", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.create_username_reset_token",
		StatSampleRate: util.DefaultStatSampleRate,
	})

	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		return nil, err
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.UsernameResetTokenResult
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			return nil, err
		}
		return &decoded, nil
	default:
		return nil, util.HandleUnexpectedResult(httpResp)
	}
}
