package clue

import (
	"context"
	"fmt"
	"math"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/creator-collab/log/errors"
	"code.justin.tv/live/autohost/internal/logfield"
	"github.com/afex/hystrix-go/hystrix"
	"github.com/cactus/go-statsd-client/statsd"

	"code.justin.tv/common/twitchhttp"
)

type HostResult string

const (
	// The target was successfully hosted.
	HostResultSuccess HostResult = "success"
	// Hosting the target failed because:
	// - The target has blocked the host
	// - The target has been banned
	// - The source or target could not be found.
	HostResultNotAllowed HostResult = "not_allowed"
	// Hosting the target failed for an unexpected reason.
	HostResultUnexpectedError HostResult = "unexpected_error"
)

// Client is a higher level wrapper around a Clue client for the Worker to use..
type Client interface {
	GetHostTargets(ctx context.Context, sourceIDs []string) (map[string]string, error)
	Host(ctx context.Context, sourceID, targetID string) (HostResult, error)
	Unhost(ctx context.Context, sourceID string) error
}

type clientImpl struct {
	serviceClient *serviceClient
}

const (
	circuitNameGetHostTargets = "Clue.GetHostTargets"
	circuitNameHost           = "Clue.Host"
	circuitNameUnhost         = "Clue.Unhost"
)

func init() {
	hystrix.Configure(map[string]hystrix.CommandConfig{
		circuitNameGetHostTargets: {
			MaxConcurrentRequests:  1000,
			Timeout:                1000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
		circuitNameHost: {
			MaxConcurrentRequests:  1000,
			Timeout:                5000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
		circuitNameUnhost: {
			MaxConcurrentRequests:  1000,
			Timeout:                5000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
	})
}

// NewClient returns a new client
func NewClient(url string, statsClient statsd.Statter, sampleReporter *telemetry.SampleReporter) (Client, error) {
	serviceClient, err := newServiceClient(url, statsClient, sampleReporter)
	if err != nil {
		return nil, err
	}

	return &clientImpl{
		serviceClient: serviceClient,
	}, nil
}

// GetHostTargets returns the host targets of the given source channels.
func (c *clientImpl) GetHostTargets(ctx context.Context, sourceIDs []string) (map[string]string, error) {
	chunkSize := 1000
	hostMap := map[string]string{}

	for i := 0; i < len(sourceIDs); i += chunkSize {
		end := int(math.Min(float64(i+chunkSize), float64(len(sourceIDs))))

		hosts, err := c.getHostTargetsSingleBatch(ctx, sourceIDs[i:end])
		if err != nil {
			return map[string]string{}, err
		}

		for channelID, hostID := range hosts {
			hostMap[channelID] = hostID
		}
	}

	return hostMap, nil
}

func (c *clientImpl) getHostTargetsSingleBatch(ctx context.Context, channelIDs []string) (map[string]string, error) {
	if len(channelIDs) == 0 {
		return nil, nil
	}

	resultChan := make(chan map[string]string, 1)
	errChan := make(chan error, 1)

	err := hystrix.Do(circuitNameGetHostTargets, func() (e error) {
		defer func() {
			if p := recover(); p != nil {
				e = errors.New(fmt.Sprintf("%s circuit panic=%v", circuitNameGetHostTargets, p))
			}
		}()

		result, err := c.serviceClient.getHost(ctx, channelIDs)
		resultChan <- result
		errChan <- err

		if r, ok := err.(*twitchhttp.Error); ok {
			if r.StatusCode < 500 {
				return nil
			}
		}

		return err
	}, nil)

	if err != nil {
		return nil, errors.Wrap(err, "clue - get host targets failed")
	}
	return <-resultChan, <-errChan
}

// Host sends a host call to TMI from <channelID> to <targetID>.
func (c *clientImpl) Host(ctx context.Context, channelID, targetID string) (HostResult, error) {
	resultChan := make(chan HostResult, 1)
	errChan := make(chan error, 1)

	err := hystrix.Do(circuitNameHost, func() (e error) {
		defer func() {
			if p := recover(); p != nil {
				e = errors.New(fmt.Sprintf("%s circuit panic=%v", circuitNameHost, p))
			}
		}()

		result, err := c.serviceClient.host(ctx, channelID, targetID)

		// Pass the result and error back to Host's goroutine.
		resultChan <- result
		errChan <- err

		// If there was an unexpected error, count it against the circuit's health.
		if err != nil && result == HostResultUnexpectedError {
			return err
		}
		return nil
	}, nil)
	if err != nil {
		return HostResultUnexpectedError, errors.Wrap(err, "clue - host failed", errors.Fields{
			logfield.HosterChannelID:     channelID,
			logfield.HostTargetChannelID: targetID,
		})
	}

	// Retrieve the results from the goroutine that hystrix creates.
	return <-resultChan, <-errChan
}

// Unhost sends an unhost call to TMI on <channelID>.
func (c *clientImpl) Unhost(ctx context.Context, channelID string) error {
	errChan := make(chan error, 1)

	err := hystrix.Do(circuitNameUnhost, func() (e error) {
		defer func() {
			if p := recover(); p != nil {
				e = errors.New(fmt.Sprintf("%s circuit panic=%v", circuitNameUnhost, p))
			}
		}()

		err := c.serviceClient.unhost(ctx, channelID)
		errChan <- err

		if r, ok := err.(*twitchhttp.Error); ok {
			if r.StatusCode < 500 {
				return nil
			}
		}

		return err
	}, nil)

	if err != nil {
		return errors.Wrap(err, "clue - unhost failed", errors.Fields{
			logfield.HosterChannelID: channelID,
		})
	}
	return <-errChan
}
