package okta

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

const (
	// GroupTypeOKTA - group type constant for an OKTA Mastered Group
	GroupTypeOKTA = "OKTA_GROUP"
	// GroupTypeBuiltIn - group type constant for a Built in OKTA groups
	GroupTypeBuiltIn = "BUILT_IN"
	// GroupTypeApp -- group type constant for app mastered group
	GroupTypeApp = "APP_GROUP"

	groupTypeFilter                  = "type"
	groupNameFilter                  = "q"
	groupLastMembershipUpdatedFilter = "lastMembershipUpdated"
	groupLastUpdatedFilter           = "lastUpdated"
)

// GroupsService handles communication with the Groups data related
// methods of the OKTA API.
type GroupsService service

// Group represents the Group Object from the OKTA API
type Group struct {
	ID                    string    `json:"id"`
	Created               time.Time `json:"created"`
	LastUpdated           time.Time `json:"lastUpdated"`
	LastMembershipUpdated time.Time `json:"lastMembershipUpdated"`
	ObjectClass           []string  `json:"objectClass"`
	Type                  string    `json:"type"`
	Profile               struct {
		Name                       string `json:"name"`
		Description                string `json:"description"`
		SamAccountName             string `json:"samAccountName"`
		Dn                         string `json:"dn"`
		WindowsDomainQualifiedName string `json:"windowsDomainQualifiedName"`
		ExternalID                 string `json:"externalId"`
	} `json:"profile"`
	Links struct {
		Logo []struct {
			Name string `json:"name"`
			Href string `json:"href"`
			Type string `json:"type"`
		} `json:"logo"`
		Users struct {
			Href string `json:"href"`
		} `json:"users"`
		Apps struct {
			Href string `json:"href"`
		} `json:"apps"`
	} `json:"_links"`
}

// GroupFilterOptions is used to generate a "Filter" to search for different groups
// The values here coorelate to API Search paramgters on the group API
type GroupFilterOptions struct {
	// 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:"-"`
	Limit         int      `url:"limit,omitempty"`

	NameStartsWith string `url:"q,omitempty"`
	GroupTypeEqual string `url:"-"`

	LastUpdated           dateFilter `url:"-"`
	LastMembershipUpdated dateFilter `url:"-"`
}

func (g Group) String() string {
	// return Stringify(g)
	return fmt.Sprintf("Group:(ID: {%v} - Type: {%v} - Group Name: {%v})\n", g.ID, g.Type, g.Profile.Name)
}

// ListWithFilter - Method to list groups with different filter options.
//  Pass in a GroupFilterOptions to specify filters. Values in that struct will turn into Query parameters
func (g *GroupsService) ListWithFilter(opt *GroupFilterOptions) ([]Group, *Response, error) {

	var u string
	var err error

	pagesRetreived := 0
	if opt.NextURL != nil {
		u = opt.NextURL.String()
	} else {
		if opt.GroupTypeEqual != "" {
			opt.FilterString = appendToFilterString(opt.FilterString, groupTypeFilter, FilterEqualOperator, opt.GroupTypeEqual)
		}

		// if opt.NameStartsWith != "" {
		// 	opt.FilterString = appendToFilterString(opt.FilterString, groupNameFilter, filterEqualOperator, opt.NameStartsWith)
		// }
		if (!opt.LastMembershipUpdated.Value.IsZero()) && (opt.LastMembershipUpdated.Operator != "") {
			opt.FilterString = appendToFilterString(opt.FilterString, groupLastMembershipUpdatedFilter, opt.LastMembershipUpdated.Operator, opt.LastMembershipUpdated.Value.UTC().Format(oktaFilterTimeFormat))
		}

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

		if opt.Limit == 0 {
			opt.Limit = defaultLimit
		}
		u, err = addOptions("groups", opt)
		if err != nil {
			return nil, nil, err
		}
	}

	req, err := g.client.NewRequest("GET", u, nil)
	if err != nil {
		return nil, nil, err
	}
	groups := make([]Group, 1)
	resp, err := g.client.Do(req, &groups)
	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 groupPage []Group
				pageOption := new(GroupFilterOptions)
				pageOption.NextURL = resp.NextURL
				pageOption.NumberOfPages = 1
				pageOption.Limit = opt.Limit

				groupPage, resp, err = g.ListWithFilter(pageOption)
				if err != nil {
					return groups, resp, err
				}
				groups = append(groups, groupPage...)
				pagesRetreived++

			} else {
				break
			}
		}
	}
	return groups, resp, err
}

