package metrics

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/golang/protobuf/ptypes"
	gh "github.com/google/go-github/github"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"

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

var (
	pullRequestMapStartTime = time.Unix(1, 0)
)

type pullRequestOpenPeriod struct {
	Opened time.Time
	Closed time.Time
}

func (p *pullRequestOpenPeriod) isOpenDuring(t *pb.TimeRange) (bool, error) {
	startts, err := ptypes.Timestamp(t.GetStart())
	if err != nil {
		return false, err
	}
	endts, err := ptypes.Timestamp(t.GetEnd())
	if err != nil {
		return false, err
	}
	return p.Opened.Before(endts) && !p.Closed.Before(startts), nil
}

type pullRequestMap struct {
	sync.RWMutex
	m           map[string]*pullRequestOpenPeriod
	eventServer pb.EventServiceServer
}

func newPullRequestMap(eventServer pb.EventServiceServer) *pullRequestMap {
	return &pullRequestMap{
		m:           make(map[string]*pullRequestOpenPeriod),
		eventServer: eventServer,
	}
}

func (p *pullRequestMap) Iter() <-chan *pullRequestOpenPeriod {
	ch := make(chan *pullRequestOpenPeriod)
	go func() {
		for _, elem := range p.m {
			ch <- elem
		}
		close(ch)
	}()
	return ch
}

func (p *pullRequestMap) key(pullRequestEvent *gh.PullRequestEvent) string {
	return pullRequestEvent.PullRequest.GetURL()
}

func (p *pullRequestMap) update(pullRequestEvent *gh.PullRequestEvent) {
	key := p.key(pullRequestEvent)
	openedAt := pullRequestEvent.PullRequest.GetCreatedAt()
	closedAt := pullRequestEvent.PullRequest.GetClosedAt()
	if closedAt.IsZero() {
		closedAt = time.Now()
	}
	p.Lock()
	defer p.Unlock()
	entry, ok := p.m[key]
	if !ok {
		p.m[key] = &pullRequestOpenPeriod{
			Opened: openedAt,
			Closed: closedAt,
		}
		return
	}
	// We found an earlier time at which this pull request was closed.
	if entry.Closed.After(closedAt) {
		entry.Closed = closedAt
	}
}

func (p *pullRequestMap) updateFromEvents(events []*pb.Event) error {
	for _, event := range events {
		githubEvent, err := getGitHubEvent(event, "pull_request")
		if err != nil {
			return errf(codes.Internal, "Error parsing GitHub event: %v", err)
		}

		switch githubEvent := githubEvent.(type) {
		case *gh.PullRequestEvent:
			if githubEvent.PullRequest != nil {
				p.update(githubEvent)
			}
		}
	}
	return nil
}

func (p *pullRequestMap) queryForPullRequests(ctx context.Context, githubRepo string, action string, end time.Time) ([]*pb.Event, error) {
	startProto, err := ptypes.TimestampProto(pullRequestMapStartTime)
	if err != nil {
		return nil, err
	}
	endProto, err := ptypes.TimestampProto(end)
	if err != nil {
		return nil, err
	}
	queryResp, err := p.eventServer.QueryEvents(ctx, &pb.QueryEventsRequest{
		Timerange: &pb.TimeRange{
			Start: startProto,
			End:   endProto,
		},
		Type: fmt.Sprintf("GitHub-pull_request-%s", action),
		Filters: []*pb.QueryEventsRequest_AttributeFilter{
			&pb.QueryEventsRequest_AttributeFilter{
				Key:   "base_repository",
				Value: githubRepo,
			},
		},
	})
	if err != nil {
		if grpc.Code(err) == codes.NotFound {
			return nil, nil
		}
		return nil, err
	}
	return queryResp.GetEvents(), nil
}

func (p *pullRequestMap) updateRepoFromDatastore(ctx context.Context, githubRepo string, end time.Time) error {
	sem := make(chan error, 2)
	queryAndUpdate := func(ctx context.Context, action string) {
		events, err := p.queryForPullRequests(ctx, githubRepo, action, end)
		if err != nil {
			sem <- err
			return
		}
		sem <- p.updateFromEvents(events)
	}
	go queryAndUpdate(ctx, "opened")
	go queryAndUpdate(ctx, "closed")
	for i := 0; i < 2; i++ {
		err := <-sem
		if err != nil {
			// TODO: cancel the context to stop any still-processing work?
			return err
		}
	}
	return nil
}

func (p *pullRequestMap) UpdateFromDatastore(ctx context.Context, githubRepos []string, end time.Time) error {
	sem := make(chan error, len(githubRepos))
	for _, githubRepo := range githubRepos {
		go func(ctx context.Context, githubRepo string) {
			sem <- p.updateRepoFromDatastore(ctx, githubRepo, end)
		}(ctx, githubRepo)
	}
	for i := 0; i < len(githubRepos); i++ {
		err := <-sem
		if err != nil {
			// TODO: cancel the context to stop any still-processing work?
			return err
		}
	}
	return nil
}
