package okta

import (
	"errors"
	"fmt"
	"net/url"
	"time"
)

const (
	profileEmailFilter       = "profile.email"
	profileLoginFilter       = "profile.login"
	profileStatusFilter      = "status"
	profileIDFilter          = "id"
	profileFirstNameFilter   = "profile.firstName"
	profileLastNameFilter    = "profile.lastName"
	profileLastUpdatedFilter = "lastUpdated"
	// UserStatusActive is a  constant to represent OKTA User State returned by the API
	UserStatusActive = "ACTIVE"
	// UserStatusStaged is a  constant to represent OKTA User State returned by the API
	UserStatusStaged = "STAGED"
	// UserStatusProvisioned is a  constant to represent OKTA User State returned by the API
	UserStatusProvisioned = "PROVISIONED"
	// UserStatusRecovery is a  constant to represent OKTA User State returned by the API
	UserStatusRecovery = "RECOVERY"
	// UserStatusLockedOut is a  constant to represent OKTA User State returned by the API
	UserStatusLockedOut = "LOCKED_OUT"
	// UserStatusPasswordExpired is a  constant to represent OKTA User State returned by the API
	UserStatusPasswordExpired = "PASSWORD_EXPIRED"
	// UserStatusSuspended is a  constant to represent OKTA User State returned by the API
	UserStatusSuspended = "SUSPENDED"
	// UserStatusDeprovisioned is a  constant to represent OKTA User State returned by the API
	UserStatusDeprovisioned = "DEPROVISIONED"

	oktaFilterTimeFormat = "2006-01-02T15:05:05.000Z"
)

// UsersService handles communication with the User data related
// methods of the OKTA API.
type UsersService service

// ActivationResponse - Response coming back from a user activation
type ActivationResponse struct {
	ActivationURL string `json:"activationUrl"`
}

type provider struct {
	Name string `json:"name,omitempty"`
	Type string `json:"type,omitempty"`
}

type recoveryQuestion struct {
	Question string `json:"question,omitempty"`
	Answer   string `json:"answer,omitempty"`
}

type passwordValue struct {
	Value string `json:"value,omitempty"`
}
type credentials struct {
	Password         *passwordValue    `json:"password,omitempty"`
	Provider         *provider         `json:"provider,omitempty"`
	RecoveryQuestion *recoveryQuestion `json:"recovery_question,omitempty"`
}

type userProfile struct {
	Email       string `json:"email"`
	FirstName   string `json:"firstName"`
	LastName    string `json:"lastName"`
	Login       string `json:"login"`
	MobilePhone string `json:"mobilePhone,omitempty"`
	SecondEmail string `json:"secondEmail,omitempty"`
	PsEmplid    string `json:"psEmplid,omitempty"`
	NickName    string `json:"nickname,omitempty"`
	DisplayName string `json:"displayName,omitempty"`

	ProfileURL        string `json:"profileUrl,omitempty"`
	PreferredLanguage string `json:"preferredLanguage,omitempty"`
	UserType          string `json:"userType,omitempty"`
	Organization      string `json:"organization,omitempty"`
	Title             string `json:"title,omitempty"`
	Division          string `json:"division,omitempty"`
	Department        string `json:"department,omitempty"`
	CostCenter        string `json:"costCenter,omitempty"`
	EmployeeNumber    string `json:"employeeNumber,omitempty"`
	PrimaryPhone      string `json:"primaryPhone,omitempty"`
	StreetAddress     string `json:"streetAddress,omitempty"`
	City              string `json:"city,omitempty"`
	State             string `json:"state,omitempty"`
	ZipCode           string `json:"zipCode,omitempty"`
	CountryCode       string `json:"countryCode,omitempty"`
}

type userLinks struct {
	ChangePassword struct {
		Href string `json:"href"`
	} `json:"changePassword"`
	ChangeRecoveryQuestion struct {
		Href string `json:"href"`
	} `json:"changeRecoveryQuestion"`
	Deactivate struct {
		Href string `json:"href"`
	} `json:"deactivate"`
	ExpirePassword struct {
		Href string `json:"href"`
	} `json:"expirePassword"`
	ForgotPassword struct {
		Href string `json:"href"`
	} `json:"forgotPassword"`
	ResetFactors struct {
		Href string `json:"href"`
	} `json:"resetFactors"`
	ResetPassword struct {
		Href string `json:"href"`
	} `json:"resetPassword"`
}

