package clue

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"

	tmi "code.justin.tv/chat/tmi/client"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/live/autohost/internal/metrics"
	"code.justin.tv/live/autohost/lib"

	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/creator-collab/log"
	"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"
)

const metricsServiceName = "Clue"

type hostResponse struct {
	Hosts []host `json:"hosts"`
}

type host struct {
	Hoster int `json:"host_id"`
	Target int `json:"target_id"`
}

type hostChannelResponse struct {
	ErrorCode string `json:"error_code,omitempty"`
}

// Client is a hostmode client
type Client interface {
	GetHost(ctx context.Context, channelIDs []string) (map[string]string, error)
	GetHosters(ctx context.Context, channelID string) ([]string, error)
	Host(ctx context.Context, channelID, targetID string, isAutohost bool, isPostRaidHost bool) error
	Unhost(ctx context.Context, channelID string) error
}

type clientImpl struct {
	twitchhttp.Client
	logger log.Logger
}

func init() {
	hystrix.Configure(map[string]hystrix.CommandConfig{
		"Hostmode.GetHost": {
			MaxConcurrentRequests:  1000,
			Timeout:                1000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
		"Hostmode.GetHosters": {
			MaxConcurrentRequests:  1000,
			Timeout:                5000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
		"Hostmode.Host": {
			MaxConcurrentRequests:  1000,
			Timeout:                5000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
		"Hostmode.Unhost": {
			MaxConcurrentRequests:  1000,
			Timeout:                5000,
			RequestVolumeThreshold: 20,
			ErrorPercentThreshold:  50,
		},
	})
}

// NewClient returns a new client
func NewClient(url string, statsClient statsd.Statter, logger log.Logger, sampleReporter *telemetry.SampleReporter) (Client, error) {
	twitchClient, err := twitchhttp.NewClient(twitchhttp.ClientConf{
		Host:           url,
		Stats:          statsClient,
		TimingXactName: "hostmode",
		Transport: twitchhttp.TransportConf{
			MaxIdleConnsPerHost: 10,
		},
		RoundTripperWrappers: []func(http.RoundTripper) http.RoundTripper{
			metrics.NewTwitchClientMiddlewareWrapper(&metrics.TwitchClientMiddlewareWrapperConfig{
				SampleReporter: sampleReporter,
			}),
		},
	})
	if err != nil {
		return nil, err
	}

	return &clientImpl{
		Client: twitchClient,
		logger: logger,
	}, nil
}

// GetHost gets the host status of a list of channels.
func (c *clientImpl) GetHost(ctx context.Context, channelIDs []string) (map[string]string, error) {
	if len(channelIDs) == 0 {
		return nil, nil
	}

	c0 := make(chan map[string]string, 1)
	ce := make(chan error, 1)

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

		r0, err := c.getHost(ctx, channelIDs)
		c0 <- r0
		ce <- 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, "tmi/clue - get target channels that are being hosted by given users")
	}
	return <-c0, <-ce
}

func (c *clientImpl) getHost(ctx context.Context, channelIDs []string) (map[string]string, error) {
	ctx = metrics.WithTwitchClientOperation(ctx, metricsServiceName, "GetHosts")

	req, err := c.NewRequest("GET", fmt.Sprintf("/hosts?host=%s", strings.Join(channelIDs, ",")), nil)
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get target channels that are being hosted by given users - creating request failed")
	}

	resp, err := c.Do(ctx, req, twitchhttp.ReqOpts{
		StatName:       "service.hostmode.get_host",
		StatSampleRate: 0.1,
	})
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get target channels that are being hosted by given users - sending request failed")
	}
	defer resp.Body.Close()

	var hosts hostResponse
	err = json.NewDecoder(resp.Body).Decode(&hosts)
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get target channels that are being hosted by given users - unmarshal failed")
	}

	hostMap := map[string]string{}
	for _, h := range hosts.Hosts {
		if h.Target == 0 {
			hostMap[strconv.Itoa(h.Hoster)] = ""
		} else {
			hostMap[strconv.Itoa(h.Hoster)] = strconv.Itoa(h.Target)
		}
	}

	return hostMap, nil
}

// GetHosters gets the list of channels hosting <channelID>.
func (c *clientImpl) GetHosters(ctx context.Context, channelID string) ([]string, error) {
	c0 := make(chan []string, 1)
	ce := make(chan error, 1)

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

		r0, err := c.getHosters(ctx, channelID)
		c0 <- r0
		ce <- 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, "tmi/clue - get channels that are hosting target failed", errors.Fields{
			logfield.HostTargetChannelID: channelID,
		})
	}
	return <-c0, <-ce
}

func (c *clientImpl) getHosters(ctx context.Context, channelID string) ([]string, error) {
	ctx = metrics.WithTwitchClientOperation(ctx, metricsServiceName, "GetHostSources")

	req, err := c.NewRequest("GET", fmt.Sprintf("/hosts?target=%s", channelID), nil)
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get channels that are hosting target - creating request failed", errors.Fields{
			logfield.HostTargetChannelID: channelID,
		})
	}

	resp, err := c.Do(ctx, req, twitchhttp.ReqOpts{
		StatName:       "service.hostmode.get_hosters",
		StatSampleRate: 0.1,
	})
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get channels that are hosting target - sending request failed", errors.Fields{
			logfield.HostTargetChannelID: channelID,
		})
	}
	defer resp.Body.Close()

	var hosts hostResponse
	err = json.NewDecoder(resp.Body).Decode(&hosts)
	if err != nil {
		return nil, errors.Wrap(err, "tmi/clue - get channels that are hosting target - unmarshal response failed", errors.Fields{
			logfield.HostTargetChannelID: channelID,
		})
	}

	hosters := make([]string, len(hosts.Hosts))
	for i, h := range hosts.Hosts {
		hosters[i] = strconv.Itoa(h.Hoster)
	}

	return hosters, nil
}

