package slack

import (
	"context"
	"encoding/json"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const (
	DEFAULT_USER_PHOTO_CROP_X = -1
	DEFAULT_USER_PHOTO_CROP_Y = -1
	DEFAULT_USER_PHOTO_CROP_W = -1
)

// UserProfile contains all the information details of a given user
type UserProfile struct {
	FirstName             string                  `json:"first_name"`
	LastName              string                  `json:"last_name"`
	RealName              string                  `json:"real_name"`
	RealNameNormalized    string                  `json:"real_name_normalized"`
	DisplayName           string                  `json:"display_name"`
	DisplayNameNormalized string                  `json:"display_name_normalized"`
	Email                 string                  `json:"email"`
	Skype                 string                  `json:"skype"`
	Phone                 string                  `json:"phone"`
	Image24               string                  `json:"image_24"`
	Image32               string                  `json:"image_32"`
	Image48               string                  `json:"image_48"`
	Image72               string                  `json:"image_72"`
	Image192              string                  `json:"image_192"`
	Image512              string                  `json:"image_512"`
	ImageOriginal         string                  `json:"image_original"`
	Title                 string                  `json:"title"`
	BotID                 string                  `json:"bot_id,omitempty"`
	ApiAppID              string                  `json:"api_app_id,omitempty"`
	StatusText            string                  `json:"status_text,omitempty"`
	StatusEmoji           string                  `json:"status_emoji,omitempty"`
	StatusExpiration      int                     `json:"status_expiration"`
	Team                  string                  `json:"team"`
	Fields                UserProfileCustomFields `json:"fields"`
}

// UserProfileCustomFields represents user profile's custom fields.
// Slack API's response data type is inconsistent so we use the struct.
// For detail, please see below.
// https://github.com/slack-go/slack/pull/298#discussion_r185159233
type UserProfileCustomFields struct {
	fields map[string]UserProfileCustomField
}

// UnmarshalJSON is the implementation of the json.Unmarshaler interface.
func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
	// https://github.com/slack-go/slack/pull/298#discussion_r185159233
	if string(b) == "[]" {
		return nil
	}
	return json.Unmarshal(b, &fields.fields)
}

// MarshalJSON is the implementation of the json.Marshaler interface.
func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) {
	if len(fields.fields) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(fields.fields)
}

// ToMap returns a map of custom fields.
func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField {
	return fields.fields
}

// Len returns the number of custom fields.
func (fields *UserProfileCustomFields) Len() int {
	return len(fields.fields)
}

// SetMap sets a map of custom fields.
func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) {
	fields.fields = m
}

// FieldsMap returns a map of custom fields.
func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField {
	return profile.Fields.ToMap()
}

// SetFieldsMap sets a map of custom fields.
func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) {
	profile.Fields.SetMap(m)
}

// UserProfileCustomField represents a custom user profile field
type UserProfileCustomField struct {
	Value string `json:"value"`
	Alt   string `json:"alt"`
	Label string `json:"label"`
}

// User contains all the information of a user
type User struct {
	ID                string         `json:"id"`
	TeamID            string         `json:"team_id"`
	Name              string         `json:"name"`
	Deleted           bool           `json:"deleted"`
	Color             string         `json:"color"`
	RealName          string         `json:"real_name"`
	TZ                string         `json:"tz,omitempty"`
	TZLabel           string         `json:"tz_label"`
	TZOffset          int            `json:"tz_offset"`
	Profile           UserProfile    `json:"profile"`
	IsBot             bool           `json:"is_bot"`
	IsAdmin           bool           `json:"is_admin"`
	IsOwner           bool           `json:"is_owner"`
	IsPrimaryOwner    bool           `json:"is_primary_owner"`
	IsRestricted      bool           `json:"is_restricted"`
	IsUltraRestricted bool           `json:"is_ultra_restricted"`
	IsStranger        bool           `json:"is_stranger"`
	IsAppUser         bool           `json:"is_app_user"`
	IsInvitedUser     bool           `json:"is_invited_user"`
	Has2FA            bool           `json:"has_2fa"`
	HasFiles          bool           `json:"has_files"`
	Presence          string         `json:"presence"`
	Locale            string         `json:"locale"`
	Updated           JSONTime       `json:"updated"`
	Enterprise        EnterpriseUser `json:"enterprise_user,omitempty"`
}

