package client

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

	"code.justin.tv/foundation/twitchclient"
	"code.justin.tv/web/users-service/models"
)

const (
	DefaultStatSampleRate = 1.0
	DefaultTimingXactName = "users_service"
	BatchSize             = 100
)

var (
	_ Error = (*models.CodedError)(nil)
	_ Error = (*models.ErrorResponse)(nil)
)

// Error describes an errors full message and HTTP status code.
type Error interface {
	// Full error message
	Error() string
	// HTTP status code
	StatusCode() int
}

type BatchResult struct {
	Users *models.PropertiesResult
	Err   error
}

type BatchIDResult struct {
	IDs *models.IDsResult
	Err error
}

type UserNotFoundError struct{}

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

func (e *UserNotFoundError) StatusCode() int {
	return http.StatusNotFound
}

func IsUserNotFound(err error) bool {
	return err != nil && err.Error() == (&UserNotFoundError{}).Error()
}

type UserFilteredError struct{}

func (e *UserFilteredError) Error() string {
	return "User exists but was excluded by filter criteria."
}

func (e *UserFilteredError) StatusCode() int {
	return http.StatusUnprocessableEntity
}

func IsUserFiltered(err error) bool {
	return err != nil && err.Error() == (&UserFilteredError{}).Error()
}

// IsInternalServerError returns whether or not the provided method represents an
// internal server error. If a error is passed that is not recognized, it
// is an classified as a server error.
func IsInternalServerError(err error) bool {
	if e, ok := err.(Error); ok && e.StatusCode() < 500 {
		return false
	} else if _, ok := err.(*UserNotFoundError); ok {
		return false
	}
	return true
}

func GetAsyncWithQuery(ctx context.Context, c twitchclient.Client, query url.Values, urlPath string, params *models.FilterParams, ch chan *BatchResult, reqOpts *twitchclient.ReqOpts) {
	defer func() {
		r := recover()
		if r != nil {
			err := fmt.Errorf("panic in users service client: %v", r)
			ch <- &BatchResult{Err: err}
		}
	}()
	ModifyQuery(&query, params)

	query.Add("return_id_as_string", "true")
	path := (&url.URL{
		Path:     urlPath,
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		ch <- &BatchResult{Err: err}
		return
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_users_batch",
		StatSampleRate: DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		ch <- &BatchResult{Err: err}
		return
	}
	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 {
			ch <- &BatchResult{Err: err}
			return
		}
		ch <- &BatchResult{Users: &decoded}

	default:
		// Unexpected result
		ch <- &BatchResult{Err: HandleUnexpectedResult(httpResp)}
	}
}

func GetAsyncWithQueryIDOnly(ctx context.Context, c twitchclient.Client, query url.Values, urlPath string, params *models.FilterParams, ch chan *BatchIDResult, reqOpts *twitchclient.ReqOpts) {
	defer func() {
		r := recover()
		if r != nil {
			err := fmt.Errorf("panic in users service client: %v", r)
			ch <- &BatchIDResult{Err: err}
		}
	}()
	ModifyQuery(&query, params)

	query.Add("return_id_only", "true")
	path := (&url.URL{
		Path:     urlPath,
		RawQuery: query.Encode(),
	}).String()
	req, err := c.NewRequest("GET", path, nil)
	if err != nil {
		ch <- &BatchIDResult{Err: err}
		return
	}

	combinedReqOpts := twitchclient.MergeReqOpts(reqOpts, twitchclient.ReqOpts{
		StatName:       "service.users_service.get_users_batch_id_only",
		StatSampleRate: DefaultStatSampleRate,
	})
	httpResp, err := c.Do(ctx, req, combinedReqOpts)
	if err != nil {
		ch <- &BatchIDResult{Err: err}
		return
	}
	defer func() {
		err = httpResp.Body.Close()
	}()

	switch httpResp.StatusCode {
	case http.StatusOK:
		var decoded models.IDsResult
		if err := json.NewDecoder(httpResp.Body).Decode(&decoded); err != nil {
			ch <- &BatchIDResult{Err: err}
			return
		}
		ch <- &BatchIDResult{IDs: &decoded}

	default:
		// Unexpected result
		ch <- &BatchIDResult{Err: HandleUnexpectedResult(httpResp)}
	}
}

func GetAsync(ctx context.Context, c twitchclient.Client, urlPath string, params *models.FilterParams, ch chan *BatchResult, reqOpts *twitchclient.ReqOpts) {
	GetAsyncWithQuery(ctx, c, url.Values{}, urlPath, params, ch, reqOpts)
}

func GetAsyncIDOnly(ctx context.Context, c twitchclient.Client, urlPath string, params *models.FilterParams, ch chan *BatchIDResult, reqOpts *twitchclient.ReqOpts) {
	GetAsyncWithQueryIDOnly(ctx, c, url.Values{}, urlPath, params, ch, reqOpts)
}

