// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"time"

	"github.com/google/syzkaller/dashboard/dashapi"
	"github.com/google/syzkaller/pkg/email"
	"github.com/google/syzkaller/pkg/html"
	"github.com/google/syzkaller/sys/targets"
	"golang.org/x/net/context"
	db "google.golang.org/appengine/v2/datastore"
	"google.golang.org/appengine/v2/log"
)

// Backend-independent reporting logic.
// Two main entry points:
//  - reportingPoll is called by backends to get list of bugs that need to be reported.
//  - incomingCommand is called by backends to update bug statuses.

const (
	maxMailReportLen           = 64 << 10
	maxInlineError             = 16 << 10
	notifyResendPeriod         = 14 * 24 * time.Hour
	notifyAboutBadCommitPeriod = 90 * 24 * time.Hour
	never                      = 100 * 365 * 24 * time.Hour
	internalError              = "internal error"
	// This is embedded as first line of syzkaller reproducer files.
	syzReproPrefix = "# See https://goo.gl/kgGztJ for information about syzkaller reproducers.\n"
)

// reportingPoll is called by backends to get list of bugs that need to be reported.
func reportingPollBugs(c context.Context, typ string) []*dashapi.BugReport {
	state, err := loadReportingState(c)
	if err != nil {
		log.Errorf(c, "%v", err)
		return nil
	}
	bugs, _, err := loadOpenBugs(c)
	if err != nil {
		log.Errorf(c, "%v", err)
		return nil
	}
	log.Infof(c, "fetched %v bugs", len(bugs))
	sort.Sort(bugReportSorter(bugs))
	var reports []*dashapi.BugReport
	for _, bug := range bugs {
		rep, err := handleReportBug(c, typ, state, bug)
		if err != nil {
			log.Errorf(c, "%v: failed to report bug %v: %v", bug.Namespace, bug.Title, err)
			continue
		}
		if rep == nil {
			continue
		}
		reports = append(reports, rep)
		// Trying to report too many at once is known to cause OOMs.
		// But new bugs appear incrementally and polling is frequent enough,
		// so reporting lots of bugs at once is also not necessary.
		if len(reports) == 3 {
			break
		}
	}
	return reports
}

func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) (
	*dashapi.BugReport, error) {
	reporting, bugReporting, _, _, _, err := needReport(c, typ, state, bug)
	if err != nil || reporting == nil {
		return nil, err
	}
	crash, crashKey, err := findCrashForBug(c, bug)
	if err != nil {
		return nil, err
	} else if crash == nil {
		return nil, fmt.Errorf("no crashes")
	}
	rep, err := createBugReport(c, bug, crash, crashKey, bugReporting, reporting)
	if err != nil {
		return nil, err
	}
	log.Infof(c, "bug %q: reporting to %v", bug.Title, reporting.Name)
	return rep, nil
}

func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (
	reporting *Reporting, bugReporting *BugReporting, reportingIdx int,
	status, link string, err error) {
	reporting, bugReporting, reportingIdx, status, err = currentReporting(bug)
	if err != nil || reporting == nil {
		return
	}
	if typ != "" && typ != reporting.Config.Type() {
		status = "on a different reporting"
		reporting, bugReporting = nil, nil
		return
	}
	link = bugReporting.Link
	if !bugReporting.Reported.IsZero() && bugReporting.ReproLevel >= bug.ReproLevel {
		status = fmt.Sprintf("%v: reported%v on %v",
			reporting.DisplayTitle, reproStr(bugReporting.ReproLevel),
			html.FormatTime(bugReporting.Reported))
		reporting, bugReporting = nil, nil
		return
	}
	ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
	cfg := config.Namespaces[bug.Namespace]
	if timeSince(c, bug.FirstTime) < cfg.ReportingDelay {
		status = fmt.Sprintf("%v: initial reporting delay", reporting.DisplayTitle)
		reporting, bugReporting = nil, nil
		return
	}
	if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
		status = fmt.Sprintf("%v: waiting for C repro", reporting.DisplayTitle)
		reporting, bugReporting = nil, nil
		return
	}
	if !cfg.MailWithoutReport && !bug.HasReport {
		status = fmt.Sprintf("%v: no report", reporting.DisplayTitle)
		reporting, bugReporting = nil, nil
		return
	}
	if bug.NumCrashes == 0 {
		status = fmt.Sprintf("%v: no crashes!", reporting.DisplayTitle)
		reporting, bugReporting = nil, nil
		return
	}

	// Limit number of reports sent per day,
	// but don't limit sending repros to already reported bugs.
	if bugReporting.Reported.IsZero() && ent.Sent >= reporting.DailyLimit {
		status = fmt.Sprintf("%v: out of quota for today", reporting.DisplayTitle)
		reporting, bugReporting = nil, nil
		return
	}

	// Ready to be reported.
	if bugReporting.Reported.IsZero() {
		// This update won't be committed, but it is useful as a best effort measure
		// so that we don't overflow the limit in a single poll.
		ent.Sent++
	}
	status = fmt.Sprintf("%v: ready to report", reporting.DisplayTitle)
	if !bugReporting.Reported.IsZero() {
		status += fmt.Sprintf(" (reported%v on %v)",
			reproStr(bugReporting.ReproLevel), html.FormatTime(bugReporting.Reported))
	}
	return
}

func reportingPollNotifications(c context.Context, typ string) []*dashapi.BugNotification {
	bugs, _, err := loadOpenBugs(c)
	if err != nil {
		log.Errorf(c, "%v", err)
		return nil
	}
	log.Infof(c, "fetched %v bugs", len(bugs))
	var notifs []*dashapi.BugNotification
	for _, bug := range bugs {
		if isDecommissioned(c, bug.Namespace) {
			continue
		}
		notif, err := handleReportNotif(c, typ, bug)
		if err != nil {
			log.Errorf(c, "%v: failed to create bug notif %v: %v", bug.Namespace, bug.Title, err)
			continue
		}
		if notif == nil {
			continue
		}
		notifs = append(notifs, notif)
		if len(notifs) >= 10 {
			break // don't send too many at once just in case
		}
	}
	return notifs
}

func handleReportNotif(c context.Context, typ string, bug *Bug) (*dashapi.BugNotification, error) {
	reporting, bugReporting, _, _, err := currentReporting(bug)
	if err != nil || reporting == nil {
		return nil, nil
	}
	if typ != "" && typ != reporting.Config.Type() {
		return nil, nil
	}
	if bug.Status != BugStatusOpen || bugReporting.Reported.IsZero() {
		return nil, nil
	}
	for _, f := range notificationGenerators {
		notif, err := f(c, bug, reporting, bugReporting)
		if notif != nil || err != nil {
			return notif, err
		}
	}
	return nil, nil
}

