package usher

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"time"

	"github.com/stvp/rollbar"

	"code.justin.tv/video/spectre/vods"
)

var (
	usherURL        = "http://usher.twitch.tv"
	heartbeatPeriod = 10 * time.Second
	// ErrBadChannels is an error given when the channel names from usher could not be unmarshaled
	ErrBadChannels = errors.New("usher: could not unmarshal channel names from usher")
	// ErrBadStreamsFormat is an error given when the response format could not be parsed
	ErrBadStreamsFormat = errors.New("usher: json was in an unexpected format for list of streams")
	// ErrNoChannelsField is an error given when no 'channels' field was found
	ErrNoChannelsField = errors.New("usher: json did not contain the expected 'channels' field")
)

type Usher interface {
	SpectreChannels() (map[int]bool, error)
	LiveChannels() (map[int]bool, error)
	StreamUp(channelId int, vodId int) (bool, int)
	StreamDown(channelId int) (bool, int)
	Update(channelId int, vodProps vods.Properties) bool
	IsLive(channelId int) (bool, error)
	RefreshStream(channelId int) bool
	RefreshViewcount(channelId int) bool
}

type UsherService struct{}

type vodRequest struct {
	VodID   int    `json:"vod_id"`
	Channel string `json:"channel,omitempty"`
	Title   string `json:"title,omitempty"`
	Game    string `json:"game,omitempty"`
	Partner bool   `json:"partner"`
}

// SpectreChannels returns the list of currently running spectre streams in Usher
func (UsherService) SpectreChannels() (map[int]bool, error) {
	url := usherURL + "/stream/list.json?broadcaster=spectre&format=hls&columns=channel_id"
	response, err := http.Get(url)
	defer func() {
		err := response.Body.Close()
		if err != nil {
			rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		}
	}()
	if err != nil {
		return nil, err
	}
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		return nil, err
	}

	var m []map[string]int
	channelMap := make(map[int]bool)
	err = json.Unmarshal(body, &m)

	if err != nil {
		return channelMap, ErrBadChannels
	}

	for _, ch := range m {
		channelId, ok := ch["channel_id"]
		if !ok {
			return nil, ErrBadStreamsFormat
		}
		channelMap[channelId] = true
	}

	return channelMap, nil
}

// LiveChannels returns the list of currently live broadcasting channel ids
func (UsherService) LiveChannels() (map[int]bool, error) {
	url := usherURL + "/spectre/non_spectre_channels.json"
	response, err := http.Get(url)
	defer func() {
		err := response.Body.Close()
		if err != nil {
			rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		}
	}()
	if err != nil {
		return nil, err
	}
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		return nil, err
	}

	// Usher response unfortunately looks like []map[string][]int.
	// Here we convert it to map[int]bool
	var m []map[string][]int
	channelMap := make(map[int]bool)
	err = json.Unmarshal(body, &m)

	if err != nil {
		return channelMap, ErrBadChannels
	}
	if len(m) != 1 {
		return channelMap, ErrBadStreamsFormat
	}
	if _, ok := m[0]["channels"]; !ok {
		return channelMap, ErrNoChannelsField
	}

	for _, ch := range m[0]["channels"] {
		channelMap[ch] = true
	}
	return channelMap, err
}

// StreamUp notifies Usher that a channel has started spectre broadcasting
func (UsherService) StreamUp(channelID int, vodId int) (bool, int) {
	url := fmt.Sprintf("%v/spectre/stream_up/%v", usherURL, channelID)
	vodProperties := vods.Properties{
		ID: vodId,
	}
	return post(url, &vodProperties)
}

// StreamDown notifies Usher that a channel has stopped spectre broadcasting
func (UsherService) StreamDown(channelID int) (bool, int) {
	url := fmt.Sprintf("%v/spectre/stream_down/%v", usherURL, channelID)
	return post(url, nil)
}

// Update notifies Usher that a channel has transitioned to another vod in the playlist
func (UsherService) Update(channelID int, vodProperties vods.Properties) bool {
	url := fmt.Sprintf("%v/spectre/update/%v", usherURL, channelID)
	vodProperties.Channel = ""
	success, _ := post(url, &vodProperties)
	return success
}

func (UsherService) RefreshStream(channelID int) bool {
	url := fmt.Sprintf("%v/spectre/refresh_stream/%v.json", usherURL, channelID)
	return postStd(url)
}

func (UsherService) RefreshViewcount(channelID int) bool {
	url := fmt.Sprintf("%v/spectre/refresh_viewcount/%v.json", usherURL, channelID)
	return postStd(url)
}

func postStd(url string) bool {
	response, err := http.Post(url, "", nil)
	if response.StatusCode != 200 || err != nil {
		fmt.Printf("ERROR: Call to usher failed; url=%v, code=%v, msg=%v\n", url, response.StatusCode, err)
		return false
	}
	return true
}

func post(url string, vodProperties *vods.Properties) (bool, int) {
	var b io.Reader
	if vodProperties != nil {
		request, err := json.Marshal(&vodRequest{
			VodID:   vodProperties.ID,
			Channel: vodProperties.Channel,
			Title:   vodProperties.Title,
			Game:    vodProperties.Game,
			Partner: true, // hardcode this to true for now. TODO: reevaluate this after v1 and we have nonpartners using this
		})
		if err != nil {
			rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		}
		b = bytes.NewBuffer(request)
	}
	response, err := http.Post(url, "application/json", b)
	if err != nil {
		rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
	}

	if response.StatusCode != 200 {
		if vodProperties != nil {
			fmt.Printf("Received %v response code from usher on %v request. Vod ID: %v, Channel: %v\n", response.StatusCode, url, vodProperties.ID, vodProperties.Channel)
		} else {
			fmt.Printf("Received %v response code from usher on %v request.\n", response.StatusCode, url)
		}

		body, _ := ioutil.ReadAll(response.Body)
		fmt.Println(string(body))
		// TODO: log to statsd
		return false, response.StatusCode
	}

	defer func() {
		err := response.Body.Close()
		if err != nil {
			rollbar.ErrorWithStack(rollbar.ERR, err, rollbar.BuildStack(0))
		}
	}()

	return true, response.StatusCode
}
