package metrics

import (
	"bytes"
	"compress/gzip"
	"context"
	"io/ioutil"
	"math"
	"net/http"
	"net/url"
	"time"

	"github.com/golang/protobuf/ptypes"
	gh "github.com/google/go-github/github"
	"github.com/jinzhu/now"
	"github.com/pkg/errors"
	"google.golang.org/grpc/codes"
	"gopkg.in/fatih/set.v0"

	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

type timeSeriesFactory func(time.Time, time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error)

var (
	timeSeriesFactories = map[pb.GetMetricRequest_BucketSize]timeSeriesFactory{
		pb.GetMetricRequest_DAY:     makeDailyTimeSeries,
		pb.GetMetricRequest_WEEK:    makeWeeklyTimeSeries,
		pb.GetMetricRequest_MONTH:   makeMonthlyTimeSeries,
		pb.GetMetricRequest_QUARTER: makeQuarterlyTimeSeries,
		pb.GetMetricRequest_YEAR:    makeYearlyTimeSeries,
	}
)

// Only accurate to the second.
func eventTimestampToTime(t float64) time.Time {
	return time.Unix(int64(t), 0)
}

func makeTimeSeriesEntry(start, end time.Time) (*pb.GetMetricResponse_TimeSeriesEntry, error) {
	startProto, err := ptypes.TimestampProto(start)
	if err != nil {
		return nil, err
	}

	endProto, err := ptypes.TimestampProto(end)
	if err != nil {
		return nil, err
	}

	return &pb.GetMetricResponse_TimeSeriesEntry{
		Timerange: &pb.TimeRange{
			Start: startProto,
			End:   endProto,
		},
	}, nil
}

func makeDailyTimeSeries(start, end time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	start = now.New(start).BeginningOfDay()
	end = now.New(end).EndOfDay()

	day := time.Hour * 24
	numEntries := int(end.Sub(start)/(day)) + 1
	timeSeries := make(
		[]*pb.GetMetricResponse_TimeSeriesEntry, numEntries, numEntries)

	entryStart := start
	for i := range timeSeries {
		entryEnd := now.New(entryStart).EndOfDay()
		entry, err := makeTimeSeriesEntry(entryStart, entryEnd)
		if err != nil {
			return nil, err
		}
		timeSeries[i] = entry
		entryStart = entryStart.AddDate(0, 0, 1)
	}
	return timeSeries, nil
}

func makeWeeklyTimeSeries(start, end time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	start = now.New(start).BeginningOfWeek()
	end = now.New(end).EndOfWeek()

	week := time.Hour * 24 * 7
	numEntries := int(end.Sub(start)/(week)) + 1

	timeSeries := make(
		[]*pb.GetMetricResponse_TimeSeriesEntry, numEntries, numEntries)

	entryStart := start
	for i := range timeSeries {
		entryEnd := now.New(entryStart).EndOfWeek()
		entry, err := makeTimeSeriesEntry(entryStart, entryEnd)
		if err != nil {
			return nil, err
		}
		timeSeries[i] = entry
		entryStart = entryStart.AddDate(0, 0, 7)
	}
	return timeSeries, nil
}

// We use our own function because now.New(...).BeginningOfMonth() has a daylight savings bug.
func beginningOfMonth(t time.Time) time.Time {
	y, m, _ := t.Date()
	return time.Date(y, m, 1, 0, 0, 0, 0, t.Location())
}

func makeMonthlyTimeSeries(start, end time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	start = beginningOfMonth(start)
	end = now.New(end).EndOfMonth()

	startYear, startMonth, _ := start.Date()
	endYear, endMonth, _ := end.Date()
	numEntries := (endYear-startYear)*12 + int(endMonth-startMonth) + 1
	timeSeries := make(
		[]*pb.GetMetricResponse_TimeSeriesEntry, numEntries, numEntries)

	entryStart := start
	for i := range timeSeries {
		entryEnd := now.New(entryStart).EndOfMonth()
		entry, err := makeTimeSeriesEntry(entryStart, entryEnd)
		if err != nil {
			return nil, err
		}
		timeSeries[i] = entry
		entryStart = entryStart.AddDate(0, 1, 0)
	}
	return timeSeries, nil
}

func makeQuarterlyTimeSeries(start, end time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	start = now.New(start).BeginningOfQuarter()
	end = now.New(end).EndOfQuarter()

	startYear, startMonth, _ := start.Date()
	startQuarter := (startMonth - 1) / 3
	endYear, endMonth, _ := end.Date()
	endQuarter := (endMonth - 1) / 3
	numEntries := (endYear-startYear)*4 + int(endQuarter-startQuarter) + 1

	timeSeries := make(
		[]*pb.GetMetricResponse_TimeSeriesEntry, numEntries, numEntries)

	entryStart := start
	for i := range timeSeries {
		entryEnd := now.New(entryStart).EndOfQuarter()
		entry, err := makeTimeSeriesEntry(entryStart, entryEnd)
		if err != nil {
			return nil, err
		}
		timeSeries[i] = entry
		entryStart = entryStart.AddDate(0, 3, 0)
	}
	return timeSeries, nil
}

func makeYearlyTimeSeries(start, end time.Time) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	start = now.New(start).BeginningOfYear()
	end = now.New(end).EndOfYear()

	numEntries := end.Year() - start.Year() + 1

	timeSeries := make(
		[]*pb.GetMetricResponse_TimeSeriesEntry, numEntries, numEntries)

	entryStart := start
	for i := range timeSeries {
		entryEnd := now.New(entryStart).EndOfYear()
		entry, err := makeTimeSeriesEntry(entryStart, entryEnd)
		if err != nil {
			return nil, err
		}
		timeSeries[i] = entry
		entryStart = entryStart.AddDate(1, 0, 0)
	}
	return timeSeries, nil
}