var notificationGenerators = []func(context.Context, *Bug, *Reporting,
	*BugReporting) (*dashapi.BugNotification, error){
	// Embargo upstreaming.
	func(c context.Context, bug *Bug, reporting *Reporting,
		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
		if reporting.moderation &&
			reporting.Embargo != 0 &&
			len(bug.Commits) == 0 &&
			bugReporting.OnHold.IsZero() &&
			timeSince(c, bugReporting.Reported) > reporting.Embargo {
			log.Infof(c, "%v: upstreaming (embargo): %v", bug.Namespace, bug.Title)
			return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
		}
		return nil, nil
	},
	// Upstreaming.
	func(c context.Context, bug *Bug, reporting *Reporting,
		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
		if reporting.moderation &&
			len(bug.Commits) == 0 &&
			bugReporting.OnHold.IsZero() &&
			reporting.Filter(bug) == FilterSkip {
			log.Infof(c, "%v: upstreaming (skip): %v", bug.Namespace, bug.Title)
			return createNotification(c, dashapi.BugNotifUpstream, true, "", bug, reporting, bugReporting)
		}
		return nil, nil
	},
	// Obsoleting.
	func(c context.Context, bug *Bug, reporting *Reporting,
		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
		if len(bug.Commits) == 0 &&
			bug.canBeObsoleted(c) &&
			timeSince(c, bug.LastActivity) > notifyResendPeriod &&
			timeSince(c, bug.LastTime) > bug.obsoletePeriod() {
			log.Infof(c, "%v: obsoleting: %v", bug.Namespace, bug.Title)
			why := bugObsoletionReason(bug)
			return createNotification(c, dashapi.BugNotifObsoleted, false, string(why), bug, reporting, bugReporting)
		}
		return nil, nil
	},
	// Bad commit.
	func(c context.Context, bug *Bug, reporting *Reporting,
		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
		if len(bug.Commits) > 0 &&
			len(bug.PatchedOn) == 0 &&
			timeSince(c, bug.LastActivity) > notifyResendPeriod &&
			timeSince(c, bug.FixTime) > notifyAboutBadCommitPeriod {
			log.Infof(c, "%v: bad fix commit: %v", bug.Namespace, bug.Title)
			commits := strings.Join(bug.Commits, "\n")
			return createNotification(c, dashapi.BugNotifBadCommit, true, commits, bug, reporting, bugReporting)
		}
		return nil, nil
	},
	// Label notifications.
	func(c context.Context, bug *Bug, reporting *Reporting,
		bugReporting *BugReporting) (*dashapi.BugNotification, error) {
		for _, label := range bug.Labels {
			if label.SetBy != "" {
				continue
			}
			str := label.String()
			if reporting.Labels[str] == "" {
				continue
			}
			if stringInList(bugReporting.GetLabels(), str) {
				continue
			}
			return createLabelNotification(c, label, bug, reporting, bugReporting)
		}
		return nil, nil
	},
}

func createLabelNotification(c context.Context, label BugLabel, bug *Bug, reporting *Reporting,
	bugReporting *BugReporting) (*dashapi.BugNotification, error) {
	labelStr := label.String()
	notif, err := createNotification(c, dashapi.BugNotifLabel, true, reporting.Labels[labelStr],
		bug, reporting, bugReporting)
	if err != nil {
		return nil, err
	}
	notif.Label = labelStr
	// For some labels also attach job results.
	if label.Label == OriginLabel {
		var err error
		notif.TreeJobs, err = treeTestJobs(c, bug)
		if err != nil {
			log.Errorf(c, "failed to extract jobs for %s: %v", bug.keyHash(), err)
			return nil, fmt.Errorf("failed to fetch jobs: %w", err)
		}
	}
	return notif, nil
}

func bugObsoletionReason(bug *Bug) dashapi.BugStatusReason {
	if bug.HeadReproLevel == ReproLevelNone && bug.ReproLevel != ReproLevelNone {
		return dashapi.InvalidatedByRevokedRepro
	}
	return dashapi.InvalidatedByNoActivity
}

var noObsoletionsKey = "Temporarily disable bug obsoletions"

func contextWithNoObsoletions(c context.Context) context.Context {
	return context.WithValue(c, &noObsoletionsKey, struct{}{})
}

func getNoObsoletions(c context.Context) bool {
	return c.Value(&noObsoletionsKey) != nil
}

// TODO: this is what we would like to do, but we need to figure out
// KMSAN story: we don't do fix bisection on it (rebased),
// do we want to close all old KMSAN bugs with repros?
// For now we only enable this in tests.
var obsoleteWhatWontBeFixBisected = false

func (bug *Bug) canBeObsoleted(c context.Context) bool {
	if getNoObsoletions(c) {
		return false
	}
	if bug.HeadReproLevel == ReproLevelNone {
		return true
	}
	if obsoleteWhatWontBeFixBisected {
		cfg := config.Namespaces[bug.Namespace]
		for _, mgr := range bug.HappenedOn {
			if !cfg.Managers[mgr].FixBisectionDisabled {
				return false
			}
		}
		return true
	}
	return false
}

func (bug *Bug) obsoletePeriod() time.Duration {
	period := never
	if config.Obsoleting.MinPeriod == 0 {
		return period
	}
	// Before we have at least 10 crashes, any estimation of frequency is too imprecise.
	// In such case we conservatively assume it still happens.
	if bug.NumCrashes >= 10 {
		// This is linear extrapolation for when the next crash should happen.
		period = bug.LastTime.Sub(bug.FirstTime) / time.Duration(bug.NumCrashes-1)
		// Let's be conservative with obsoleting too early.
		period *= 100
	}
	min, max := config.Obsoleting.MinPeriod, config.Obsoleting.MaxPeriod
	if config.Obsoleting.NonFinalMinPeriod != 0 &&
		bug.Reporting[len(bug.Reporting)-1].Reported.IsZero() {
		min, max = config.Obsoleting.NonFinalMinPeriod, config.Obsoleting.NonFinalMaxPeriod
	}
	if mgr := bug.managerConfig(); mgr != nil && mgr.ObsoletingMinPeriod != 0 {
		min, max = mgr.ObsoletingMinPeriod, mgr.ObsoletingMaxPeriod
	}
	if period < min {
		period = min
	}
	if period > max {
		period = max
	}
	return period
}

func (bug *Bug) managerConfig() *ConfigManager {
	if len(bug.HappenedOn) != 1 {
		return nil
	}
	mgr := config.Namespaces[bug.Namespace].Managers[bug.HappenedOn[0]]
	return &mgr
}