// UserPresence contains details about a user online status
type UserPresence struct {
	Presence        string   `json:"presence,omitempty"`
	Online          bool     `json:"online,omitempty"`
	AutoAway        bool     `json:"auto_away,omitempty"`
	ManualAway      bool     `json:"manual_away,omitempty"`
	ConnectionCount int      `json:"connection_count,omitempty"`
	LastActivity    JSONTime `json:"last_activity,omitempty"`
}

type UserIdentityResponse struct {
	User UserIdentity `json:"user"`
	Team TeamIdentity `json:"team"`
	SlackResponse
}

type UserIdentity struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	Image24  string `json:"image_24"`
	Image32  string `json:"image_32"`
	Image48  string `json:"image_48"`
	Image72  string `json:"image_72"`
	Image192 string `json:"image_192"`
	Image512 string `json:"image_512"`
}

// EnterpriseUser is present when a user is part of Slack Enterprise Grid
// https://api.slack.com/types/user#enterprise_grid_user_objects
type EnterpriseUser struct {
	ID             string   `json:"id"`
	EnterpriseID   string   `json:"enterprise_id"`
	EnterpriseName string   `json:"enterprise_name"`
	IsAdmin        bool     `json:"is_admin"`
	IsOwner        bool     `json:"is_owner"`
	Teams          []string `json:"teams"`
}

type TeamIdentity struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	Domain        string `json:"domain"`
	Image34       string `json:"image_34"`
	Image44       string `json:"image_44"`
	Image68       string `json:"image_68"`
	Image88       string `json:"image_88"`
	Image102      string `json:"image_102"`
	Image132      string `json:"image_132"`
	Image230      string `json:"image_230"`
	ImageDefault  bool   `json:"image_default"`
	ImageOriginal string `json:"image_original"`
}

type userResponseFull struct {
	Members []User `json:"members,omitempty"`
	User    `json:"user,omitempty"`
	Users   []User `json:"users,omitempty"`
	UserPresence
	SlackResponse
	Metadata ResponseMetadata `json:"response_metadata"`
}

type UserSetPhotoParams struct {
	CropX int
	CropY int
	CropW int
}

func NewUserSetPhotoParams() UserSetPhotoParams {
	return UserSetPhotoParams{
		CropX: DEFAULT_USER_PHOTO_CROP_X,
		CropY: DEFAULT_USER_PHOTO_CROP_Y,
		CropW: DEFAULT_USER_PHOTO_CROP_W,
	}
}

func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
	response := &userResponseFull{}
	err := api.postMethod(ctx, path, values, response)
	if err != nil {
		return nil, err
	}

	return response, response.Err()
}

// GetUserPresence will retrieve the current presence status of given user.
func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
	return api.GetUserPresenceContext(context.Background(), user)
}

// GetUserPresenceContext will retrieve the current presence status of given user with a custom context.
func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) {
	values := url.Values{
		"token": {api.token},
		"user":  {user},
	}

	response, err := api.userRequest(ctx, "users.getPresence", values)
	if err != nil {
		return nil, err
	}
	return &response.UserPresence, nil
}

// GetUserInfo will retrieve the complete user information
func (api *Client) GetUserInfo(user string) (*User, error) {
	return api.GetUserInfoContext(context.Background(), user)
}

// GetUserInfoContext will retrieve the complete user information with a custom context
func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
	values := url.Values{
		"token":          {api.token},
		"user":           {user},
		"include_locale": {strconv.FormatBool(true)},
	}

	response, err := api.userRequest(ctx, "users.info", values)
	if err != nil {
		return nil, err
	}
	return &response.User, nil
}

// GetUsersInfo will retrieve the complete multi-users information
func (api *Client) GetUsersInfo(users ...string) (*[]User, error) {
	return api.GetUsersInfoContext(context.Background(), users...)
}

// GetUsersInfoContext will retrieve the complete multi-users information with a custom context
func (api *Client) GetUsersInfoContext(ctx context.Context, users ...string) (*[]User, error) {
	values := url.Values{
		"token":          {api.token},
		"users":          {strings.Join(users, ",")},
		"include_locale": {strconv.FormatBool(true)},
	}

	response, err := api.userRequest(ctx, "users.info", values)
	if err != nil {
		return nil, err
	}
	return &response.Users, nil
}

// GetUsersOption options for the GetUsers method call.
type GetUsersOption func(*UserPagination)

// GetUsersOptionLimit limit the number of users returned
func GetUsersOptionLimit(n int) GetUsersOption {
	return func(p *UserPagination) {
		p.limit = n
	}
}

// GetUsersOptionPresence include user presence
func GetUsersOptionPresence(n bool) GetUsersOption {
	return func(p *UserPagination) {
		p.presence = n
	}
}

