package vods

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"math"
	"net/http"
	"strconv"
	"sync"
	"time"

	"code.justin.tv/video/spectre/swift"
	"code.justin.tv/video/spectre/util"

	"github.com/stvp/rollbar"
)

var ErrEmptyManifest = errors.New("No chunks in manifest")

// Vod is a vod object, which can be played in a playlist
type Vod struct {
	ID         int
	Manifests  map[string]*swift.Manifest
	Properties Properties
}

// Properties is metadata for a vod object
type Properties struct {
	ID            int                      `json:"id"`
	OwnerID       int                      `json:"owner_id"`
	Channel       string                   `json:"channel"`
	Title         string                   `json:"title"`
	Duration      float64                  `json:"duration"`
	Game          string                   `json:"game"`
	BroadcastType string                   `json:"broadcast_type"`
	URI           string                   `json:"uri"`
	Origin        string                   `json:"origin"`
	Formats       map[string]interface{}   `json:"formats"`
	Manifest      string                   `json:"manifest"`
	Thumbnails    []map[string]interface{} `json:"thumbnails"`
}

// InitializeVods initializes all vods in a list
func InitializeVod(channelID int, vodIDs []int, index int) (*Vod, int) {
	for i := index; i < index+len(vodIDs); i++ {
		vodID := vodIDs[i%len(vodIDs)]
		vod := Vod{ID: vodID}

		response, err := http.Get(util.VodAPIURL() + "/" + strconv.Itoa(vodID))
		if err != nil {
			// Videoshim network error. Abort out of this individual vod for now.
			continue
		}
		defer response.Body.Close()
		if response.StatusCode == 404 {
			continue
		}
		body, err := ioutil.ReadAll(response.Body)

		err = json.Unmarshal(body, &vod.Properties)
		if err != nil {
			rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
			continue
		}
		if vod.Properties.OwnerID != channelID {
			continue
		}

		vod.Properties = repairVodPropertiesManifest(vod.Properties)
		vod.Manifests, err = initializeManifests(vod.Properties)
		if err != nil {
			continue
		}

		return &vod, i
	}
	return nil, -1
}

// Historically, vod.manifest was only set on vods if the value deviated from the default.
// Nowadays vod.manifest is guaranteed to be set, but we still have to handle cases where vod.manifest is unset
func repairVodPropertiesManifest(p Properties) Properties {
	if p.Manifest != "" {
		return p
	}

	switch p.BroadcastType {
	case "archive":
		p.Manifest = "index-dvr.m3u8"
	case "highlight":
		p.Manifest = fmt.Sprintf("highlight-%v.m3u8", p.ID)
	}
	return p
}

func initializeManifests(p Properties) (map[string]*swift.Manifest, error) {
	manifests := map[string]*swift.Manifest{}
	// different formats' manifests can vary slightly in duration
	// we must trim formats' chunks if they exceed the shortest formats' duration
	shortestManifestLen := math.MaxInt64
	var shortestManifestDuration time.Duration

	numFormats := len(p.Formats)
	manifestArray := make([]*swift.Manifest, numFormats)

	var wg sync.WaitGroup
	i := 0

	for format := range p.Formats {
		wg.Add(1)
		go func(i int, format string) {
			defer wg.Done()
			var err error
			manifestArray[i], err = swift.GetManifest(p.ID, p.URI, p.Origin, format, p.Manifest)
			if err != nil {
				manifestArray[i] = nil
			}
		}(i, format)
		i++
	}
	wg.Wait()

	for _, manifest := range manifestArray {
		if manifest == nil || len(manifest.Chunks) == 0 {
			return nil, ErrEmptyManifest
		}

		if shortestManifestLen > len(manifest.Chunks) {
			shortestManifestLen = len(manifest.Chunks)
			shortestManifestDuration = manifest.Duration
		}
		manifests[manifest.Format] = manifest
	}

	for format := range manifests {
		manifests[format].Chunks = manifests[format].Chunks[0:shortestManifestLen]
		manifests[format].Duration = shortestManifestDuration
	}
	return manifests, nil
}

// MarshalJSON marshals a vod object
func (v Vod) MarshalJSON() ([]byte, error) {
	j, err := json.Marshal(map[string]interface{}{
		"id":                 v.ID,
		"duration":           v.Manifests["chunked"].Duration.Seconds(),
		"transition_segment": nil,
	})
	if err != nil {
		rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
	}
	return j, err
}