func createNotification(c context.Context, typ dashapi.BugNotif, public bool, text string, bug *Bug,
	reporting *Reporting, bugReporting *BugReporting) (*dashapi.BugNotification, error) {
	reportingConfig, err := json.Marshal(reporting.Config)
	if err != nil {
		return nil, err
	}
	crash, _, err := findCrashForBug(c, bug)
	if err != nil {
		return nil, err
	}
	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
	if err != nil {
		return nil, err
	}
	kernelRepo := kernelRepoInfo(c, build)
	notif := &dashapi.BugNotification{
		Type:      typ,
		Namespace: bug.Namespace,
		Config:    reportingConfig,
		ID:        bugReporting.ID,
		ExtID:     bugReporting.ExtID,
		Title:     bug.displayTitle(),
		Text:      text,
		Public:    public,
		Link:      fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
		CC:        kernelRepo.CC.Always,
	}
	if public {
		notif.Maintainers = append(crash.Maintainers, kernelRepo.CC.Maintainers...)
	}
	if (public || reporting.moderation) && bugReporting.CC != "" {
		notif.CC = append(notif.CC, strings.Split(bugReporting.CC, "|")...)
	}
	if mgr := bug.managerConfig(); mgr != nil {
		notif.CC = append(notif.CC, mgr.CC.Always...)
		if public {
			notif.Maintainers = append(notif.Maintainers, mgr.CC.Maintainers...)
		}
	}
	return notif, nil
}

func currentReporting(bug *Bug) (*Reporting, *BugReporting, int, string, error) {
	if bug.NumCrashes == 0 {
		// This is possible during the short window when we already created a bug,
		// but did not attach the first crash to it yet. We need to avoid reporting this bug yet
		// and wait for the crash. Otherwise reporting filter may mis-classify it as e.g.
		// not having a report or something else.
		return nil, nil, 0, "no crashes yet", nil
	}
	for i := range bug.Reporting {
		bugReporting := &bug.Reporting[i]
		if !bugReporting.Closed.IsZero() {
			continue
		}
		reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
		if reporting == nil {
			return nil, nil, 0, "", fmt.Errorf("%v: missing in config", bugReporting.Name)
		}
		if reporting.DailyLimit == 0 {
			return nil, nil, 0, fmt.Sprintf("%v: reporting has daily limit 0", reporting.DisplayTitle), nil
		}
		switch reporting.Filter(bug) {
		case FilterSkip:
			if bugReporting.Reported.IsZero() {
				continue
			}
			fallthrough
		case FilterReport:
			return reporting, bugReporting, i, "", nil
		case FilterHold:
			return nil, nil, 0, fmt.Sprintf("%v: reporting suspended", reporting.DisplayTitle), nil
		}
	}
	return nil, nil, 0, "", fmt.Errorf("no reporting left")
}

func reproStr(level dashapi.ReproLevel) string {
	switch level {
	case ReproLevelSyz:
		return " syz repro"
	case ReproLevelC:
		return " C repro"
	default:
		return ""
	}
}

// nolint: gocyclo
func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
	var job *Job
	if bug.BisectCause == BisectYes || bug.BisectCause == BisectInconclusive || bug.BisectCause == BisectHorizont {
		// If we have bisection results, report the crash/repro used for bisection.
		causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
		if err != nil {
			return nil, err
		}
		if causeBisect != nil {
			err := causeBisect.loadCrash(c)
			if err != nil {
				return nil, err
			}
		}
		// If we didn't check whether the bisect is unreliable, even though it would not be
		// reported anyway, we could still eventually Cc people from those commits later
		// (e.g. when we did bisected with a syz repro and then notified about a C repro).
		if causeBisect != nil && !causeBisect.job.isUnreliableBisect() {
			job = causeBisect.job
			if causeBisect.crash.ReproC != 0 || crash.ReproC == 0 {
				// Don't override the crash in this case,
				// otherwise we will always think that we haven't reported the C repro.
				crash, crashKey = causeBisect.crash, causeBisect.crashKey
			}
		}
	}
	rep, err := crashBugReport(c, bug, crash, crashKey, bugReporting, reporting)
	if err != nil {
		return nil, err
	}
	if job != nil {
		cause, emails := bisectFromJob(c, job)
		rep.BisectCause = cause
		rep.Maintainers = append(rep.Maintainers, emails...)
	}
	return rep, nil
}

// crashBugReport fills in crash and build related fields into *dashapi.BugReport.
func crashBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
	bugReporting *BugReporting, reporting *Reporting) (*dashapi.BugReport, error) {
	reportingConfig, err := json.Marshal(reporting.Config)
	if err != nil {
		return nil, err
	}
	crashLog, _, err := getText(c, textCrashLog, crash.Log)
	if err != nil {
		return nil, err
	}
	report, _, err := getText(c, textCrashReport, crash.Report)
	if err != nil {
		return nil, err
	}
	if len(report) > maxMailReportLen {
		report = report[:maxMailReportLen]
	}
	reproC, _, err := getText(c, textReproC, crash.ReproC)
	if err != nil {
		return nil, err
	}
	reproSyz, err := loadReproSyz(c, crash)
	if err != nil {
		return nil, err
	}
	machineInfo, _, err := getText(c, textMachineInfo, crash.MachineInfo)
	if err != nil {
		return nil, err
	}
	build, err := loadBuild(c, bug.Namespace, crash.BuildID)
	if err != nil {
		return nil, err
	}
	typ := dashapi.ReportNew
	if !bugReporting.Reported.IsZero() {
		typ = dashapi.ReportRepro
	}
	assetList := createAssetList(build, crash)
	kernelRepo := kernelRepoInfo(c, build)
	rep := &dashapi.BugReport{
		Type:            typ,
		Config:          reportingConfig,
		ExtID:           bugReporting.ExtID,
		First:           bugReporting.Reported.IsZero(),
		Moderation:      reporting.moderation,
		Log:             crashLog,
		LogLink:         externalLink(c, textCrashLog, crash.Log),
		LogHasStrace:    dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
		Report:          report,
		ReportLink:      externalLink(c, textCrashReport, crash.Report),
		CC:              kernelRepo.CC.Always,
		Maintainers:     append(crash.Maintainers, kernelRepo.CC.Maintainers...),
		ReproC:          reproC,
		ReproCLink:      externalLink(c, textReproC, crash.ReproC),
		ReproSyz:        reproSyz,
		ReproSyzLink:    externalLink(c, textReproSyz, crash.ReproSyz),
		ReproOpts:       crash.ReproOpts,
		MachineInfo:     machineInfo,
		MachineInfoLink: externalLink(c, textMachineInfo, crash.MachineInfo),
		CrashID:         crashKey.IntID(),
		CrashTime:       crash.Time,
		NumCrashes:      bug.NumCrashes,
		HappenedOn:      managersToRepos(c, bug.Namespace, bug.HappenedOn),
		Manager:         crash.Manager,
		Assets:          assetList,
		ReportElements:  &dashapi.ReportElements{GuiltyFiles: crash.ReportElements.GuiltyFiles},
	}
	if bugReporting.CC != "" {
		rep.CC = append(rep.CC, strings.Split(bugReporting.CC, "|")...)
	}
	if build.Type == BuildFailed {
		rep.Maintainers = append(rep.Maintainers, kernelRepo.CC.BuildMaintainers...)
	}
	if mgr := bug.managerConfig(); mgr != nil {
		rep.CC = append(rep.CC, mgr.CC.Always...)
		rep.Maintainers = append(rep.Maintainers, mgr.CC.Maintainers...)
		if build.Type == BuildFailed {
			rep.Maintainers = append(rep.Maintainers, mgr.CC.BuildMaintainers...)
		}
	}
	for _, label := range bug.Labels {
		text, ok := reporting.Labels[label.String()]
		if !ok {
			continue
		}
		if rep.LabelMessages == nil {
			rep.LabelMessages = map[string]string{}
		}
		rep.LabelMessages[label.String()] = text
	}
	if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil {
		return nil, err
	}
	return rep, nil
}

