package cfclient

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"

	"github.com/pkg/errors"
)

type UserRequest struct {
	Guid             string `json:"guid"`
	DefaultSpaceGuid string `json:"default_space_guid,omitempty"`
}

type Users []User

type User struct {
	Guid                  string `json:"guid"`
	CreatedAt             string `json:"created_at"`
	UpdatedAt             string `json:"updated_at"`
	Admin                 bool   `json:"admin"`
	Active                bool   `json:"active"`
	DefaultSpaceGUID      string `json:"default_space_guid"`
	Username              string `json:"username"`
	SpacesURL             string `json:"spaces_url"`
	OrgsURL               string `json:"organizations_url"`
	ManagedOrgsURL        string `json:"managed_organizations_url"`
	BillingManagedOrgsURL string `json:"billing_managed_organizations_url"`
	AuditedOrgsURL        string `json:"audited_organizations_url"`
	ManagedSpacesURL      string `json:"managed_spaces_url"`
	AuditedSpacesURL      string `json:"audited_spaces_url"`
	c                     *Client
}

type UserResource struct {
	Meta   Meta `json:"metadata"`
	Entity User `json:"entity"`
}

type UserResponse struct {
	Count     int            `json:"total_results"`
	Pages     int            `json:"total_pages"`
	NextUrl   string         `json:"next_url"`
	Resources []UserResource `json:"resources"`
}

// GetUserByGUID retrieves the user with the provided guid.
func (c *Client) GetUserByGUID(guid string) (User, error) {
	var userRes UserResource
	r := c.NewRequest("GET", "/v2/users/"+guid)
	resp, err := c.DoRequest(r)
	if err != nil {
		return User{}, err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return User{}, err
	}
	err = json.Unmarshal(body, &userRes)
	if err != nil {
		return User{}, err
	}
	return c.mergeUserResource(userRes), nil
}

func (c *Client) ListUsersByQuery(query url.Values) (Users, error) {
	var users []User
	requestUrl := "/v2/users?" + query.Encode()
	for {
		userResp, err := c.getUserResponse(requestUrl)
		if err != nil {
			return []User{}, err
		}
		for _, user := range userResp.Resources {
			user.Entity.Guid = user.Meta.Guid
			user.Entity.CreatedAt = user.Meta.CreatedAt
			user.Entity.UpdatedAt = user.Meta.UpdatedAt
			user.Entity.c = c
			users = append(users, user.Entity)
		}
		requestUrl = userResp.NextUrl
		if requestUrl == "" || query.Get("page") != "" {
			break
		}
	}
	return users, nil
}

func (c *Client) ListUsers() (Users, error) {
	return c.ListUsersByQuery(nil)
}

func (c *Client) ListUserSpaces(userGuid string) ([]Space, error) {
	return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/spaces", userGuid), url.Values{})
}

func (c *Client) ListUserAuditedSpaces(userGuid string) ([]Space, error) {
	return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/audited_spaces", userGuid), url.Values{})
}

func (c *Client) ListUserManagedSpaces(userGuid string) ([]Space, error) {
	return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/managed_spaces", userGuid), url.Values{})
}

func (c *Client) ListUserOrgs(userGuid string) ([]Org, error) {
	return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/organizations", userGuid))
}

func (c *Client) ListUserManagedOrgs(userGuid string) ([]Org, error) {
	return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/managed_organizations", userGuid))
}

func (c *Client) ListUserAuditedOrgs(userGuid string) ([]Org, error) {
	return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/audited_organizations", userGuid))
}

func (c *Client) ListUserBillingManagedOrgs(userGuid string) ([]Org, error) {
	return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/billing_managed_organizations", userGuid))
}

func (c *Client) CreateUser(req UserRequest) (User, error) {
	buf := bytes.NewBuffer(nil)
	err := json.NewEncoder(buf).Encode(req)
	if err != nil {
		return User{}, err
	}
	r := c.NewRequestWithBody("POST", "/v2/users", buf)
	resp, err := c.DoRequest(r)
	if err != nil {
		return User{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusCreated {
		return User{}, errors.Wrapf(err, "Error creating user, response code: %d", resp.StatusCode)
	}
	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return User{}, err
	}
	var userResource UserResource
	err = json.Unmarshal(body, &userResource)
	if err != nil {
		return User{}, err
	}
	user := userResource.Entity
	user.Guid = userResource.Meta.Guid
	user.c = c
	return user, nil
}

func (c *Client) DeleteUser(userGuid string) error {
	resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/users/%s", userGuid)))
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusNoContent {
		return errors.Wrapf(err, "Error deleting user %s, response code: %d", userGuid, resp.StatusCode)
	}
	return nil
}

func (u Users) GetUserByUsername(username string) User {
	for _, user := range u {
		if user.Username == username {
			return user
		}
	}
	return User{}
}

func (c *Client) getUserResponse(requestUrl string) (UserResponse, error) {
	var userResp UserResponse
	r := c.NewRequest("GET", requestUrl)
	resp, err := c.DoRequest(r)
	if err != nil {
		return UserResponse{}, errors.Wrap(err, "Error requesting users")
	}
	defer resp.Body.Close()
	resBody, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return UserResponse{}, errors.Wrap(err, "Error reading user request")
	}
	err = json.Unmarshal(resBody, &userResp)
	if err != nil {
		return UserResponse{}, errors.Wrap(err, "Error unmarshalling user")
	}
	return userResp, nil
}

func (c *Client) mergeUserResource(u UserResource) User {
	u.Entity.Guid = u.Meta.Guid
	u.Entity.CreatedAt = u.Meta.CreatedAt
	u.Entity.UpdatedAt = u.Meta.UpdatedAt
	u.Entity.c = c
	return u.Entity
}