func HandleUnexpectedResult(httpResp *http.Response) error {
	var e models.ErrorResponse
	e.Status = httpResp.StatusCode
	err := json.NewDecoder(httpResp.Body).Decode(&e)
	if err != nil {
		e.Message = fmt.Sprintf("Unable to read response body: %s", err)
		return e
	}
	if models.ErrMap[e.ErrorCode] != nil {
		return models.ErrMap[e.ErrorCode]
	}
	return e
}

func BatchParams(params *models.FilterParams) []*models.FilterParams {
	if params == nil {
		return []*models.FilterParams{}
	}

	newBatch := &models.FilterParams{}
	batches := []*models.FilterParams{}
	newBatch = appendBatchesFromValues(&batches, newBatch, params.IDs, func(params *models.FilterParams, vals []string) {
		params.IDs = vals
	})
	newBatch = appendBatchesFromValues(&batches, newBatch, params.Logins, func(params *models.FilterParams, vals []string) {
		params.Logins = vals
	})
	newBatch = appendBatchesFromValues(&batches, newBatch, params.DisplayNames, func(params *models.FilterParams, vals []string) {
		params.DisplayNames = vals
	})
	newBatch = appendBatchesFromValues(&batches, newBatch, params.Emails, func(params *models.FilterParams, vals []string) {
		params.Emails = vals
	})
	newBatch = appendBatchesFromValues(&batches, newBatch, params.Ips, func(params *models.FilterParams, vals []string) {
		params.Ips = vals
	})
	newBatch = appendBatchesFromValues(&batches, newBatch, params.PhoneNumbers, func(params *models.FilterParams, vals []string) {
		params.PhoneNumbers = vals
	})

	// always append the last batch if it was not a complete batch
	if newBatch.FilterLength() > 0 {
		batches = append(batches, newBatch)
	}

	for _, batch := range batches {
		batch.NoDMCAViolation = params.NoDMCAViolation
		batch.NoTOSViolation = params.NoTOSViolation
		batch.NotDeleted = params.NotDeleted
		batch.InlineIdentifierValidation = params.InlineIdentifierValidation
	}

	return batches
}

func CombineBatches(ch chan *BatchResult, batches []*models.FilterParams) (*models.PropertiesResult, error) {
	propertiesResult := &models.PropertiesResult{}
	for i := 0; i < len(batches); i++ {
		result := <-ch
		if result.Err != nil {
			return nil, result.Err
		}
		propertiesResult.Results = append(propertiesResult.Results, result.Users.Results...)
		propertiesResult.BadIdentifiers = append(propertiesResult.BadIdentifiers, result.Users.BadIdentifiers...)
	}
	return propertiesResult, nil
}

func CombineBatchIDs(ch chan *BatchIDResult, batches []*models.FilterParams) (*models.IDsResult, error) {
	idsResult := &models.IDsResult{}
	for i := 0; i < len(batches); i++ {
		result := <-ch
		if result.Err != nil {
			return nil, result.Err
		}
		idsResult.Results = append(idsResult.Results, result.IDs.Results...)
		idsResult.BadIdentifiers = append(idsResult.BadIdentifiers, result.IDs.BadIdentifiers...)
	}
	return idsResult, nil
}

// appendBatchesFromValues batches values and appends them as params to batches.
//
// batches is a slice of filter params to which new batches will be appended
// nextBatch is a batch which can still accept more identifiers before it is at batchSize
// values is a slice of identifiers
// assignFn is a function which will assign new slices of identifiers to a batch
//
// it returns an incomplete batch
func appendBatchesFromValues(batches *[]*models.FilterParams, nextBatch *models.FilterParams, values []string, assignFn func(*models.FilterParams, []string)) *models.FilterParams {
	for i := 0; i < len(values); {
		remainingFilters := BatchSize - nextBatch.FilterLength()
		batchSlice := getBatchSlice(values, i, remainingFilters)
		assignFn(nextBatch, batchSlice)

		if nextBatch.FilterLength() == BatchSize {
			*batches = append(*batches, nextBatch)
			nextBatch = &models.FilterParams{}
		}

		i += len(batchSlice)
	}

	return nextBatch
}

func min(x int, y int) int {
	if x < y {
		return x
	}
	return y
}

func getBatchSlice(values []string, batchStart int, maxSize int) []string {
	batchEnd := batchStart + min(BatchSize, maxSize)

	if batchEnd > len(values) {
		return values[batchStart:]
	}
	return values[batchStart:batchEnd]
}

func ModifyQuery(query *url.Values, params *models.FilterParams) {
	if params == nil {
		return
	}

	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)
		}
	}
	if params.PhoneNumbers != nil {
		for _, phoneNumber := range params.PhoneNumbers {
			query.Add("phone_number", phoneNumber)
		}
	}
	if params.NotDeleted {
		query.Add(models.NotDeletedParam, "true")
	}

	if params.NoTOSViolation {
		query.Add(models.NoTOSViolationParam, "true")
	}

	if params.NoDMCAViolation {
		query.Add(models.NoDMCAViolationParam, "true")
	}

	if params.InlineIdentifierValidation {
		query.Add(models.InlineIdentifierValidation, "true")
	}
}

func ModifyQueryFieldValue(query *url.Values, field string, value string) {
	if value == "" {
		return
	}
	query.Add(field, value)
}
