package clue

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

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/creator-collab/log/errors"
	"code.justin.tv/live/autohost/internal/logfield"
	"code.justin.tv/live/autohost/internal/metrics"
	"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 serviceClient struct {
	twitchClient twitchhttp.Client
}

func newServiceClient(url string, statsClient statsd.Statter, sampleReporter *telemetry.SampleReporter) (*serviceClient, 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, errors.Wrap(err, "failed to create hostmode client")
	}

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

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

	req, err := c.twitchClient.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.twitchClient.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
}

func (c *serviceClient) host(ctx context.Context, channelID, targetID string) (HostResult, error) {
	ctx = metrics.WithTwitchClientOperation(ctx, metricsServiceName, "Host")
	errFields := errors.Fields{
		logfield.HosterChannelID:     channelID,
		logfield.HostTargetChannelID: targetID,
	}

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

	body := "autohost=true&force_replication=false"
	req.Body = ioutil.NopCloser(strings.NewReader(body))
	req.Header.Set("Accept", "*/*")
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

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

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

	// http errors come through the response, return the error here
	if resp.StatusCode >= 400 {
		result := HostResultUnexpectedError
		if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnprocessableEntity {
			result = HostResultNotAllowed
		}

		errFields["tmi_error_code"] = string(respBody)
		errFields["status_code"] = resp.StatusCode
		return result, errors.New("tmi/clue - host failed", errFields)
	}

	return HostResultSuccess, nil
}

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

	req, err := c.twitchClient.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.twitchClient.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 {
		return errors.Wrap(err, "tmi/clue - unhost - reading response failed", errFields)
	}

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

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

	return nil
}
