package usersservice

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

	"golang.org/x/net/context"

	"time"

	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/web/users-service/models"
)

const (
	defaultStatSampleRate = 1.0
	defaultTimingXactName = "users_service"
)

// Client is an interface that exposes methods to fetch data from the users service.
type Client interface {
	GetUserByID(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) (*models.Properties, error)
	GetUserByLogin(ctx context.Context, login string, reqOpts *twitchhttp.ReqOpts) (*models.Properties, error)
	GetUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchhttp.ReqOpts) (*models.PropertiesResult, error)
	GetUserByEmail(ctx context.Context, email string, reqOpts *twitchhttp.ReqOpts) (*models.Properties, error)
	GetBannedUsers(ctx context.Context, until time.Time, reqOpts *twitchhttp.ReqOpts) (*models.PropertiesResult, error)
	GetRenameEligibility(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) (*models.RenameProperties, error)
	BanUserByID(ctx context.Context, userID string, reportingID string, banType string, isWarning bool, banReason string, reqOpts *twitchhttp.ReqOpts) error
	UnbanUserByID(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) error
	AddDMCAStrike(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) error
	RemoveDMCAStrike(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) error
	SetUser(ctx context.Context, userID string, uup *models.UpdateableProperties, reqOpts *twitchhttp.ReqOpts) error
}

type clientImpl struct {
	twitchhttp.Client
}

type UserNotFoundError struct{}

func (e *UserNotFoundError) Error() string {
	return "User not found"
}

// NewClient creates a client for the users service.
func NewClient(conf twitchhttp.ClientConf) (Client, error) {
	if conf.TimingXactName == "" {
		conf.TimingXactName = defaultTimingXactName
	}

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

	return &clientImpl{twitchClient}, nil
}

func (c *clientImpl) GetUserByID(ctx context.Context, userID string, reqOpts *twitchhttp.ReqOpts) (*models.Properties, error) {
	path := fmt.Sprintf("/users/%s?return_id_as_string=true", userID)
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		return nil, err
	}

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.get_user",
		StatSampleRate: 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, &UserNotFoundError{}

	default:
		// Unexpected result
		return nil, twitchhttp.HandleFailedResponse(httpResp)
	}
}

func (c *clientImpl) GetUserByLogin(ctx context.Context, login string, reqOpts *twitchhttp.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, &UserNotFoundError{}
	}
	return users.Results[0], nil
}

func (c *clientImpl) GetUserByEmail(ctx context.Context, email string, reqOpts *twitchhttp.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, &UserNotFoundError{}
	}
	return users.Results[0], nil
}

func (c *clientImpl) GetUsers(ctx context.Context, params *models.FilterParams, reqOpts *twitchhttp.ReqOpts) (*models.PropertiesResult, error) {
	query := url.Values{}
	if params.IDs != nil {
		for _, ID := range params.IDs {
			query.Add("id", fmt.Sprintf("%s", ID))
		}
	}
	if params.Logins != nil {
		for _, login := range params.Logins {
			query.Add("login", login)
		}
	}
	if params.Emails != nil {
		for _, email := range params.Emails {
			query.Add("email", email)
		}
	}

	if params.DisplayNames != nil {
		for _, dn := range params.DisplayNames {
			query.Add("displayname", dn)
		}
	}
	if params.Ips != nil {
		for _, ip := range params.Ips {
			query.Add("ip", ip)
		}
	}

	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 := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.get_users_batch",
		StatSampleRate: 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, twitchhttp.HandleFailedResponse(httpResp)
	}
}

func (c *clientImpl) GetBannedUsers(ctx context.Context, until time.Time, reqOpts *twitchhttp.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 := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.get_banned_users",
		StatSampleRate: 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, twitchhttp.HandleFailedResponse(httpResp)
	}
}

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

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.get_rename_eligibility",
		StatSampleRate: 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, &UserNotFoundError{}

	default:
		// Unexpected result
		return nil, twitchhttp.HandleFailedResponse(httpResp)
	}
}

func (c *clientImpl) BanUserByID(ctx context.Context, userID string, reportingID string, banType string, isWarning bool, banReason string, reqOpts *twitchhttp.ReqOpts) error {
	path := fmt.Sprintf("/users/%s/ban", userID)

	params := struct {
		ReportingID string `json:"reporting_id"`
		Type        string `json:"type"`
		Warn        bool   `json:"warn"`
		Reason      string `json:"reason"`
	}{
		fmt.Sprintf("%s", reportingID),
		banType,
		isWarning,
		banReason,
	}

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

	body := bytes.NewBuffer(bodyJson)

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

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.ban_user",
		StatSampleRate: 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 &UserNotFoundError{}

	default:
		// Unexpected result
		return twitchhttp.HandleFailedResponse(httpResp)
	}
}

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

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: 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 &UserNotFoundError{}

	default:
		// Unexpected result
		return twitchhttp.HandleFailedResponse(httpResp)
	}
}

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

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: 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 &UserNotFoundError{}

	default:
		// Unexpected result
		return twitchhttp.HandleFailedResponse(httpResp)
	}
}

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

	combinedReqOpts := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.unban_user",
		StatSampleRate: 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 &UserNotFoundError{}

	default:
		// Unexpected result
		return twitchhttp.HandleFailedResponse(httpResp)
	}
}

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

	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 := twitchhttp.MergeReqOpts(reqOpts, twitchhttp.ReqOpts{
		StatName:       "service.users_service.update_user_properties",
		StatSampleRate: 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 &UserNotFoundError{}

	default:
		// Unexpected result
		return twitchhttp.HandleFailedResponse(httpResp)
	}

}