func loadReproSyz(c context.Context, crash *Crash) ([]byte, error) {
	reproSyz, _, err := getText(c, textReproSyz, crash.ReproSyz)
	if err != nil || len(reproSyz) == 0 {
		return nil, err
	}
	buf := new(bytes.Buffer)
	buf.WriteString(syzReproPrefix)
	if len(crash.ReproOpts) != 0 {
		fmt.Fprintf(buf, "#%s\n", crash.ReproOpts)
	}
	buf.Write(reproSyz)
	return buf.Bytes(), nil
}

// fillBugReport fills common report fields for bug and job reports.
func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting,
	build *Build) error {
	kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig)
	if err != nil {
		return err
	}
	creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID)
	if err != nil {
		return err
	}
	rep.BugStatus, err = bug.dashapiStatus()
	if err != nil {
		return err
	}
	rep.Namespace = bug.Namespace
	rep.ID = bugReporting.ID
	rep.Title = bug.displayTitle()
	rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID)
	rep.CreditEmail = creditEmail
	rep.OS = build.OS
	rep.Arch = build.Arch
	rep.VMArch = build.VMArch
	rep.UserSpaceArch = kernelArch(build.Arch)
	rep.BuildID = build.ID
	rep.BuildTime = build.Time
	rep.CompilerID = build.CompilerID
	rep.KernelRepo = build.KernelRepo
	rep.KernelRepoAlias = kernelRepoInfo(c, build).Alias
	rep.KernelBranch = build.KernelBranch
	rep.KernelCommit = build.KernelCommit
	rep.KernelCommitTitle = build.KernelCommitTitle
	rep.KernelCommitDate = build.KernelCommitDate
	rep.KernelConfig = kernelConfig
	rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig)
	rep.SyzkallerCommit = build.SyzkallerCommit
	rep.NoRepro = build.Type == BuildFailed
	for _, item := range bug.LabelValues(SubsystemLabel) {
		rep.Subsystems = append(rep.Subsystems, dashapi.BugSubsystem{
			Name:  item.Value,
			SetBy: item.SetBy,
			Link:  fmt.Sprintf("%v/%s/s/%s", appURL(c), bug.Namespace, item.Value),
		})
		rep.Maintainers = email.MergeEmailLists(rep.Maintainers,
			subsystemMaintainers(c, rep.Namespace, item.Value))
	}
	for _, addr := range bug.UNCC {
		rep.CC = email.RemoveFromEmailList(rep.CC, addr)
		rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr)
	}
	return nil
}

func managersToRepos(c context.Context, ns string, managers []string) []string {
	var repos []string
	dedup := make(map[string]bool)
	for _, manager := range managers {
		build, err := lastManagerBuild(c, ns, manager)
		if err != nil {
			log.Errorf(c, "failed to get manager %q build: %v", manager, err)
			continue
		}
		repo := kernelRepoInfo(c, build).Alias
		if dedup[repo] {
			continue
		}
		dedup[repo] = true
		repos = append(repos, repo)
	}
	sort.Strings(repos)
	return repos
}

func queryCrashesForBug(c context.Context, bugKey *db.Key, limit int) (
	[]*Crash, []*db.Key, error) {
	var crashes []*Crash
	keys, err := db.NewQuery("Crash").
		Ancestor(bugKey).
		Order("-ReportLen").
		Order("-Time").
		Limit(limit).
		GetAll(c, &crashes)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to fetch crashes: %w", err)
	}
	return crashes, keys, nil
}

