package youtube

import (
	"encoding/json"
	"fmt"
	"time"

	"google.golang.org/api/youtube/v3"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"

	"code.justin.tv/common/config"
	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/web/users-service/client"
	"github.com/cactus/go-statsd-client/statsd"

	"code.justin.tv/identity/connections/client"
)

const (
	//StatusDown is a status codes to be used as a parameter to SetYoutubeAnnotationsStatus
	StatusDown = 0
	//StatusUp is a status code to be used as a parameter to SetYoutubeAnnotationsStatus
	StatusUp = 1

	promoOffsetMS   = 3000
	promoDurationMS = 10000

	offsetFromStartTimingType = "offsetFromStart"
)

type yt struct {
	connections connections.Client
	users       usersservice.Client

	clientID     string
	clientSecret string

	stats statsd.Statter
}

func init() {
	config.Register(map[string]string{
		"youtube-client-id":     "",
		"youtube-client-secret": "",
		"connections-url":       "http://connections-dev.us-west-2.elasticbeanstalk.com",
		"users-url":             "http://users-service-staging.us-west2.twitch.tv",
	})
}

func New() (*yt, error) {
	connectionsClient, err := connections.NewClient(twitchhttp.ClientConf{
		Host:           config.MustResolve("connections-url"),
		TimingXactName: "connections",
		Stats:          config.Statsd(),
		Transport: twitchhttp.TransportConf{
			MaxIdleConnsPerHost: 10,
		},
	})
	if err != nil {
		return nil, err
	}

	usersClient, err := usersservice.NewClient(twitchhttp.ClientConf{
		Host:           config.MustResolve("users-url"),
		TimingXactName: "users-service",
		Stats:          config.Statsd(),
		Transport: twitchhttp.TransportConf{
			MaxIdleConnsPerHost: 10,
		},
	})
	if err != nil {
		return nil, err
	}

	return &yt{
		connections:  connectionsClient,
		users:        usersClient,
		clientID:     config.MustResolve("youtube-client-id"),
		clientSecret: config.MustResolve("youtube-client-secret"),
		stats:        config.Statsd(),
	}, nil
}

// newClient returns a client for the youtube api
func (T *yt) newClient(ctx context.Context, userID string) (client, error) {
	config := oauth2.Config{
		ClientID:     T.clientID,
		ClientSecret: T.clientSecret,
		Endpoint:     google.Endpoint,
		RedirectURL:  "http://localhost",
		Scopes:       []string{},
	}

	user, err := T.connections.GetYoutubeUser(ctx, userID, nil)
	if err != nil {
		return nil, err
	}

	client := config.Client(ctx, &oauth2.Token{
		AccessToken:  user.Token,
		RefreshToken: user.RefreshToken,
		Expiry:       time.Unix(user.ExpiresOn, 0),
	})

	service, err := youtube.New(client)
	return &ytClient{
		service,
	}, err
}

// GetYoutubeAnnotations returns the current annotations for a user
func (T *yt) GetYoutubeAnnotations(ctx context.Context, userID string) (*youtube.InvideoPromotion, error) {
	client, err := T.newClient(ctx, userID)
	if err != nil {
		if err == connections.ErrNoConnection {
			return nil, nil
		}
		return nil, fmt.Errorf("get client error: %v", err)
	}

	return getYoutubeAnnotations(client, userID)
}

// getYoutubeAnnotations gets current annotations for a user
// This function takes in a client so it is more easily testable
func getYoutubeAnnotations(client client, userID string) (*youtube.InvideoPromotion, error) {
	channel, err := getYoutubeChannel(client)
	if err != nil {
		return nil, fmt.Errorf("get youtube channel error: %v", err)
	}

	return channel.InvideoPromotion, nil
}

// ClearYoutubeAnnotations clears all youtube live annotations for the user.
func (T *yt) ClearYoutubeAnnotations(ctx context.Context, userID string) error {
	client, err := T.newClient(ctx, userID)
	if err != nil {
		if err == connections.ErrNoConnection {
			return nil
		}
		return fmt.Errorf("get client error: %v", err)
	}

	return clearYoutubeAnnotations(client, userID)
}

