// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// Package buildbucket provides helpers for interfacing with build-bucket
package buildbucket

import (
	"context"
	"fmt"
	"log"
	"net/url"

	"dawn.googlesource.com/dawn/tools/src/gerrit"
	"dawn.googlesource.com/dawn/tools/src/utils"
	"go.chromium.org/luci/auth"
	bbpb "go.chromium.org/luci/buildbucket/proto"
	bbgrpcpb "go.chromium.org/luci/buildbucket/proto/grpcpb"
	"go.chromium.org/luci/grpc/prpc"
	"go.chromium.org/luci/hardcoded/chromeinfra"
)

// Buildbucket is the client to communicate with Buildbucket.
type Buildbucket struct {
	client bbgrpcpb.BuildsClient
}

// Builder describes a buildbucket builder
type Builder struct {
	Project string
	Bucket  string
	Builder string
}

// BuildID is a unique identifier of a build
type BuildID int64

// Build describes a buildbucket build
type Build struct {
	ID      BuildID
	Status  BuildStatus
	Builder Builder
}

// BuildStatus is the status of a build
type BuildStatus string

// Enumerator values for BuildStatus
const (
	// Unspecified state. Meaning depends on the context.
	StatusUnknown BuildStatus = "unknown"
	// Build was scheduled, but did not start or end yet.
	StatusScheduled BuildStatus = "scheduled"
	// Build/step has started.
	StatusStarted BuildStatus = "started"
	// A build/step ended successfully.
	// This is a terminal status. It may not transition to another status.
	StatusSuccess BuildStatus = "success"
	// A build/step ended unsuccessfully due to its Build.Input,
	// e.g. tests failed, and NOT due to a build infrastructure failure.
	// This is a terminal status. It may not transition to another status.
	StatusFailure BuildStatus = "failure"
	// A build/step ended unsuccessfully due to a failure independent of the
	// input, e.g. swarming failed, not enough capacity or the recipe was unable
	// to read the patch from gerrit.
	// start_time is not required for this status.
	// This is a terminal status. It may not transition to another status.
	StatusInfraFailure BuildStatus = "infra-failure"
	// A build was cancelled explicitly, e.g. via an RPC.
	// This is a terminal status. It may not transition to another status.
	StatusCanceled BuildStatus = "canceled"
)

// Running returns true if the build is still running
func (s BuildStatus) Running() bool {
	switch s {
	case StatusScheduled, StatusStarted:
		return true
	default:
		return false
	}
}

// pb returns a protobuf BuilderID constructed from the Builder
func (b Builder) pb() *bbpb.BuilderID {
	return &bbpb.BuilderID{
		Project: b.Project,
		Bucket:  b.Bucket,
		Builder: b.Builder,
	}
}

// toBuilder returns a Builder constructed from the protobuf BuilderID
func toBuilder(b *bbpb.BuilderID) Builder {
	return Builder{
		Project: b.Project,
		Bucket:  b.Bucket,
		Builder: b.Builder,
	}
}

// gerritChange returns the protobuf GerritChange from a gerrit.Patchset
func gerritChange(ps gerrit.Patchset) *bbpb.GerritChange {
	host := ps.Host
	if u, err := url.Parse(ps.Host); err == nil && u.Host != "" {
		host = u.Host // Strip scheme from URL
	}
	return &bbpb.GerritChange{
		Host:     host,
		Project:  ps.Project,
		Change:   int64(ps.Change),
		Patchset: int64(ps.Patchset),
	}
}

// toBuildStatus returns a BuildStatus from a protobuf Status
func toBuildStatus(s bbpb.Status) BuildStatus {
	switch s {
	default:
		return StatusUnknown
	case bbpb.Status_SCHEDULED:
		return StatusScheduled
	case bbpb.Status_STARTED:
		return StatusStarted
	case bbpb.Status_SUCCESS:
		return StatusSuccess
	case bbpb.Status_FAILURE:
		return StatusFailure
	case bbpb.Status_INFRA_FAILURE:
		return StatusInfraFailure
	case bbpb.Status_CANCELED:
		return StatusCanceled
	}
}

// toBuild returns a Build from a protobuf Build
func toBuild(b *bbpb.Build) Build {
	return Build{BuildID(b.Id), toBuildStatus(b.Status), toBuilder(b.Builder)}
}

// New creates a client to communicate with Buildbucket.
func New(ctx context.Context, credentials auth.Options) (*Buildbucket, error) {
	http, err := auth.NewAuthenticator(ctx, auth.InteractiveLogin, credentials).Client()
	if err != nil {
		return nil, err
	}
	client := bbgrpcpb.NewBuildsClient(
		&prpc.Client{
			C:       http,
			Host:    chromeinfra.BuildbucketHost,
			Options: prpc.DefaultOptions(),
		})

	return &Buildbucket{client}, nil
}

// SearchBuilds queries the list of builds performed for the given gerrit change.
func (r *Buildbucket) SearchBuilds(ctx context.Context, ps gerrit.Patchset, f func(Build) error) error {
	pageToken := ""
	for {
		rsp, err := r.client.SearchBuilds(ctx, &bbpb.SearchBuildsRequest{
			Predicate: &bbpb.BuildPredicate{
				GerritChanges: []*bbpb.GerritChange{gerritChange(ps)},
			},
			PageSize:  1000, // Maximum page size.
			PageToken: pageToken,
		})
		if err != nil {
			return err
		}

		for _, res := range rsp.Builds {
			if err := f(toBuild(res)); err != nil {
				return err
			}
		}

		pageToken = rsp.GetNextPageToken()
		if pageToken == "" {
			// No more test variants with unexpected result.
			break
		}
	}

	return nil
}

// StartBuild starts a build.
func (r *Buildbucket) StartBuild(
	ctx context.Context,
	ps gerrit.Patchset,
	builder Builder,
	parentSwarmingRunID string,
	forceBuild bool) (Build, error) {

	id := ""
	if !forceBuild {
		id = utils.Hash(ps, builder)
	}

	req := &bbpb.ScheduleBuildRequest{
		RequestId:     id,
		Builder:       builder.pb(),
		GerritChanges: []*bbpb.GerritChange{gerritChange(ps)},
	}
	if parentSwarmingRunID != "" {
		req.Swarming = &bbpb.ScheduleBuildRequest_Swarming{
			ParentRunId: parentSwarmingRunID,
		}
	}

	build, err := r.client.ScheduleBuild(ctx, req)
	if err != nil {
		return Build{}, fmt.Errorf("failed to start build for patchset %+v on builder %+v: %w", ps, builder, err)
	}

	if status := toBuildStatus(build.Status); !forceBuild && !status.Running() {
		log.Printf("ScheduleBuild() returned with %v, attempting to force a retry...\n", status)
		return r.StartBuild(ctx, ps, builder, parentSwarmingRunID, true)
	}

	return toBuild(build), nil
}

// QueryBuild queries the status of a build.
func (r *Buildbucket) QueryBuild(ctx context.Context, id BuildID) (Build, error) {
	b, err := r.client.GetBuild(ctx, &bbpb.GetBuildRequest{Id: int64(id)})
	if err != nil {
		return Build{}, fmt.Errorf("failed to query build with id %v: %w", id, err)
	}
	return toBuild(b), nil
}