// Host sends a host call to TMI from <channelID> to <targetID>.
// isAutohost modifies the message displayed to the hosted channel.
func (c *clientImpl) Host(ctx context.Context, channelID, targetID string, isAutohost bool, isPostRaidHost bool) error {
	ce := make(chan error, 1)

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

		err := c.host(ctx, channelID, targetID, isAutohost, isPostRaidHost)
		ce <- err

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

		return err
	}, nil)

	if err != nil {
		return errors.Wrap(err, "tmi/clue - host failed", errors.Fields{
			logfield.HosterChannelID:     channelID,
			logfield.HostTargetChannelID: targetID,
			"is_autohost":                isAutohost,
			"is_post_raid_host":          isPostRaidHost,
		})
	}
	return <-ce
}

func (c *clientImpl) host(ctx context.Context, channelID, targetID string, isAutohost bool, isPostRaidHost bool) error {
	ctx = metrics.WithTwitchClientOperation(ctx, metricsServiceName, "Host")
	errFields := errors.Fields{
		logfield.HosterChannelID:     channelID,
		logfield.HostTargetChannelID: targetID,
		"is_autohost":                isAutohost,
		"is_post_raid_host":          isPostRaidHost,
	}

	req, err := c.NewRequest("PUT", fmt.Sprintf("/internal/users/%s/hosting/%s", channelID, targetID), nil)
	if err != nil {
		return errors.Wrap(err, "tmi/clue - host - create request failed", errFields)
	}

	body := make([]string, 0, 3)
	if isAutohost {
		body = append(body, "autohost=true")
	}
	if isPostRaidHost {
		body = append(body, "postraid=true")
	}
	body = append(body, "force_replication=false")

	if len(body) > 0 {
		req.Body = ioutil.NopCloser(strings.NewReader(strings.Join(body, "&")))
	}

	req.Header.Set("Accept", "*/*")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := c.Do(ctx, req, twitchhttp.ReqOpts{
		StatName:       "service.hostmode.host",
		StatSampleRate: 0.1,
	})
	if err != nil {
		return errors.Wrap(err, "tmi/clue - host - send request failed", errFields)
	}
	defer resp.Body.Close()

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		c.logger.Error(errors.Wrap(err, "tmi/clue - host - reading error response body failed", errFields))
	}

	// http errors come through the response, return the error here
	if resp.StatusCode >= 400 {
		errFields["tmi_response_body"] = string(respBody)
		errFields["tmi_response_code"] = resp.StatusCode

		c.logger.Error(errors.New("tmi/clue - host - TMI Response Error", errFields))

		return c.convertTMIError(resp.StatusCode, respBody)
	}

	return nil
}

func (c *clientImpl) convertTMIError(statusCode int, respBody []byte) error {
	if len(respBody) == 0 {
		return &twitchhttp.Error{
			StatusCode: statusCode,
			Message:    lib.ErrUnknown,
		}
	}

	var hostChannelResponse hostChannelResponse
	err := json.Unmarshal(respBody, &hostChannelResponse)
	if err != nil {
		c.logger.Error(errors.Wrap(err, "tmi/clue - host - json unmarshaling failed"))
		return &twitchhttp.Error{
			StatusCode: statusCode,
			Message:    lib.ErrUnknown,
		}
	}

	errorCode := convertTMIErrorCode(hostChannelResponse.ErrorCode)
	return &twitchhttp.Error{
		StatusCode: statusCode,
		Message:    errorCode,
	}
}

// Converts a TMI error into an Autohost error.
func convertTMIErrorCode(errCode string) string {
	switch errCode {
	case tmi.AlreadyHostingError:
		return lib.ErrAlreadyHosting
	case tmi.UnhostableChannelError:
		return lib.ErrUnhostableChannel
	case tmi.HostTargetDisabledError:
		return lib.ErrHostTargetDisabled
	case tmi.CannotHostSelfError:
		return lib.ErrCannotHostSelf
	default:
		return lib.ErrUnknown
	}
}

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

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

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

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

		return err
	}, nil)

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

func (c *clientImpl) unhost(ctx context.Context, channelID string) error {
	ctx = metrics.WithTwitchClientOperation(ctx, metricsServiceName, "Unhost")
	errFields := errors.Fields{
		logfield.HosterChannelID: channelID,
	}

	req, err := c.NewRequest("DELETE", fmt.Sprintf("/internal/users/%s/hosting", channelID), nil)
	if err != nil {
		return errors.Wrap(err, "tmi/clue - unhost - creating request failed", errFields)
	}

	req.Header.Set("Accept", "*/*")

	resp, err := c.Do(ctx, req, twitchhttp.ReqOpts{
		StatName:       "service.hostmode.unhost",
		StatSampleRate: 0.1,
	})
	if err != nil {
		return errors.Wrap(err, "tmi/clue - unhost - sending request failed", errFields)
	}
	defer resp.Body.Close()

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		c.logger.Error(errors.Wrap(err, "tmi/clue - unhost - reading error response body failed", errFields))
	}

	// http errors come through the response, return the error here
	if resp.StatusCode >= 400 {
		errFields["tmi_response_body"] = string(respBody)
		c.logger.Error(errors.New("tmi/clue - unhost - TMI Response Error", errFields))

		return &twitchhttp.Error{
			StatusCode: resp.StatusCode,
			Message:    "TMI Response Error",
		}
	}

	return nil
}