// User is a struct that represents a user object from OKTA.
type User struct {
	Activated       string          `json:"activated,omitempty"`
	Created         string          `json:"created,omitempty"`
	Credentials     credentials     `json:"credentials,omitempty"`
	ID              string          `json:"id,omitempty"`
	LastLogin       string          `json:"lastLogin,omitempty"`
	LastUpdated     string          `json:"lastUpdated,omitempty"`
	PasswordChanged string          `json:"passwordChanged,omitempty"`
	Profile         userProfile     `json:"profile"`
	Status          string          `json:"status,omitempty"`
	StatusChanged   string          `json:"statusChanged,omitempty"`
	Links           userLinks       `json:"_links,omitempty"`
	MFAFactors      []userMFAFactor `json:"-,"`
	Groups          []Group         `json:"-"`
}

type userMFAFactor struct {
	ID          string    `json:"id,omitempty"`
	FactorType  string    `json:"factorType,omitempty"`
	Provider    string    `json:"provider,omitempty"`
	VendorName  string    `json:"vendorName,omitempty"`
	Status      string    `json:"status,omitempty"`
	Created     time.Time `json:"created,omitempty"`
	LastUpdated time.Time `json:"lastUpdated,omitempty"`
	Profile     struct {
		CredentialID string `json:"credentialId,omitempty"`
	} `json:"profile,omitempty"`
}

// NewUser object to create user objects in OKTA
type NewUser struct {
	Profile     userProfile  `json:"profile"`
	Credentials *credentials `json:"credentials,omitempty"`
}

type newPasswordSet struct {
	Credentials credentials `json:"credentials"`
}

// ResetPasswordResponse struct that returns data about the password reset
type ResetPasswordResponse struct {
	ResetPasswordURL string `json:"resetPasswordUrl"`
}

// NewUser - Returns a new user object. This is used to create users in OKTA. It only has the properties that
// OKTA will take as input. The "User" object has more feilds that are OKTA returned like the ID, etc
func (s *UsersService) NewUser() NewUser {
	return NewUser{}
}

// SetPassword Adds a specified password to the new User
func (u *NewUser) SetPassword(passwordIn string) {

	if passwordIn != "" {

		pass := new(passwordValue)
		pass.Value = passwordIn

		var cred *credentials
		if u.Credentials == nil {
			cred = new(credentials)
		} else {
			cred = u.Credentials
		}

		cred.Password = pass
		u.Credentials = cred

	}
}

// SetRecoveryQuestion - Sets a custom security question and answer on a user object
func (u *NewUser) SetRecoveryQuestion(questionIn string, answerIn string) {

	if questionIn != "" && answerIn != "" {
		recovery := new(recoveryQuestion)

		recovery.Question = questionIn
		recovery.Answer = answerIn

		var cred *credentials
		if u.Credentials == nil {
			cred = new(credentials)
		} else {
			cred = u.Credentials
		}
		cred.RecoveryQuestion = recovery
		u.Credentials = cred

	}
}

func (u User) String() string {
	return stringify(u)
	// return fmt.Sprintf("ID: %v \tLogin: %v", u.ID, u.Profile.Login)
}

// GetByID returns a user object for a specific OKTA ID.
// Generally the id input string is the cryptic OKTA key value from User.ID. However, the OKTA API may accept other values like "me", or login shortname
func (s *UsersService) GetByID(id string) (*User, *Response, error) {
	u := fmt.Sprintf("users/%v", id)
	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}

	user := new(User)
	resp, err := s.client.Do(req, user)
	if err != nil {
		return nil, resp, err
	}

	return user, resp, err
}

// UserListFilterOptions is a struct that you can populate which will "filter" user searches
// the exported struct fields should allow you to do different filters based on what is allowed in the OKTA API.
//  The filter OKTA API is limited in the fields it can search
//  NOTE: In the current form you can't add parenthesis and ordering
// OKTA API Supports only a limited number of properties:
// status, lastUpdated, id, profile.login, profile.email, profile.firstName, and profile.lastName.
// http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter
type UserListFilterOptions struct {
	Limit         int    `url:"limit,omitempty"`
	EmailEqualTo  string `url:"-"`
	LoginEqualTo  string `url:"-"`
	StatusEqualTo string `url:"-"`
	IDEqualTo     string `url:"-"`

	FirstNameEqualTo string `url:"-"`
	LastNameEqualTo  string `url:"-"`
	//  API documenation says you can search with "starts with" but these don't work

	// FirstNameStartsWith    string    `url:"-"`
	// LastNameStartsWith     string    `url:"-"`

	// This will be built by internal - may not need to export
	FilterString  string     `url:"filter,omitempty"`
	NextURL       *url.URL   `url:"-"`
	GetAllPages   bool       `url:"-"`
	NumberOfPages int        `url:"-"`
	LastUpdated   dateFilter `url:"-"`
}