func loadAllBugs(c context.Context, filter func(*db.Query) *db.Query) ([]*Bug, []*db.Key, error) {
	var bugs []*Bug
	var keys []*db.Key
	err := foreachBug(c, filter, func(bug *Bug, key *db.Key) error {
		bugs = append(bugs, bug)
		keys = append(keys, key)
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	return bugs, keys, nil
}

func loadNamespaceBugs(c context.Context, ns string) ([]*Bug, []*db.Key, error) {
	return loadAllBugs(c, func(query *db.Query) *db.Query {
		return query.Filter("Namespace=", ns)
	})
}

func loadOpenBugs(c context.Context) ([]*Bug, []*db.Key, error) {
	return loadAllBugs(c, func(query *db.Query) *db.Query {
		return query.Filter("Status<", BugStatusFixed)
	})
}

func foreachBug(c context.Context, filter func(*db.Query) *db.Query, fn func(bug *Bug, key *db.Key) error) error {
	const batchSize = 2000
	var cursor *db.Cursor
	for {
		query := db.NewQuery("Bug").Limit(batchSize)
		if filter != nil {
			query = filter(query)
		}
		if cursor != nil {
			query = query.Start(*cursor)
		}
		iter := query.Run(c)
		for i := 0; ; i++ {
			bug := new(Bug)
			key, err := iter.Next(bug)
			if err == db.Done {
				if i < batchSize {
					return nil
				}
				break
			}
			if err != nil {
				return fmt.Errorf("failed to fetch bugs: %v", err)
			}
			if err := fn(bug, key); err != nil {
				return err
			}
		}
		cur, err := iter.Cursor()
		if err != nil {
			return fmt.Errorf("cursor failed while fetching bugs: %v", err)
		}
		cursor = &cur
	}
}

func filterBugs(bugs []*Bug, keys []*db.Key, filter func(*Bug) bool) ([]*Bug, []*db.Key) {
	var retBugs []*Bug
	var retKeys []*db.Key
	for i, bug := range bugs {
		if filter(bug) {
			retBugs = append(retBugs, bugs[i])
			retKeys = append(retKeys, keys[i])
		}
	}
	return retBugs, retKeys
}

// reportingPollClosed is called by backends to get list of closed bugs.
func reportingPollClosed(c context.Context, ids []string) ([]string, error) {
	idMap := make(map[string]bool, len(ids))
	for _, id := range ids {
		idMap[id] = true
	}
	var closed []string
	err := foreachBug(c, nil, func(bug *Bug, _ *db.Key) error {
		for i := range bug.Reporting {
			bugReporting := &bug.Reporting[i]
			if !idMap[bugReporting.ID] {
				continue
			}
			var err error
			bug, err = canonicalBug(c, bug)
			if err != nil {
				log.Errorf(c, "%v", err)
				break
			}
			if bug.Status >= BugStatusFixed || !bugReporting.Closed.IsZero() ||
				isDecommissioned(c, bug.Namespace) {
				closed = append(closed, bugReporting.ID)
			}
			break
		}
		return nil
	})
	return closed, err
}

// incomingCommand is entry point to bug status updates.
func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
	log.Infof(c, "got command: %+v", cmd)
	ok, reason, err := incomingCommandImpl(c, cmd)
	if err != nil {
		log.Errorf(c, "%v (%v)", reason, err)
	} else if !ok && reason != "" {
		log.Errorf(c, "invalid update: %v", reason)
	}
	return ok, reason, err
}

func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (bool, string, error) {
	for i, com := range cmd.FixCommits {
		if len(com) >= 2 && com[0] == '"' && com[len(com)-1] == '"' {
			com = com[1 : len(com)-1]
			cmd.FixCommits[i] = com
		}
		if len(com) < 3 {
			return false, fmt.Sprintf("bad commit title: %q", com), nil
		}
	}
	bug, bugKey, err := findBugByReportingID(c, cmd.ID)
	if err != nil {
		return false, internalError, err
	}
	var dupKey *db.Key
	if cmd.Status == dashapi.BugStatusDup {
		if looksLikeReportingHash(cmd.DupOf) {
			_, dupKey, _ = findBugByReportingID(c, cmd.DupOf)
		}
		if dupKey == nil {
			// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
			var dup *Bug
			dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
			if err != nil || dup == nil {
				return false, "can't find the dup bug", err
			}
			dupReporting := lastReportedReporting(dup)
			if dupReporting == nil {
				return false, "can't find the dup bug", fmt.Errorf("dup does not have reporting")
			}
			cmd.DupOf = dupReporting.ID
		}
	}
	now := timeNow(c)
	ok, reply := false, ""
	tx := func(c context.Context) error {
		var err error
		ok, reply, err = incomingCommandTx(c, now, cmd, bugKey, dupKey)
		return err
	}
	err = db.RunInTransaction(c, tx, &db.TransactionOptions{
		XG: true,
		// Default is 3 which fails sometimes.
		// We don't want incoming bug updates to fail,
		// because for e.g. email we won't have an external retry.
		Attempts: 30,
	})
	if err != nil {
		return false, internalError, err
	}
	return ok, reply, nil
}

func checkDupBug(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugKey, dupKey *db.Key) (
	*Bug, bool, string, error) {
	dup := new(Bug)
	if err := db.Get(c, dupKey, dup); err != nil {
		return nil, false, internalError, fmt.Errorf("can't find the dup by key: %v", err)
	}
	bugReporting, _ := bugReportingByID(bug, cmd.ID)
	dupReporting, _ := bugReportingByID(dup, cmd.DupOf)
	if bugReporting == nil || dupReporting == nil {
		return nil, false, internalError, fmt.Errorf("can't find bug reporting")
	}
	if bugKey.StringID() == dupKey.StringID() {
		if bugReporting.Name == dupReporting.Name {
			return nil, false, "Can't dup bug to itself.", nil
		}
		return nil, false, fmt.Sprintf("Can't dup bug to itself in different reporting (%v->%v).\n"+
			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
			bugReporting.Name, dupReporting.Name), nil
	}
	if bug.Namespace != dup.Namespace {
		return nil, false, fmt.Sprintf("Duplicate bug corresponds to a different kernel (%v->%v).\n"+
			"Please dup syzbot bugs only onto syzbot bugs for the same kernel.",
			bug.Namespace, dup.Namespace), nil
	}
	if !allowCrossReportingDup(c, bug, dup, bugReporting, dupReporting) {
		return nil, false, fmt.Sprintf("Can't dup bug to a bug in different reporting (%v->%v)."+
			"Please dup syzbot bugs only onto syzbot bugs for the same kernel/reporting.",
			bugReporting.Name, dupReporting.Name), nil
	}
	dupCanon, err := canonicalBug(c, dup)
	if err != nil {
		return nil, false, internalError, fmt.Errorf("failed to get canonical bug for dup: %v", err)
	}
	if !dupReporting.Closed.IsZero() && dupCanon.Status == BugStatusOpen {
		return nil, false, "Dup bug is already upstreamed.", nil
	}
	if dupCanon.keyHash() == bugKey.StringID() {
		return nil, false, "Setting this dup would lead to a bug cycle, cycles are not allowed.", nil
	}
	return dup, true, "", nil
}

func allowCrossReportingDup(c context.Context, bug, dup *Bug,
	bugReporting, dupReporting *BugReporting) bool {
	bugIdx := getReportingIdx(c, bug, bugReporting)
	dupIdx := getReportingIdx(c, dup, dupReporting)
	if bugIdx < 0 || dupIdx < 0 {
		return false
	}
	if bugIdx == dupIdx {
		return true
	}
	// We generally allow duping only within the same reporting.
	// But there is one exception: we also allow duping from last but one
	// reporting to the last one (which is stable, final destination)
	// provided that these two reportings have the same access level and type.
	// The rest of the combinations can lead to surprising states and
	// information hiding, so we don't allow them.
	cfg := config.Namespaces[bug.Namespace]
	bugConfig := &cfg.Reporting[bugIdx]
	dupConfig := &cfg.Reporting[dupIdx]
	lastIdx := len(cfg.Reporting) - 1
	return bugIdx == lastIdx-1 && dupIdx == lastIdx &&
		bugConfig.AccessLevel == dupConfig.AccessLevel &&
		bugConfig.Config.Type() == dupConfig.Config.Type()
}

func getReportingIdx(c context.Context, bug *Bug, bugReporting *BugReporting) int {
	for i := range bug.Reporting {
		if bug.Reporting[i].Name == bugReporting.Name {
			return i
		}
	}
	log.Errorf(c, "failed to find bug reporting by name: %q/%q", bug.Title, bugReporting.Name)
	return -1
}

func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey, dupKey *db.Key) (
	bool, string, error) {
	bug := new(Bug)
	if err := db.Get(c, bugKey, bug); err != nil {
		return false, internalError, fmt.Errorf("can't find the corresponding bug: %v", err)
	}
	var dup *Bug
	if cmd.Status == dashapi.BugStatusDup {
		dup1, ok, reason, err := checkDupBug(c, cmd, bug, bugKey, dupKey)
		if !ok || err != nil {
			return ok, reason, err
		}
		dup = dup1
	}
	state, err := loadReportingState(c)
	if err != nil {
		return false, internalError, err
	}
	ok, reason, err := incomingCommandUpdate(c, now, cmd, bugKey, bug, dup, state)
	if !ok || err != nil {
		return ok, reason, err
	}
	if _, err := db.Put(c, bugKey, bug); err != nil {
		return false, internalError, fmt.Errorf("failed to put bug: %v", err)
	}
	if err := saveReportingState(c, state); err != nil {
		return false, internalError, err
	}
	return true, "", nil
}