// clearYoutubeAnnotations clears all youtube live annotations for the user.
// This function takes in a client so it is more easily testable
func clearYoutubeAnnotations(client client, userID string) error {
	channel, err := getYoutubeChannel(client)
	if err != nil {
		return fmt.Errorf("get youtube channel error: %v", err)
	}

	return updateYoutubeChannel(client, channel.Id, []*youtube.PromotedItem{})
}

// SetYoutubeAnnotationsStatus sets the status of a single user's annotations.
// - status should be one of StatusUp or StatusDown
func (T *yt) SetYoutubeAnnotationsStatus(ctx context.Context, userID string, status int) error {
	client, err := T.newClient(ctx, userID)
	if err != nil {
		if err == connections.ErrNoConnection {
			return nil
		}
		return fmt.Errorf("get client error: %v", err)
	}

	user, err := T.users.GetUserByID(ctx, userID, nil)
	if err != nil {
		return err
	}

	if user.Login == nil {
		return nil
	}

	return setYoutubeAnnotationsStatus(client, *user.Login, status)
}

// setYoutubeAnnotationsStatus sets the status of a single user's annotations.
// This function takes in a client so it is more easily testable
func setYoutubeAnnotationsStatus(client client, login string, status int) error {
	channel, err := getYoutubeChannel(client)
	if err != nil {
		return fmt.Errorf("get youtube channel error: %v", err)
	}

	if status == StatusDown {
		// check if we have annotations enabled before doing any more work
		if !twitchAnnotationsEnabled(channel) {
			return nil
		}
	}

	items, err := cleanPromotionItems(client, channel.InvideoPromotion)
	if err != nil {
		return err
	}

	if status == StatusUp {
		shiftPromotions(items)

		items = append(items, createTwitchPromotion(login))
	}

	return updateYoutubeChannel(client, channel.Id, items)
}

// cleanPromotionsItems returns the promotion items of `promotions` stripped of the following:
// - twitch promotions
// - video promotions with invalid videos
func cleanPromotionItems(client client, promotions *youtube.InvideoPromotion) ([]*youtube.PromotedItem, error) {
	cleanedPromotions := []*youtube.PromotedItem{}
	if promotions != nil {
		for _, item := range promotions.Items {
			if isTwitchPromotion(item) {
				continue
			}
			if item.Id == nil {
				continue
			}
			if item.Id.Type == "video" {
				exists, err := client.VideoExists(item.Id.VideoId)
				if err != nil {
					return nil, err
				}
				if !exists {
					continue
				}
			}
			cleanedPromotions = append(cleanedPromotions, item)
		}
	}
	return cleanedPromotions, nil
}

// helper functions

func getYoutubeChannel(client client) (*youtube.Channel, error) {
	listResponse, err := client.ListChannels()
	if err != nil {
		return nil, err
	}

	if len(listResponse.Items) == 0 || listResponse.Items[0] == nil {
		return nil, fmt.Errorf("Error retrieving channel")
	}

	channel := listResponse.Items[0]
	return channel, nil
}

func updateYoutubeChannel(client client, youtubeChannelID string, promotions []*youtube.PromotedItem) error {
	updatedChannel := &youtube.Channel{
		Id: youtubeChannelID,
		InvideoPromotion: &youtube.InvideoPromotion{
			DefaultTiming: &youtube.InvideoTiming{
				DurationMs: promoDurationMS,
				OffsetMs:   promoOffsetMS,
				Type:       offsetFromStartTimingType,
			},
			Items: promotions,
		},
	}

	err := client.UpdateChannel(updatedChannel)
	if err != nil {
		b, _ := json.Marshal(promotions)
		return fmt.Errorf("update channel error: %v\nData: %v", err, string(b))
	}

	return nil
}