// GetUsersOptionTeamID include team Id
func GetUsersOptionTeamID(teamId string) GetUsersOption {
	return func(p *UserPagination) {
		p.teamId = teamId
	}
}

func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) {
	up = UserPagination{
		c:     c,
		limit: 200, // per slack api documentation.
	}

	for _, opt := range options {
		opt(&up)
	}

	return up
}

// UserPagination allows for paginating over the users
type UserPagination struct {
	Users        []User
	limit        int
	presence     bool
	teamId       string
	previousResp *ResponseMetadata
	c            *Client
}

// Done checks if the pagination has completed
func (UserPagination) Done(err error) bool {
	return err == errPaginationComplete
}

// Failure checks if pagination failed.
func (t UserPagination) Failure(err error) error {
	if t.Done(err) {
		return nil
	}

	return err
}

func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) {
	var (
		resp *userResponseFull
	)

	if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
		return t, errPaginationComplete
	}

	t.previousResp = t.previousResp.initialize()

	values := url.Values{
		"limit":          {strconv.Itoa(t.limit)},
		"presence":       {strconv.FormatBool(t.presence)},
		"token":          {t.c.token},
		"cursor":         {t.previousResp.Cursor},
		"team_id":        {t.teamId},
		"include_locale": {strconv.FormatBool(true)},
	}

	if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
		return t, err
	}

	t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata)
	t.Users = resp.Members
	t.previousResp = &resp.Metadata

	return t, nil
}

// GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage.
func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination {
	return newUserPagination(api, options...)
}

// GetUsers returns the list of users (with their detailed information)
func (api *Client) GetUsers(options ...GetUsersOption) ([]User, error) {
	return api.GetUsersContext(context.Background(), options...)
}

// GetUsersContext returns the list of users (with their detailed information) with a custom context
func (api *Client) GetUsersContext(ctx context.Context, options ...GetUsersOption) (results []User, err error) {
	p := api.GetUsersPaginated(options...)
	for err == nil {
		p, err = p.Next(ctx)
		if err == nil {
			results = append(results, p.Users...)
		} else if rateLimitedError, ok := err.(*RateLimitedError); ok {
			select {
			case <-ctx.Done():
				err = ctx.Err()
			case <-time.After(rateLimitedError.RetryAfter):
				err = nil
			}
		}
	}

	return results, p.Failure(err)
}

// GetUserByEmail will retrieve the complete user information by email
func (api *Client) GetUserByEmail(email string) (*User, error) {
	return api.GetUserByEmailContext(context.Background(), email)
}

// GetUserByEmailContext will retrieve the complete user information by email with a custom context
func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
	values := url.Values{
		"token": {api.token},
		"email": {email},
	}
	response, err := api.userRequest(ctx, "users.lookupByEmail", values)
	if err != nil {
		return nil, err
	}
	return &response.User, nil
}

// SetUserAsActive marks the currently authenticated user as active
func (api *Client) SetUserAsActive() error {
	return api.SetUserAsActiveContext(context.Background())
}

// SetUserAsActiveContext marks the currently authenticated user as active with a custom context
func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
	values := url.Values{
		"token": {api.token},
	}

	_, err = api.userRequest(ctx, "users.setActive", values)
	return err
}

// SetUserPresence changes the currently authenticated user presence
func (api *Client) SetUserPresence(presence string) error {
	return api.SetUserPresenceContext(context.Background(), presence)
}

// SetUserPresenceContext changes the currently authenticated user presence with a custom context
func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error {
	values := url.Values{
		"token":    {api.token},
		"presence": {presence},
	}

	_, err := api.userRequest(ctx, "users.setPresence", values)
	return err
}

// GetUserIdentity will retrieve user info available per identity scopes
func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
	return api.GetUserIdentityContext(context.Background())
}

// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) {
	values := url.Values{
		"token": {api.token},
	}
	response = &UserIdentityResponse{}

	err = api.postMethod(ctx, "users.identity", values, response)
	if err != nil {
		return nil, err
	}

	if err := response.Err(); err != nil {
		return nil, err
	}

	return response, nil
}

// SetUserPhoto changes the currently authenticated user's profile image
func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
	return api.SetUserPhotoContext(context.Background(), image, params)
}

// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) {
	response := &SlackResponse{}
	values := url.Values{}
	if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
		values.Add("crop_x", strconv.Itoa(params.CropX))
	}
	if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
		values.Add("crop_y", strconv.Itoa(params.CropX))
	}
	if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
		values.Add("crop_w", strconv.Itoa(params.CropW))
	}

	err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", api.token, values, response, api)
	if err != nil {
		return err
	}

	return response.Err()
}