func incomingCommandUpdate(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bugKey *db.Key,
	bug, dup *Bug, state *ReportingState) (bool, string, error) {
	bugReporting, final := bugReportingByID(bug, cmd.ID)
	if bugReporting == nil {
		return false, internalError, fmt.Errorf("can't find bug reporting")
	}
	if ok, reply, err := checkBugStatus(c, cmd, bug, bugReporting); !ok {
		return false, reply, err
	}
	stateEnt := state.getEntry(now, bug.Namespace, bugReporting.Name)
	if ok, reply, err := incomingCommandCmd(c, now, cmd, bug, dup, bugReporting, final, stateEnt); !ok {
		return false, reply, err
	}
	if (len(cmd.FixCommits) != 0 || cmd.ResetFixCommits) &&
		(bug.Status == BugStatusOpen || bug.Status == BugStatusDup) {
		sort.Strings(cmd.FixCommits)
		if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) {
			bug.updateCommits(cmd.FixCommits, now)
		}
	}
	toReport := append([]int64{}, cmd.ReportCrashIDs...)
	if cmd.CrashID != 0 {
		bugReporting.CrashID = cmd.CrashID
		toReport = append(toReport, cmd.CrashID)
	}
	newRef := CrashReference{CrashReferenceReporting, bugReporting.Name, now}
	for _, crashID := range toReport {
		err := addCrashReference(c, crashID, bugKey, newRef)
		if err != nil {
			return false, internalError, err
		}
	}
	for _, crashID := range cmd.UnreportCrashIDs {
		err := removeCrashReference(c, crashID, bugKey, CrashReferenceReporting, bugReporting.Name)
		if err != nil {
			return false, internalError, err
		}
	}
	if bugReporting.ExtID == "" {
		bugReporting.ExtID = cmd.ExtID
	}
	if bugReporting.Link == "" {
		bugReporting.Link = cmd.Link
	}
	if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC {
		merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
		bugReporting.CC = strings.Join(merged, "|")
	}
	if bugReporting.ReproLevel < cmd.ReproLevel {
		bugReporting.ReproLevel = cmd.ReproLevel
	}
	if bug.Status != BugStatusDup {
		bug.DupOf = ""
	}
	if cmd.Status != dashapi.BugStatusOpen || !cmd.OnHold {
		bugReporting.OnHold = time.Time{}
	}
	if cmd.Status != dashapi.BugStatusUpdate || cmd.ExtID != "" {
		// Update LastActivity only on important events.
		// Otherwise it impedes bug obsoletion.
		bug.LastActivity = now
	}
	return true, "", nil
}

func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate, bug, dup *Bug,
	bugReporting *BugReporting, final bool, stateEnt *ReportingStateEntry) (bool, string, error) {
	switch cmd.Status {
	case dashapi.BugStatusOpen:
		bug.Status = BugStatusOpen
		bug.Closed = time.Time{}
		if bugReporting.Reported.IsZero() {
			bugReporting.Reported = now
			stateEnt.Sent++ // sending repro does not count against the quota
		}
		if bugReporting.OnHold.IsZero() && cmd.OnHold {
			bugReporting.OnHold = now
		}
		// Close all previous reporting if they are not closed yet
		// (can happen due to Status == ReportingDisabled).
		for i := range bug.Reporting {
			if bugReporting == &bug.Reporting[i] {
				break
			}
			if bug.Reporting[i].Closed.IsZero() {
				bug.Reporting[i].Closed = now
			}
		}
		if bug.ReproLevel < cmd.ReproLevel {
			return false, internalError,
				fmt.Errorf("bug update with invalid repro level: %v/%v",
					bug.ReproLevel, cmd.ReproLevel)
		}
	case dashapi.BugStatusUpstream:
		if final {
			return false, "Can't upstream, this is final destination.", nil
		}
		if len(bug.Commits) != 0 {
			// We could handle this case, but how/when it will occur
			// in real life is unclear now.
			return false, "Can't upstream this bug, the bug has fixing commits.", nil
		}
		bug.Status = BugStatusOpen
		bug.Closed = time.Time{}
		bugReporting.Closed = now
		bugReporting.Auto = cmd.Notification
	case dashapi.BugStatusInvalid:
		bug.Closed = now
		bug.Status = BugStatusInvalid
		bugReporting.Closed = now
		bugReporting.Auto = cmd.Notification
	case dashapi.BugStatusDup:
		bug.Status = BugStatusDup
		bug.Closed = now
		bug.DupOf = dup.keyHash()
	case dashapi.BugStatusUpdate:
		// Just update Link, Commits, etc below.
	case dashapi.BugStatusUnCC:
		bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC)
	default:
		return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status)
	}
	if cmd.StatusReason != "" {
		bug.StatusReason = cmd.StatusReason
	}
	for _, label := range cmd.Labels {
		bugReporting.AddLabel(label)
	}
	return true, "", nil
}