// PopulateGroups will populate the groups a user is a member of. You pass in a pointer to an existing users
func (s *UsersService) PopulateGroups(user *User) (*Response, error) {
	u := fmt.Sprintf("users/%v/groups", user.ID)
	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, err
	}
	// Get first page of users.
	resp, err := s.client.Do(req, &user.Groups)
	if err != nil {
		return resp, err
	}
	// Look for any remaining user group pages.
	var nextURL string
	if resp.NextURL != nil {
		nextURL = resp.NextURL.String()
	}
	for {

		if nextURL != "" {
			req, err := s.client.NewRequest("GET", nextURL, nil)
			userGroupsPages := []Group{}

			resp, err := s.client.Do(req, &userGroupsPages)
			nextURL = ""
			if err != nil {
				return resp, err
			}
			user.Groups = append(user.Groups, userGroupsPages...)
			if resp.NextURL != nil {
				nextURL = resp.NextURL.String()
			}

		} else {
			return resp, err
		}

	}

}

// PopulateEnrolledFactors will populate the Enrolled MFA Factors a user is a member of.
// You pass in a pointer to an existing users
// http://developer.okta.com/docs/api/resources/factors.html#list-enrolled-factors
func (s *UsersService) PopulateEnrolledFactors(user *User) (*Response, error) {
	u := fmt.Sprintf("users/%v/factors", user.ID)
	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, err
	}
	// TODO: If user has more than 200 groups this will only return those first 200
	resp, err := s.client.Do(req, &user.MFAFactors)
	if err != nil {
		return resp, err
	}

	return resp, err
}

// List users with status of LOCKED_OUT
// filter=status eq "LOCKED_OUT"
// List users updated after 06/01/2013 but before 01/01/2014
// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z"
// List users updated after 06/01/2013 but before 01/01/2014 with a status of ACTIVE
// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and lastUpdated lt "2014-01-01T00:00:00.000Z" and status eq "ACTIVE"
// TODO - Currently no way to do parenthesis
// List users updated after 06/01/2013 but with a status of LOCKED_OUT or RECOVERY
// filter=lastUpdated gt "2013-06-01T00:00:00.000Z" and (status eq "LOCKED_OUT" or status eq "RECOVERY")

// OTKA API docs: http://developer.okta.com/docs/api/resources/users.html#list-users-with-a-filter

func appendToFilterString(currFilterString string, appendFilterKey string, appendFilterOperator string, appendFilterValue string) (rs string) {
	if currFilterString != "" {
		rs = fmt.Sprintf("%v and %v %v \"%v\"", currFilterString, appendFilterKey, appendFilterOperator, appendFilterValue)
	} else {
		rs = fmt.Sprintf("%v %v \"%v\"", appendFilterKey, appendFilterOperator, appendFilterValue)
	}

	return rs
}

// ListWithFilter will use the input UserListFilterOptions to find users and return a paged result set
func (s *UsersService) ListWithFilter(opt *UserListFilterOptions) ([]User, *Response, error) {
	var u string
	var err error

	pagesRetreived := 0

	if opt.NextURL != nil {
		u = opt.NextURL.String()
	} else {
		if opt.EmailEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileEmailFilter, FilterEqualOperator, opt.EmailEqualTo)
		}
		if opt.LoginEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileLoginFilter, FilterEqualOperator, opt.LoginEqualTo)
		}

		if opt.StatusEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileStatusFilter, FilterEqualOperator, opt.StatusEqualTo)
		}

		if opt.IDEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileIDFilter, FilterEqualOperator, opt.IDEqualTo)
		}

		if opt.FirstNameEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, FilterEqualOperator, opt.FirstNameEqualTo)
		}

		if opt.LastNameEqualTo != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, FilterEqualOperator, opt.LastNameEqualTo)
		}

		//  API documenation says you can search with "starts with" but these don't work
		// if opt.FirstNameStartsWith != "" {
		// 	opt.FilterString = appendToFilterString(opt.FilterString, profileFirstNameFilter, filterStartsWithOperator, opt.FirstNameStartsWith)
		// }

		// if opt.LastNameStartsWith != "" {
		// 	opt.FilterString = appendToFilterString(opt.FilterString, profileLastNameFilter, filterStartsWithOperator, opt.LastNameStartsWith)
		// }

		if !opt.LastUpdated.Value.IsZero() {
			opt.FilterString = appendToFilterString(opt.FilterString, profileLastUpdatedFilter, opt.LastUpdated.Operator, opt.LastUpdated.Value.UTC().Format(oktaFilterTimeFormat))
		}

		if opt.Limit == 0 {
			opt.Limit = defaultLimit
		}

		u, err = addOptions("users", opt)

	}

	if err != nil {
		return nil, nil, err
	}

	req, err := s.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}
	users := make([]User, 1)
	resp, err := s.client.Do(req, &users)
	if err != nil {
		return nil, resp, err
	}

	pagesRetreived++

	if (opt.NumberOfPages > 0 && pagesRetreived < opt.NumberOfPages) || opt.GetAllPages {

		for {

			if pagesRetreived == opt.NumberOfPages {
				break
			}
			if resp.NextURL != nil {
				var userPage []User
				pageOption := new(UserListFilterOptions)
				pageOption.NextURL = resp.NextURL
				pageOption.NumberOfPages = 1
				pageOption.Limit = opt.Limit

				userPage, resp, err = s.ListWithFilter(pageOption)
				if err != nil {
					return users, resp, err
				}
				users = append(users, userPage...)
				pagesRetreived++
			} else {
				break
			}
		}
	}
	return users, resp, err
}

