package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os/user"
	"path/filepath"
	"sort"
	"time"

	rpc "code.justin.tv/amzn/TwitchAutoprofLambdaTwirp"
	ap "code.justin.tv/video/autoprof-client"
	"github.com/golang/protobuf/ptypes"
	"github.com/twitchtv/twirp"
)

const (
	apURL           = "https://autoprof.video.xarth.tv"
	bundleSizeLimit = 100 << 20
)

type client struct {
	http    *http.Client
	bundles rpc.BundleCollection
}

func newBundleCollectionClient() (*client, error) {
	var c client
	c.http = &http.Client{Transport: &http.Transport{}}

	u, err := user.Current()
	if err != nil {
		return nil, err
	}

	cookie, err := ioutil.ReadFile(filepath.Join(u.HomeDir, ".midway/cookie"))
	if err != nil {
		return nil, err
	}

	c.bundles, err = ap.NewBundleCollectionMidwayClient(apURL, c.http, cookie)
	if err != nil {
		return nil, err
	}

	return &c, nil
}

func (c *client) ListApps(ctx context.Context, start, end time.Time) ([]string, error) {
	var err error
	var listReq rpc.ListApplicationsRequest

	listReq.StartTime, err = ptypes.TimestampProto(start)
	if err != nil {
		return nil, fmt.Errorf("ptypes.TimestampProto(start): %w", err)
	}
	listReq.EndTime, err = ptypes.TimestampProto(end)
	if err != nil {
		return nil, fmt.Errorf("ptypes.TimestampProto(end): %w", err)
	}

	appSet := make(map[string]struct{})
	for {
		listResp, err := c.bundles.ListApplications(ctx, &listReq)
		if err != nil {
			return nil, fmt.Errorf("autoprof.ListApplications: %w", err)
		}
		for _, app := range listResp.GetApplications() {
			appSet[app.GetName()] = struct{}{}
		}

		listReq.PageToken = listResp.GetNextPageToken()
		if listReq.PageToken == "" {
			break
		}
	}

	apps := make([]string, 0, len(appSet))
	for app := range appSet {
		apps = append(apps, app)
	}
	sort.Strings(apps)

	return apps, nil
}

func (c *client) ListProcesses(ctx context.Context, app string, start, end time.Time) (map[string][]*rpc.Bundle, error) {
	var err error
	var listReq rpc.ListBundlesRequest

	listReq.StartTime, err = ptypes.TimestampProto(start)
	if err != nil {
		return nil, fmt.Errorf("ptypes.TimestampProto(start): %w", err)
	}
	listReq.EndTime, err = ptypes.TimestampProto(end)
	if err != nil {
		return nil, fmt.Errorf("ptypes.TimestampProto(end): %w", err)
	}

	listReq.AppName = app

	procs := make(map[string][]*rpc.Bundle)
	for {
		listResp, err := c.bundles.ListBundles(ctx, &listReq)
		if err != nil {
			if te, ok := err.(twirp.Error); ok && te.Code() == twirp.NoError {
				// The Twirp SDK should not return an error that claims to not
				// be an error, but somehow it does. Ignore for now.
				return nil, nil
			}
			return nil, fmt.Errorf("autoprof.ListBundles: %w", err)
		}

		for _, bundle := range listResp.GetBundles() {
			pid := bundle.GetProcessId() // TODO: the API doesn't set this...
			procs[pid] = append(procs[pid], bundle)
		}

		listReq.PageToken = listResp.GetNextPageToken()
		if listReq.PageToken == "" {
			break
		}
	}

	for _, bundles := range procs {
		sort.Slice(bundles, func(i, j int) bool {
			if ti, tj := bundles[i].GetCaptureTime().GetSeconds(), bundles[j].GetCaptureTime().GetSeconds(); ti != tj {
				return ti < tj
			}
			if ti, tj := bundles[i].GetCaptureTime().GetNanos(), bundles[j].GetCaptureTime().GetNanos(); ti != tj {
				return ti < tj
			}
			return false
		})
	}

	return procs, nil
}

func (c *client) DownloadBundle(ctx context.Context, name string) ([]byte, error) {
	var err error
	var bundleReq rpc.GetBundleRequest

	bundleReq.Name = name
	bundleResp, err := c.bundles.GetBundle(ctx, &bundleReq)
	if err != nil {
		return nil, fmt.Errorf("autoprof.GetBundle: %w", err)
	}

	getReq, err := http.NewRequestWithContext(ctx, "GET", bundleResp.GetDetail().GetS3PresignUri(), nil)
	if err != nil {
		return nil, fmt.Errorf("NewRequestWithContext: %w", err)
	}
	for k, v := range bundleResp.GetDetail().GetS3PresignHeaders() {
		getReq.Header.Set(k, v)
	}

	getResp, err := c.http.Do(getReq)
	if err != nil {
		return nil, fmt.Errorf("http.Client.Do: %w", err)
	}
	defer getResp.Body.Close()

	body, err := ioutil.ReadAll(io.LimitReader(getResp.Body, bundleSizeLimit))
	if err != nil {
		return nil, fmt.Errorf("ReadAll: %w", err)
	}

	n, _ := getResp.Body.Read(make([]byte, 1))
	if n > 0 {
		return nil, errors.New("bundle is too large to read")
	}

	return body, nil
}