func checkBugStatus(c context.Context, cmd *dashapi.BugUpdate, bug *Bug, bugReporting *BugReporting) (
	bool, string, error) {
	switch bug.Status {
	case BugStatusOpen:
	case BugStatusDup:
		canon, err := canonicalBug(c, bug)
		if err != nil {
			return false, internalError, err
		}
		if canon.Status != BugStatusOpen {
			// We used to reject updates to closed bugs,
			// but this is confusing and non-actionable for users.
			// So now we fail the update, but give empty reason,
			// which means "don't notify user".
			if cmd.Status == dashapi.BugStatusUpdate {
				// This happens when people discuss old bugs.
				log.Infof(c, "Dup bug is already closed")
			} else {
				log.Warningf(c, "incoming command %v: dup bug is already closed", cmd.ID)
			}
			return false, "", nil
		}
	case BugStatusFixed, BugStatusInvalid:
		if cmd.Status != dashapi.BugStatusUpdate {
			log.Errorf(c, "incoming command %v: bug is already closed", cmd.ID)
		}
		return false, "", nil
	default:
		return false, internalError, fmt.Errorf("unknown bug status %v", bug.Status)
	}
	if !bugReporting.Closed.IsZero() {
		if cmd.Status != dashapi.BugStatusUpdate {
			log.Errorf(c, "incoming command %v: bug reporting is already closed", cmd.ID)
		}
		return false, "", nil
	}
	return true, "", nil
}

func findBugByReportingID(c context.Context, id string) (*Bug, *db.Key, error) {
	var bugs []*Bug
	keys, err := db.NewQuery("Bug").
		Filter("Reporting.ID=", id).
		Limit(2).
		GetAll(c, &bugs)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to fetch bugs: %v", err)
	}
	if len(bugs) == 0 {
		return nil, nil, fmt.Errorf("failed to find bug by reporting id %q", id)
	}
	if len(bugs) > 1 {
		return nil, nil, fmt.Errorf("multiple bugs for reporting id %q", id)
	}
	return bugs[0], keys[0], nil
}

func findDupByTitle(c context.Context, ns, title string) (*Bug, *db.Key, error) {
	title, seq, err := splitDisplayTitle(title)
	if err != nil {
		return nil, nil, err
	}
	bugHash := bugKeyHash(ns, title, seq)
	bugKey := db.NewKey(c, "Bug", bugHash, 0, nil)
	bug := new(Bug)
	if err := db.Get(c, bugKey, bug); err != nil {
		if err == db.ErrNoSuchEntity {
			return nil, nil, nil // This is not really an error, we should notify the user instead.
		}
		return nil, nil, fmt.Errorf("failed to get dup: %v", err)
	}
	return bug, bugKey, nil
}

func bugReportingByID(bug *Bug, id string) (*BugReporting, bool) {
	for i := range bug.Reporting {
		if bug.Reporting[i].ID == id {
			return &bug.Reporting[i], i == len(bug.Reporting)-1
		}
	}
	return nil, false
}

func bugReportingByName(bug *Bug, name string) *BugReporting {
	for i := range bug.Reporting {
		if bug.Reporting[i].Name == name {
			return &bug.Reporting[i]
		}
	}
	return nil
}

func lastReportedReporting(bug *Bug) *BugReporting {
	for i := len(bug.Reporting) - 1; i >= 0; i-- {
		if !bug.Reporting[i].Reported.IsZero() {
			return &bug.Reporting[i]
		}
	}
	return nil
}

// The updateReporting method is supposed to be called both to fully initialize a new
// Bug object and also to adjust it to the updated namespace configuration.
func (bug *Bug) updateReportings(cfg *Config, now time.Time) error {
	oldReportings := map[string]BugReporting{}
	oldPositions := map[string]int{}
	for i, rep := range bug.Reporting {
		oldReportings[rep.Name] = rep
		oldPositions[rep.Name] = i
	}
	maxPos := 0
	bug.Reporting = nil
	for _, rep := range cfg.Reporting {
		if oldRep, ok := oldReportings[rep.Name]; ok {
			oldPos := oldPositions[rep.Name]
			if oldPos < maxPos {
				// At the moment we only support insertions and deletions of reportings.
				// TODO: figure out what exactly can go wrong if we also allow reordering.
				return fmt.Errorf("the order of reportings is changed, before: %v", oldPositions)
			}
			maxPos = oldPos
			bug.Reporting = append(bug.Reporting, oldRep)
		} else {
			bug.Reporting = append(bug.Reporting, BugReporting{
				Name: rep.Name,
				ID:   bugReportingHash(bug.keyHash(), rep.Name),
			})
		}
	}
	// We might have added new BugReporting objects between/before the ones that were
	// already reported. To let syzbot continue from the same reporting stage where it
	// stopped, close such outliers.
	seenProcessed := false
	minTime := now
	for i := len(bug.Reporting) - 1; i >= 0; i-- {
		rep := &bug.Reporting[i]
		if !rep.Reported.IsZero() || !rep.Closed.IsZero() || !rep.OnHold.IsZero() {
			if !rep.Reported.IsZero() && rep.Reported.Before(minTime) {
				minTime = rep.Reported
			}
			seenProcessed = true
		} else if seenProcessed {
			rep.Dummy = true
		}
	}
	// Yet another stage -- we set Closed and Reported to dummy objects in non-decreasing order.
	currTime := minTime
	for i := range bug.Reporting {
		rep := &bug.Reporting[i]
		if rep.Dummy {
			rep.Closed = currTime
			rep.Reported = currTime
		} else if rep.Reported.After(currTime) {
			currTime = rep.Reported
		}
	}
	return nil
}

func findCrashForBug(c context.Context, bug *Bug) (*Crash, *db.Key, error) {
	bugKey := bug.key(c)
	crashes, keys, err := queryCrashesForBug(c, bugKey, 1)
	if err != nil {
		return nil, nil, err
	}
	if len(crashes) < 1 {
		return nil, nil, fmt.Errorf("no crashes")
	}
	crash, key := crashes[0], keys[0]
	if bug.HeadReproLevel == ReproLevelC {
		if crash.ReproC == 0 {
			log.Errorf(c, "bug '%v': has C repro, but crash without C repro", bug.Title)
		}
	} else if bug.HeadReproLevel == ReproLevelSyz {
		if crash.ReproSyz == 0 {
			log.Errorf(c, "bug '%v': has syz repro, but crash without syz repro", bug.Title)
		}
	}
	return crash, key, nil
}

func loadReportingState(c context.Context) (*ReportingState, error) {
	state := new(ReportingState)
	key := db.NewKey(c, "ReportingState", "", 1, nil)
	if err := db.Get(c, key, state); err != nil && err != db.ErrNoSuchEntity {
		return nil, fmt.Errorf("failed to get reporting state: %v", err)
	}
	return state, nil
}

func saveReportingState(c context.Context, state *ReportingState) error {
	key := db.NewKey(c, "ReportingState", "", 1, nil)
	if _, err := db.Put(c, key, state); err != nil {
		return fmt.Errorf("failed to put reporting state: %v", err)
	}
	return nil
}