// GetByID gets a group from OKTA by the Gropu ID. An error is returned if the group is not found
func (g *GroupsService) GetByID(groupID string) (*Group, *Response, error) {

	u := fmt.Sprintf("groups/%v", groupID)
	req, err := g.client.NewRequest("GET", u, nil)

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

	group := new(Group)

	resp, err := g.client.Do(req, group)

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

	return group, resp, err
}

// GetUsers returns the members in a group
//   Pass in an optional GroupFilterOptions struct to filter the results
//   The Users in the group are returned
func (g *GroupsService) GetUsers(groupID string, opt *GroupUserFilterOptions) (users []User, resp *Response, err error) {
	pagesRetreived := 0
	var u string
	if opt.NextURL != nil {
		u = opt.NextURL.String()
	} else {
		u = fmt.Sprintf("groups/%v/users", groupID)

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

		u, _ = addOptions(u, opt)
	}

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

	if err != nil {
		return nil, nil, err
	}
	resp, err = g.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
				pageOpts := new(GroupUserFilterOptions)
				pageOpts.NextURL = resp.NextURL
				pageOpts.Limit = opt.Limit
				pageOpts.NumberOfPages = 1

				userPage, resp, err = g.GetUsers(groupID, pageOpts)
				if err != nil {
					return users, resp, err
				}
				users = append(users, userPage...)
				pagesRetreived++
			} else {
				break
			}

		}
	}

	return users, resp, err
}

// Add - Adds an OKTA Mastered Group with name and description. GroupName is required.
func (g *GroupsService) Add(groupName string, groupDescription string) (*Group, *Response, error) {

	if groupName == "" {
		return nil, nil, errors.New("groupName parameter is required for ADD")
	}

	newGroup := newGroup{}
	newGroup.Profile.Name = groupName
	newGroup.Profile.Description = groupDescription

	u := fmt.Sprintf("groups")

	req, err := g.client.NewRequest("POST", u, newGroup)

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

	group := new(Group)

	resp, err := g.client.Do(req, group)

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

	return group, resp, err
}

// Delete - Deletes an OKTA Mastered Group with ID
func (g *GroupsService) Delete(groupID string) (*Response, error) {

	if groupID == "" {
		return nil, errors.New("groupID parameter is required for Delete")
	}
	u := fmt.Sprintf("groups/%v", groupID)

	req, err := g.client.NewRequest("DELETE", u, nil)

	if err != nil {
		return nil, err
	}

	resp, err := g.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// AddUserToGroup - Adds a user to a group.
func (g *GroupsService) AddUserToGroup(groupID string, userID string) (*Response, error) {

	if groupID == "" {
		return nil, errors.New("groupID parameter is required for Delete")
	}
	if userID == "" {
		return nil, errors.New("groupID parameter is required for Delete")
	}

	u := fmt.Sprintf("groups/%v/users/%v", groupID, userID)

	req, err := g.client.NewRequest("PUT", u, nil)

	if err != nil {
		return nil, err
	}

	resp, err := g.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// RemoveUserFromGroup - Removes a user to a group.
func (g *GroupsService) RemoveUserFromGroup(groupID string, userID string) (*Response, error) {

	if groupID == "" {
		return nil, errors.New("groupID parameter is required for Delete")
	}
	if userID == "" {
		return nil, errors.New("groupID parameter is required for Delete")
	}

	u := fmt.Sprintf("groups/%v/users/%v", groupID, userID)

	req, err := g.client.NewRequest("DELETE", u, nil)

	if err != nil {
		return nil, err
	}

	resp, err := g.client.Do(req, nil)

	if err != nil {
		return resp, err
	}

	return resp, err
}

// GroupUserFilterOptions is a struct that you populate which will limit or control group fetches and searches
//  The values here will coorelate to the search filtering allowed in the OKTA API. These values are turned into Query Parameters
type GroupUserFilterOptions struct {
	Limit         int      `url:"limit,omitempty"`
	NextURL       *url.URL `url:"-"`
	GetAllPages   bool     `url:"-"`
	NumberOfPages int      `url:"-"`
}

type newGroup struct {
	Profile struct {
		Name        string `json:"name"`
		Description string `json:"description"`
	} `json:"profile"`
}