// DeleteUserPhoto deletes the current authenticated user's profile image
func (api *Client) DeleteUserPhoto() error {
	return api.DeleteUserPhotoContext(context.Background())
}

// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
	response := &SlackResponse{}
	values := url.Values{
		"token": {api.token},
	}

	err = api.postMethod(ctx, "users.deletePhoto", values, response)
	if err != nil {
		return err
	}

	return response.Err()
}

// SetUserRealName changes the currently authenticated user's realName
//
// For more information see SetUserRealNameContextWithUser
func (api *Client) SetUserRealName(realName string) error {
	return api.SetUserRealNameContextWithUser(context.Background(), "", realName)
}

// SetUserRealNameContextWithUser will set a real name for the provided user with a custom context
func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, realName string) error {
	profile, err := json.Marshal(
		&struct {
			RealName string `json:"real_name"`
		}{
			RealName: realName,
		},
	)

	if err != nil {
		return err
	}

	values := url.Values{
		"token":   {api.token},
		"profile": {string(profile)},
	}

	// optional field. It should not be set if empty
	if user != "" {
		values["user"] = []string{user}
	}

	response := &userResponseFull{}
	if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
		return err
	}

	return response.Err()
}

// SetUserCustomStatus will set a custom status and emoji for the currently
// authenticated user. If statusEmoji is "" and statusText is not, the Slack API
// will automatically set it to ":speech_balloon:". Otherwise, if both are ""
// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0
// the status will not expire.
func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
	return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
}

// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error {
	return api.SetUserCustomStatusContextWithUser(ctx, "", statusText, statusEmoji, statusExpiration)
}

// SetUserCustomStatusWithUser will set a custom status and emoji for the provided user.
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error {
	return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration)
}

// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context
//
// For more information see SetUserCustomStatus
func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
	// XXX(theckman): this anonymous struct is for making requests to the Slack
	// API for setting and unsetting a User's Custom Status/Emoji. To change
	// these values we must provide a JSON document as the profile POST field.
	//
	// We use an anonymous struct over UserProfile because to unset the values
	// on the User's profile we cannot use the `json:"omitempty"` tag. This is
	// because an empty string ("") is what's used to unset the values. Check
	// out the API docs for more details:
	//
	// - https://api.slack.com/docs/presence-and-status#custom_status
	profile, err := json.Marshal(
		&struct {
			StatusText       string `json:"status_text"`
			StatusEmoji      string `json:"status_emoji"`
			StatusExpiration int64  `json:"status_expiration"`
		}{
			StatusText:       statusText,
			StatusEmoji:      statusEmoji,
			StatusExpiration: statusExpiration,
		},
	)

	if err != nil {
		return err
	}

	values := url.Values{
		"token":   {api.token},
		"profile": {string(profile)},
	}

	// optional field. It should not be set if empty
	if user != "" {
		values["user"] = []string{user}
	}

	response := &userResponseFull{}
	if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
		return err
	}

	return response.Err()
}

// UnsetUserCustomStatus removes the custom status message for the currently
// authenticated user. This is a convenience method that wraps (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatus() error {
	return api.UnsetUserCustomStatusContext(context.Background())
}

// UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user
// with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus().
func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
	return api.SetUserCustomStatusContext(ctx, "", "", 0)
}

// GetUserProfileParameters are the parameters required to get user profile
type GetUserProfileParameters struct {
	UserID        string
	IncludeLabels bool
}

// GetUserProfile retrieves a user's profile information.
func (api *Client) GetUserProfile(params *GetUserProfileParameters) (*UserProfile, error) {
	return api.GetUserProfileContext(context.Background(), params)
}

type getUserProfileResponse struct {
	SlackResponse
	Profile *UserProfile `json:"profile"`
}

// GetUserProfileContext retrieves a user's profile information with a context.
func (api *Client) GetUserProfileContext(ctx context.Context, params *GetUserProfileParameters) (*UserProfile, error) {
	values := url.Values{"token": {api.token}}

	if params.UserID != "" {
		values.Add("user", params.UserID)
	}
	if params.IncludeLabels {
		values.Add("include_labels", "true")
	}
	resp := &getUserProfileResponse{}

	err := api.postMethod(ctx, "users.profile.get", values, &resp)
	if err != nil {
		return nil, err
	}

	if err := resp.Err(); err != nil {
		return nil, err
	}

	return resp.Profile, nil
}