func (state *ReportingState) getEntry(now time.Time, namespace, name string) *ReportingStateEntry {
	if namespace == "" || name == "" {
		panic(fmt.Sprintf("requesting reporting state for %v/%v", namespace, name))
	}
	// Convert time to date of the form 20170125.
	date := timeDate(now)
	for i := range state.Entries {
		ent := &state.Entries[i]
		if ent.Namespace == namespace && ent.Name == name {
			if ent.Date != date {
				ent.Date = date
				ent.Sent = 0
			}
			return ent
		}
	}
	state.Entries = append(state.Entries, ReportingStateEntry{
		Namespace: namespace,
		Name:      name,
		Date:      date,
		Sent:      0,
	})
	return &state.Entries[len(state.Entries)-1]
}

func loadFullBugInfo(c context.Context, bug *Bug, bugKey *db.Key,
	bugReporting *BugReporting) (*dashapi.FullBugInfo, error) {
	reporting := config.Namespaces[bug.Namespace].ReportingByName(bugReporting.Name)
	if reporting == nil {
		return nil, fmt.Errorf("failed to find the reporting object")
	}
	ret := &dashapi.FullBugInfo{}
	// Query bisections.
	var err error
	if bug.BisectCause > BisectPending {
		ret.BisectCause, err = prepareBisectionReport(c, bug, JobBisectCause, reporting)
		if err != nil {
			return nil, err
		}
	}
	if bug.BisectFix > BisectPending {
		ret.BisectFix, err = prepareBisectionReport(c, bug, JobBisectFix, reporting)
		if err != nil {
			return nil, err
		}
	}
	// Query similar bugs.
	similar, err := loadSimilarBugs(c, bug)
	if err != nil {
		return nil, err
	}
	for _, similarBug := range similar {
		_, bugReporting, _, _, _ := currentReporting(similarBug)
		if bugReporting == nil {
			continue
		}
		status, err := similarBug.dashapiStatus()
		if err != nil {
			return nil, err
		}
		ret.SimilarBugs = append(ret.SimilarBugs, &dashapi.SimilarBugInfo{
			Title:      similarBug.displayTitle(),
			Status:     status,
			Namespace:  similarBug.Namespace,
			ReproLevel: similarBug.ReproLevel,
			Link:       fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID),
			ReportLink: bugReporting.Link,
			Closed:     similarBug.Closed,
		})
	}
	// Query crashes.
	crashes, err := representativeCrashes(c, bugKey)
	if err != nil {
		return nil, err
	}
	for _, crash := range crashes {
		rep, err := crashBugReport(c, bug, crash.crash, crash.key, bugReporting, reporting)
		if err != nil {
			return nil, fmt.Errorf("crash %d: %e", crash.key.IntID(), err)
		}
		ret.Crashes = append(ret.Crashes, rep)
	}
	// Query tree testing jobs.
	ret.TreeJobs, err = treeTestJobs(c, bug)
	if err != nil {
		return nil, err
	}
	return ret, nil
}

func prepareBisectionReport(c context.Context, bug *Bug, jobType JobType,
	reporting *Reporting) (*dashapi.BugReport, error) {
	bisectionJob, err := queryBestBisection(c, bug, jobType)
	if bisectionJob == nil || err != nil {
		return nil, err
	}
	job, jobKey := bisectionJob.job, bisectionJob.key
	if job.Reporting != "" {
		ret, err := createBugReportForJob(c, job, jobKey, reporting.Config)
		if err != nil {
			return nil, fmt.Errorf("failed to create the job bug report: %w", err)
		}
		return ret, nil
	}
	return nil, nil
}

type crashWithKey struct {
	crash *Crash
	key   *db.Key
}

func representativeCrashes(c context.Context, bugKey *db.Key) ([]*crashWithKey, error) {
	// There should generally be no need to query lots of crashes.
	const fetchCrashes = 50
	allCrashes, allCrashKeys, err := queryCrashesForBug(c, bugKey, fetchCrashes)
	if err != nil {
		return nil, err
	}
	// Let's consider a crash fresh if it happened within the last week.
	const recentDuration = time.Hour * 24 * 7
	now := timeNow(c)

	var bestCrash, recentCrash, straceCrash *crashWithKey
	for id, crash := range allCrashes {
		key := allCrashKeys[id]
		if bestCrash == nil {
			bestCrash = &crashWithKey{crash, key}
		}
		if now.Sub(crash.Time) < recentDuration && recentCrash == nil {
			recentCrash = &crashWithKey{crash, key}
		}
		if dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0 &&
			straceCrash == nil {
			straceCrash = &crashWithKey{crash, key}
		}
	}
	// It's possible that there are so many crashes with reproducers that
	// we do not get to see the recent crashes without them.
	// Give it one more try.
	if recentCrash == nil {
		var crashes []*Crash
		keys, err := db.NewQuery("Crash").
			Ancestor(bugKey).
			Order("-Time").
			Limit(1).
			GetAll(c, &crashes)
		if err != nil {
			return nil, fmt.Errorf("failed to fetch the latest crash: %v", err)
		}
		if len(crashes) > 0 {
			recentCrash = &crashWithKey{crashes[0], keys[0]}
		}
	}
	dedup := map[int64]bool{}
	crashes := []*crashWithKey{}
	for _, item := range []*crashWithKey{recentCrash, bestCrash, straceCrash} {
		if item == nil || dedup[item.key.IntID()] {
			continue
		}
		crashes = append(crashes, item)
		dedup[item.key.IntID()] = true
	}
	// Sort by Time in desc order.
	sort.Slice(crashes, func(i, j int) bool {
		return crashes[i].crash.Time.After(crashes[j].crash.Time)
	})
	return crashes, nil
}

// bugReportSorter sorts bugs by priority we want to report them.
// E.g. we want to report bugs with reproducers before bugs without reproducers.
type bugReportSorter []*Bug

func (a bugReportSorter) Len() int      { return len(a) }
func (a bugReportSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a bugReportSorter) Less(i, j int) bool {
	if a[i].ReproLevel != a[j].ReproLevel {
		return a[i].ReproLevel > a[j].ReproLevel
	}
	if a[i].HasReport != a[j].HasReport {
		return a[i].HasReport
	}
	if a[i].NumCrashes != a[j].NumCrashes {
		return a[i].NumCrashes > a[j].NumCrashes
	}
	return a[i].FirstTime.Before(a[j].FirstTime)
}

// kernelArch returns arch as kernel developers know it (rather than Go names).
// Currently Linux-specific.
func kernelArch(arch string) string {
	switch arch {
	case targets.I386:
		return "i386"
	default:
		return arch
	}
}