func makeTimeSeries(timerange *pb.TimeRange, bucketSize pb.GetMetricRequest_BucketSize, ianaTimeZone string) ([]*pb.GetMetricResponse_TimeSeriesEntry, error) {
	loc := time.UTC
	if ianaTimeZone != "" {
		var err error
		loc, err = time.LoadLocation(ianaTimeZone)
		if err != nil {
			return nil, errors.Wrapf(err, "Could't load time zone %q", ianaTimeZone)
		}
	}

	start, err := ptypes.Timestamp(timerange.Start)
	if err != nil {
		return nil, err
	}
	start = start.In(loc)
	end, err := ptypes.Timestamp(timerange.End)
	if err != nil {
		return nil, err
	}
	end = end.In(loc)
	return timeSeriesFactories[bucketSize](start, end)
}

func getJenkinsJobsForProject(ctx context.Context, projectMetadataServer pb.ProjectMetadataServiceServer, projectID string) ([]string, error) {
	metadataResp, err := projectMetadataServer.GetMetadata(ctx, &pb.GetMetadataRequest{
		ProjectId: projectID,
	})
	if err != nil {
		return nil, err
	}

	buildServices := metadataResp.GetProject().GetBuildService()
	jenkinsJobs := set.New()
	for _, buildService := range buildServices {
		switch service := buildService.GetService().(type) {
		case *pb.BuildService_JenkinsJob:
			jenkinsJobs.Add(service.JenkinsJob.GetName())
		}
	}

	return set.StringSlice(jenkinsJobs), nil
}

func getGitHubReposForProject(ctx context.Context, projectMetadataServer pb.ProjectMetadataServiceServer, projectID string) ([]string, error) {
	metadataResp, err := projectMetadataServer.GetMetadata(ctx, &pb.GetMetadataRequest{
		ProjectId: projectID,
	})
	if err != nil {
		return nil, err
	}

	sourceRepos := metadataResp.GetProject().GetSourceRepositories()
	githubRepos := set.New()
	for _, sourceRepo := range sourceRepos {
		switch sourceRepo := sourceRepo.GetRepository().(type) {
		case *pb.SourceRepository_GithubRepository:
			repoURL := url.URL{
				Scheme: "https",
				Host:   sourceRepo.GithubRepository.GetHost(),
				Path:   "/" + sourceRepo.GithubRepository.GetName(),
			}
			githubRepos.Add(repoURL.String())
		}
	}

	return set.StringSlice(githubRepos), nil
}

// getGitHubEvent auto-detects whether the GitHub event body is zlib compressed
func getGitHubEvent(event *pb.Event, messageType string) (interface{}, error) {
	json := event.GetBody()

	switch http.DetectContentType(json) {
	case "application/x-gzip":
		r, err := gzip.NewReader(bytes.NewReader(json))
		if err != nil {
			return nil, errf(codes.Internal,
				"Error decompressing GitHub event body: %v", err)
		}
		json, err = ioutil.ReadAll(r)
		if err != nil {
			return nil, errf(codes.Internal,
				"Error decompressing GitHub event body: %v", err)
		}
		_ = r.Close()
	}

	githubEvent, err := gh.ParseWebHook(messageType, json)
	if err != nil {
		return nil, errf(codes.Internal,
			"Error parsing GitHub event: %v", err)
	}
	return githubEvent, nil
}

// Round up if fractional part is >= .5, otherwise round down.
func Round(f float64) float64 {
	return math.Floor(f + .5)
}

// RoundPlus rounds number to given number of fractional decimal digits.
func RoundPlus(f float64, places int) float64 {
	shift := math.Pow(10, float64(places))
	return Round(f*shift) / shift
}