// Create - Creates a new user. You must pass in a "newUser" object created from Users.NewUser()
// There are many differnt reasons that OKTA may reject the request so you have to check the error messages
func (s *UsersService) Create(userIn NewUser, createAsActive bool) (*User, *Response, error) {

	u := fmt.Sprintf("users?activate=%v", createAsActive)

	req, err := s.client.NewRequest("POST", u, userIn)

	if err != nil {
		return nil, nil, err
	}

	newUser := new(User)
	resp, err := s.client.Do(req, newUser)
	if err != nil {
		return nil, resp, err
	}

	return newUser, resp, err
}

// Activate Activates a user. You can have OKTA send an email by including a "sendEmail=true"
// If you pass in sendEmail=false, then activationResponse.ActivationURL will have a string URL that
// can be sent to the end user. You can discard response if sendEmail=true
func (s *UsersService) Activate(id string, sendEmail bool) (*ActivationResponse, *Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/activate?sendEmail=%v", id, sendEmail)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, nil, err
	}

	activationInfo := new(ActivationResponse)
	resp, err := s.client.Do(req, activationInfo)

	if err != nil {
		return nil, resp, err
	}

	return activationInfo, resp, err
}

// Deactivate - Deactivates a user
func (s *UsersService) Deactivate(id string) (*Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/deactivate", id)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// Suspend - Suspends a user - If user is NOT active an Error will come back based on OKTA API:
// http://developer.okta.com/docs/api/resources/users.html#suspend-user
func (s *UsersService) Suspend(id string) (*Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/suspend", id)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// Unsuspend - Unsuspends a user - If user is NOT SUSPENDED, an Error will come back based on OKTA API:
// http://developer.okta.com/docs/api/resources/users.html#unsuspend-user
func (s *UsersService) Unsuspend(id string) (*Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/unsuspend", id)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// Unlock - Unlocks a user - Per docs, only for OKTA Mastered Account
// http://developer.okta.com/docs/api/resources/users.html#unlock-user
func (s *UsersService) Unlock(id string) (*Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/unlock", id)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// SetPassword - Sets a user password to an Admin provided String
func (s *UsersService) SetPassword(id string, newPassword string) (*User, *Response, error) {

	if id == "" || newPassword == "" {
		return nil, nil, errors.New("please provide a User ID and Password")
	}

	passwordUpdate := new(newPasswordSet)

	pass := new(passwordValue)
	pass.Value = newPassword

	passwordUpdate.Credentials.Password = pass

	u := fmt.Sprintf("users/%v", id)
	req, err := s.client.NewRequest("POST", u, passwordUpdate)
	if err != nil {
		return nil, nil, err
	}

	user := new(User)
	resp, err := s.client.Do(req, user)
	if err != nil {
		return nil, resp, err
	}

	return user, resp, err
}

// ResetPassword - Generates a one-time token (OTT) that can be used to reset a userâ€™s password.
// The OTT link can be automatically emailed to the user or returned to the API caller and distributed using a custom flow.
// http://developer.okta.com/docs/api/resources/users.html#reset-password
// If you pass in sendEmail=false, then resetPasswordResponse.resetPasswordUrl will have a string URL that
// can be sent to the end user. You can discard response if sendEmail=true
func (s *UsersService) ResetPassword(id string, sendEmail bool) (*ResetPasswordResponse, *Response, error) {
	u := fmt.Sprintf("users/%v/lifecycle/reset_password?sendEmail=%v", id, sendEmail)

	req, err := s.client.NewRequest("POST", u, nil)
	if err != nil {
		return nil, nil, err
	}

	resetInfo := new(ResetPasswordResponse)
	resp, err := s.client.Do(req, resetInfo)

	if err != nil {
		return nil, resp, err
	}

	return resetInfo, resp, err
}

// PopulateMFAFactors will populate the MFA Factors a user is a member of. You pass in a pointer to an existing users
func (s *UsersService) PopulateMFAFactors(user *User) (*Response, error) {
	u := fmt.Sprintf("users/%v/factors", user.ID)

	req, err := s.client.NewRequest("GET", u, nil)

	if err != nil {
		return nil, err
	}

	resp, err := s.client.Do(req, &user.MFAFactors)
	if err != nil {
		return resp, err
	}

	return resp, err
}
