package pkger

import (
	"context"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"github.com/influxdata/influxdb/v2"
	ierrors "github.com/influxdata/influxdb/v2/kit/errors"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/notification"
	icheck "github.com/influxdata/influxdb/v2/notification/check"
	"github.com/influxdata/influxdb/v2/notification/endpoint"
	"github.com/influxdata/influxdb/v2/notification/rule"
	"github.com/influxdata/influxdb/v2/pkger/internal/wordplay"
	"github.com/influxdata/influxdb/v2/snowflake"
	"github.com/influxdata/influxdb/v2/task/taskmodel"
)

var idGenerator = snowflake.NewDefaultIDGenerator()

// NameGenerator generates a random name. Includes an optional fuzz option to
// further randomize the name.
type NameGenerator func() string

// ResourceToClone is a resource that will be cloned.
type ResourceToClone struct {
	Kind Kind        `json:"kind"`
	ID   platform.ID `json:"id,omitempty"`
	Name string      `json:"name"`
	// note(jsteenb2): For time being we'll allow this internally, but not externally. A lot of
	// issues to account for when exposing this to the outside world. Not something I'm keen
	// to accommodate at this time.
	MetaName string `json:"-"`
}

// OK validates a resource clone is viable.
func (r ResourceToClone) OK() error {
	if err := r.Kind.OK(); err != nil {
		return err
	}
	if r.ID == platform.ID(0) && len(r.Name) == 0 {
		return errors.New("must provide an ID or name")
	}
	return nil
}

var kindPriorities = map[Kind]int{
	KindLabel:                         1,
	KindBucket:                        2,
	KindCheck:                         3,
	KindCheckDeadman:                  4,
	KindCheckThreshold:                5,
	KindNotificationEndpoint:          6,
	KindNotificationEndpointHTTP:      7,
	KindNotificationEndpointPagerDuty: 8,
	KindNotificationEndpointSlack:     9,
	KindNotificationRule:              10,
	KindTask:                          11,
	KindVariable:                      12,
	KindDashboard:                     13,
	KindTelegraf:                      14,
}

type exportKey struct {
	orgID platform.ID
	id    platform.ID
	name  string
	kind  Kind
}

func newExportKey(orgID, id platform.ID, k Kind, name string) exportKey {
	return exportKey{
		orgID: orgID,
		id:    id,
		name:  name,
		kind:  k,
	}
}

type resourceExporter struct {
	nameGen NameGenerator

	bucketSVC   influxdb.BucketService
	checkSVC    influxdb.CheckService
	dashSVC     influxdb.DashboardService
	labelSVC    influxdb.LabelService
	endpointSVC influxdb.NotificationEndpointService
	ruleSVC     influxdb.NotificationRuleStore
	taskSVC     taskmodel.TaskService
	teleSVC     influxdb.TelegrafConfigStore
	varSVC      influxdb.VariableService

	mObjects        map[exportKey]Object
	mPkgNames       map[string]bool
	mStackResources map[exportKey]StackResource
}

func newResourceExporter(svc *Service) *resourceExporter {
	return &resourceExporter{
		nameGen:         wordplay.GetRandomName,
		bucketSVC:       svc.bucketSVC,
		checkSVC:        svc.checkSVC,
		dashSVC:         svc.dashSVC,
		labelSVC:        svc.labelSVC,
		endpointSVC:     svc.endpointSVC,
		ruleSVC:         svc.ruleSVC,
		taskSVC:         svc.taskSVC,
		teleSVC:         svc.teleSVC,
		varSVC:          svc.varSVC,
		mObjects:        make(map[exportKey]Object),
		mPkgNames:       make(map[string]bool),
		mStackResources: make(map[exportKey]StackResource),
	}
}

func (ex *resourceExporter) Export(ctx context.Context, resourcesToClone []ResourceToClone, labelNames ...string) error {
	mLabelIDsToMetaName := make(map[platform.ID]string)
	for _, r := range resourcesToClone {
		if !r.Kind.is(KindLabel) || r.MetaName == "" {
			continue
		}
		mLabelIDsToMetaName[r.ID] = r.MetaName
	}

	cloneAssFn, err := ex.resourceCloneAssociationsGen(ctx, mLabelIDsToMetaName, labelNames...)
	if err != nil {
		return err
	}

	resourcesToClone = uniqResourcesToClone(resourcesToClone)
	// sorting this in priority order guarantees that the dependencies/associations
	// for a resource are handled prior to the resource being processed.
	// 	i.e. if a bucket depends on a label, then labels need to be run first
	//		to guarantee they are available before a bucket is exported.
	sort.Slice(resourcesToClone, func(i, j int) bool {
		iName, jName := resourcesToClone[i].Name, resourcesToClone[j].Name
		iKind, jKind := resourcesToClone[i].Kind, resourcesToClone[j].Kind

		if iKind.is(jKind) {
			return iName < jName
		}
		return kindPriorities[iKind] < kindPriorities[jKind]
	})

	for _, r := range resourcesToClone {
		err := ex.resourceCloneToKind(ctx, r, cloneAssFn)
		if err != nil {
			return internalErr(fmt.Errorf("failed to clone resource: resource_id=%s resource_kind=%s err=%q", r.ID, r.Kind, err))
		}
	}

	return nil
}

func (ex *resourceExporter) Objects() []Object {
	objects := make([]Object, 0, len(ex.mObjects))
	for _, obj := range ex.mObjects {
		objects = append(objects, obj)
	}

	return sortObjects(objects)
}

func (ex *resourceExporter) StackResources() []StackResource {
	resources := make([]StackResource, 0, len(ex.mStackResources))
	for _, res := range ex.mStackResources {
		resources = append(resources, res)
	}
	return resources
}

// we only need an id when we have resources that are not unique by name via the
// metastore. resoureces that are unique by name will be provided a default stamp
// making looksup unique since each resource will be unique by name.
const uniqByNameResID = platform.ID(0)

type cloneAssociationsFn func(context.Context, ResourceToClone) (associations []ObjectAssociation, skipResource bool, err error)

func (ex *resourceExporter) resourceCloneToKind(ctx context.Context, r ResourceToClone, cFn cloneAssociationsFn) (e error) {
	defer func() {
		if e != nil {
			e = ierrors.Wrap(e, "cloning resource")
		}
	}()

	ass, skipResource, err := cFn(ctx, r)
	if err != nil {
		return err
	}
	if skipResource {
		return nil
	}

	mapResource := func(orgID, uniqResID platform.ID, k Kind, object Object) {
		// overwrite the default metadata.name field with export generated one here
		metaName := r.MetaName
		if r.MetaName == "" {
			metaName = ex.uniqName()
		}

		stackResource := StackResource{
			APIVersion: APIVersion,
			ID:         r.ID,
			MetaName:   metaName,
			Kind:       r.Kind,
		}
		for _, a := range ass {
			stackResource.Associations = append(stackResource.Associations, StackResourceAssociation(a))
		}

		object.SetMetadataName(metaName)
		object.AddAssociations(ass...)
		key := newExportKey(orgID, uniqResID, k, object.Spec.stringShort(fieldName))
		ex.mObjects[key] = object
		ex.mStackResources[key] = stackResource
	}

	switch {
	case r.Kind.is(KindBucket):
		filter := influxdb.BucketFilter{}
		if r.ID != platform.ID(0) {
			filter.ID = &r.ID
		}
		if len(r.Name) > 0 {
			filter.Name = &r.Name
		}

		bkts, n, err := ex.bucketSVC.FindBuckets(ctx, filter)
		if err != nil {
			return err
		}
		if n < 1 {
			return errors.New("no buckets found")
		}

		for _, bkt := range bkts {
			mapResource(bkt.OrgID, bkt.ID, KindBucket, BucketToObject(r.Name, *bkt))
		}
	case r.Kind.is(KindCheck), r.Kind.is(KindCheckDeadman), r.Kind.is(KindCheckThreshold):
		filter := influxdb.CheckFilter{}
		if r.ID != platform.ID(0) {
			filter.ID = &r.ID
		}
		if len(r.Name) > 0 {
			filter.Name = &r.Name
		}
		chs, n, err := ex.checkSVC.FindChecks(ctx, filter)
		if err != nil {
			return err
		}
		if n < 1 {
			return errors.New("no checks found")
		}

		for _, ch := range chs {
			mapResource(ch.GetOrgID(), ch.GetID(), KindCheck, CheckToObject(r.Name, ch))
		}
	case r.Kind.is(KindDashboard):
		var (
			hasID  bool
			filter = influxdb.DashboardFilter{}
		)
		if r.ID != platform.ID(0) {
			hasID = true
			filter.IDs = []*platform.ID{&r.ID}
		}

		dashes, _, err := ex.dashSVC.FindDashboards(ctx, filter, influxdb.DefaultDashboardFindOptions)
		if err != nil {
			return err
		}

		var mapped bool
		for _, dash := range dashes {
			if (!hasID && len(r.Name) > 0 && dash.Name != r.Name) || (hasID && dash.ID != r.ID) {
				continue
			}

			for _, cell := range dash.Cells {
				v, err := ex.dashSVC.GetDashboardCellView(ctx, dash.ID, cell.ID)
				if err != nil {
					continue
				}
				cell.View = v
			}

			mapResource(dash.OrganizationID, dash.ID, KindDashboard, DashboardToObject(r.Name, *dash))
			mapped = true
		}

		if !mapped {
			return errors.New("no dashboards found")
		}
	case r.Kind.is(KindLabel):
		switch {
		case r.ID != platform.ID(0):
			l, err := ex.labelSVC.FindLabelByID(ctx, r.ID)
			if err != nil {
				return err
			}

			mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l))
		case len(r.Name) > 0:
			labels, err := ex.labelSVC.FindLabels(ctx, influxdb.LabelFilter{Name: r.Name})
			if err != nil {
				return err
			}

			for _, l := range labels {
				mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l))
			}
		}
	case r.Kind.is(KindNotificationEndpoint),
		r.Kind.is(KindNotificationEndpointHTTP),
		r.Kind.is(KindNotificationEndpointPagerDuty),
		r.Kind.is(KindNotificationEndpointSlack):
		var endpoints []influxdb.NotificationEndpoint

		switch {
		case r.ID != platform.ID(0):
			notifEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, r.ID)
			if err != nil {
				return err
			}
			endpoints = append(endpoints, notifEndpoint)
		case len(r.Name) != 0:
			allEndpoints, _, err := ex.endpointSVC.FindNotificationEndpoints(ctx, influxdb.NotificationEndpointFilter{})
			if err != nil {
				return err
			}

			for _, notifEndpoint := range allEndpoints {
				if notifEndpoint.GetName() != r.Name || notifEndpoint == nil {
					continue
				}
				endpoints = append(endpoints, notifEndpoint)
			}
		}

		if len(endpoints) == 0 {
			return errors.New("no notification endpoints found")
		}

		for _, e := range endpoints {
			mapResource(e.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject(r.Name, e))
		}
	case r.Kind.is(KindNotificationRule):
		var rules []influxdb.NotificationRule

		switch {
		case r.ID != platform.ID(0):
			r, err := ex.ruleSVC.FindNotificationRuleByID(ctx, r.ID)
			if err != nil {
				return err
			}
			rules = append(rules, r)
		case len(r.Name) != 0:
			allRules, _, err := ex.ruleSVC.FindNotificationRules(ctx, influxdb.NotificationRuleFilter{})
			if err != nil {
				return err
			}

			for _, rule := range allRules {
				if rule.GetName() != r.Name {
					continue
				}
				rules = append(rules, rule)
			}
		}

		if len(rules) == 0 {
			return errors.New("no notification rules found")
		}

		for _, rule := range rules {
			ruleEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, rule.GetEndpointID())
			if err != nil {
				return err
			}

			endpointKey := newExportKey(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, ruleEndpoint.GetName())
			object, ok := ex.mObjects[endpointKey]
			if !ok {
				mapResource(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject("", ruleEndpoint))
				object = ex.mObjects[endpointKey]
			}
			endpointObjectName := object.Name()

			mapResource(rule.GetOrgID(), rule.GetID(), KindNotificationRule, NotificationRuleToObject(r.Name, endpointObjectName, rule))
		}
	case r.Kind.is(KindTask):
		switch {
		case r.ID != platform.ID(0):
			t, err := ex.taskSVC.FindTaskByID(ctx, r.ID)
			if err != nil {
				return err
			}
			mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t))
		case len(r.Name) > 0:
			tasks, n, err := ex.taskSVC.FindTasks(ctx, taskmodel.TaskFilter{Name: &r.Name})
			if err != nil {
				return err
			}
			if n < 1 {
				return errors.New("no tasks found")
			}

			for _, t := range tasks {
				mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t))
			}
		}
	case r.Kind.is(KindTelegraf):
		switch {
		case r.ID != platform.ID(0):
			t, err := ex.teleSVC.FindTelegrafConfigByID(ctx, r.ID)
			if err != nil {
				return err
			}
			mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t))
		case len(r.Name) > 0:
			telegrafs, _, err := ex.teleSVC.FindTelegrafConfigs(ctx, influxdb.TelegrafConfigFilter{})
			if err != nil {
				return err
			}

			var mapped bool
			for _, t := range telegrafs {
				if t.Name != r.Name {
					continue
				}

				mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t))
				mapped = true
			}
			if !mapped {
				return errors.New("no telegraf configs found")
			}

		}
	case r.Kind.is(KindVariable):
		switch {
		case r.ID != platform.ID(0):
			v, err := ex.varSVC.FindVariableByID(ctx, r.ID)
			if err != nil {
				return err
			}
			mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v))
		case len(r.Name) > 0:
			variables, err := ex.varSVC.FindVariables(ctx, influxdb.VariableFilter{})
			if err != nil {
				return err
			}

			var mapped bool
			for _, v := range variables {
				if v.Name != r.Name {
					continue
				}

				mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v))
				mapped = true
			}
			if !mapped {
				return errors.New("no variables found")
			}
		}
	default:
		return errors.New("unsupported kind provided: " + string(r.Kind))
	}

	return nil
}

func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, labelIDsToMetaName map[platform.ID]string, labelNames ...string) (cloneAssociationsFn, error) {
	mLabelNames := make(map[string]bool)
	for _, labelName := range labelNames {
		mLabelNames[labelName] = true
	}

	mLabelIDs, err := getLabelIDMap(ctx, ex.labelSVC, labelNames)
	if err != nil {
		return nil, err
	}

	cloneFn := func(ctx context.Context, r ResourceToClone) ([]ObjectAssociation, bool, error) {
		if r.Kind.is(KindUnknown) {
			return nil, true, nil
		}
		if r.Kind.is(KindLabel) {
			// check here verifies the label maps to an id of a valid label name
			shouldSkip := len(mLabelIDs) > 0 && !mLabelIDs[r.ID]
			return nil, shouldSkip, nil
		}

		if len(r.Name) > 0 && r.ID == platform.ID(0) {
			return nil, false, nil
		}

		labels, err := ex.labelSVC.FindResourceLabels(ctx, influxdb.LabelMappingFilter{
			ResourceID:   r.ID,
			ResourceType: r.Kind.ResourceType(),
		})
		if err != nil {
			return nil, false, ierrors.Wrap(err, "finding resource labels")
		}

		if len(mLabelNames) > 0 {
			shouldSkip := true
			for _, l := range labels {
				if _, ok := mLabelNames[l.Name]; ok {
					shouldSkip = false
					break
				}
			}
			if shouldSkip {
				return nil, true, nil
			}
		}

		var associations []ObjectAssociation
		for _, l := range labels {
			if len(mLabelNames) > 0 {
				if _, ok := mLabelNames[l.Name]; !ok {
					continue
				}
			}

			labelObject := LabelToObject("", *l)
			metaName := labelIDsToMetaName[l.ID]
			if metaName == "" {
				metaName = ex.uniqName()
			}
			labelObject.Metadata[fieldName] = metaName

			k := newExportKey(l.OrgID, uniqByNameResID, KindLabel, l.Name)
			existing, ok := ex.mObjects[k]
			if ok {
				associations = append(associations, ObjectAssociation{
					Kind:     KindLabel,
					MetaName: existing.Name(),
				})
				continue
			}
			associations = append(associations, ObjectAssociation{
				Kind:     KindLabel,
				MetaName: labelObject.Name(),
			})
			ex.mObjects[k] = labelObject
		}
		sort.Slice(associations, func(i, j int) bool {
			return associations[i].MetaName < associations[j].MetaName
		})
		return associations, false, nil
	}

	return cloneFn, nil
}

func (ex *resourceExporter) uniqName() string {
	return uniqMetaName(ex.nameGen, idGenerator, ex.mPkgNames)
}

func uniqMetaName(nameGen NameGenerator, idGen platform.IDGenerator, existingNames map[string]bool) string {
	uuid := strings.ToLower(idGen.ID().String())
	name := uuid
	for i := 1; i < 250; i++ {
		name = fmt.Sprintf("%s-%s", nameGen(), uuid[10:])
		if !existingNames[name] {
			break
		}
	}
	return name
}

func uniqResourcesToClone(resources []ResourceToClone) []ResourceToClone {
	type key struct {
		kind Kind
		id   platform.ID
	}
	m := make(map[key]ResourceToClone)

	for i := range resources {
		r := resources[i]
		rKey := key{kind: r.Kind, id: r.ID}

		kr, ok := m[rKey]
		switch {
		case ok && kr.Name == r.Name && kr.MetaName == r.MetaName:
		case ok && kr.MetaName != "" && r.MetaName == "":
		case ok && kr.MetaName == "" && kr.Name != "" && r.Name == "":
		default:
			m[rKey] = r
		}
	}

	out := make([]ResourceToClone, 0, len(resources))
	for _, r := range m {
		out = append(out, r)
	}
	return out
}

// BucketToObject converts a influxdb.Bucket into an Object.
func BucketToObject(name string, bkt influxdb.Bucket) Object {
	if name == "" {
		name = bkt.Name
	}

	o := newObject(KindBucket, name)
	assignNonZeroStrings(o.Spec, map[string]string{fieldDescription: bkt.Description})
	if bkt.RetentionPeriod != 0 {
		o.Spec[fieldBucketRetentionRules] = retentionRules{newRetentionRule(bkt.RetentionPeriod)}
	}
	return o
}

func CheckToObject(name string, ch influxdb.Check) Object {
	if name == "" {
		name = ch.GetName()
	}
	o := newObject(KindCheck, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldDescription: ch.GetDescription(),
		fieldStatus:      taskmodel.TaskStatusActive,
	})

	assignBase := func(base icheck.Base) {
		o.Spec[fieldQuery] = strings.TrimSpace(base.Query.Text)
		o.Spec[fieldCheckStatusMessageTemplate] = base.StatusMessageTemplate
		assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{
			fieldEvery:  base.Every,
			fieldOffset: base.Offset,
		})

		var tags []Resource
		for _, t := range base.Tags {
			if t.Valid() != nil {
				continue
			}
			tags = append(tags, Resource{
				fieldKey:   t.Key,
				fieldValue: t.Value,
			})
		}
		if len(tags) > 0 {
			o.Spec[fieldCheckTags] = tags
		}
	}

	switch cT := ch.(type) {
	case *icheck.Deadman:
		o.Kind = KindCheckDeadman
		assignBase(cT.Base)
		assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{
			fieldCheckTimeSince: cT.TimeSince,
			fieldCheckStaleTime: cT.StaleTime,
		})
		o.Spec[fieldLevel] = cT.Level.String()
		assignNonZeroBools(o.Spec, map[string]bool{fieldCheckReportZero: cT.ReportZero})
	case *icheck.Threshold:
		o.Kind = KindCheckThreshold
		assignBase(cT.Base)
		var thresholds []Resource
		for _, th := range cT.Thresholds {
			thresholds = append(thresholds, convertThreshold(th))
		}
		o.Spec[fieldCheckThresholds] = thresholds
	}
	return o
}

func convertThreshold(th icheck.ThresholdConfig) Resource {
	r := Resource{fieldLevel: th.GetLevel().String()}

	assignLesser := func(threshType thresholdType, allValues bool, val float64) {
		r[fieldType] = string(threshType)
		assignNonZeroBools(r, map[string]bool{fieldCheckAllValues: allValues})
		r[fieldValue] = val
	}

	switch realType := th.(type) {
	case icheck.Lesser:
		assignLesser(thresholdTypeLesser, realType.AllValues, realType.Value)
	case *icheck.Lesser:
		assignLesser(thresholdTypeLesser, realType.AllValues, realType.Value)
	case icheck.Greater:
		assignLesser(thresholdTypeGreater, realType.AllValues, realType.Value)
	case *icheck.Greater:
		assignLesser(thresholdTypeGreater, realType.AllValues, realType.Value)
	case icheck.Range:
		assignRangeThreshold(r, realType)
	case *icheck.Range:
		assignRangeThreshold(r, *realType)
	}

	return r
}

func assignRangeThreshold(r Resource, rangeThreshold icheck.Range) {
	thType := thresholdTypeOutsideRange
	if rangeThreshold.Within {
		thType = thresholdTypeInsideRange
	}
	r[fieldType] = string(thType)
	assignNonZeroBools(r, map[string]bool{fieldCheckAllValues: rangeThreshold.AllValues})
	r[fieldMax] = rangeThreshold.Max
	r[fieldMin] = rangeThreshold.Min
}

func convertCellView(cell influxdb.Cell) chart {
	var name string
	if cell.View != nil {
		name = cell.View.Name
	}
	ch := chart{
		Name:   name,
		Height: int(cell.H),
		Width:  int(cell.W),
		XPos:   int(cell.X),
		YPos:   int(cell.Y),
	}

	setCommon := func(k chartKind, iColors []influxdb.ViewColor, dec influxdb.DecimalPlaces, iQueries []influxdb.DashboardQuery) {
		ch.Kind = k
		ch.Colors = convertColors(iColors)
		ch.DecimalPlaces = int(dec.Digits)
		ch.EnforceDecimals = dec.IsEnforced
		ch.Queries = convertQueries(iQueries)
	}

	setNoteFixes := func(note string, noteOnEmpty bool, prefix, suffix string) {
		ch.Note = note
		ch.NoteOnEmpty = noteOnEmpty
		ch.Prefix = prefix
		ch.Suffix = suffix
	}

	setStaticLegend := func(sl influxdb.StaticLegend) {
		ch.StaticLegend.ColorizeRows = sl.ColorizeRows
		ch.StaticLegend.HeightRatio = sl.HeightRatio
		ch.StaticLegend.Show = sl.Show
		ch.StaticLegend.Opacity = sl.Opacity
		ch.StaticLegend.OrientationThreshold = sl.OrientationThreshold
		ch.StaticLegend.ValueAxis = sl.ValueAxis
		ch.StaticLegend.WidthRatio = sl.WidthRatio
	}

	props := cell.View.Properties
	switch p := props.(type) {
	case influxdb.GaugeViewProperties:
		setCommon(chartKindGauge, p.ViewColors, p.DecimalPlaces, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix)
		ch.TickPrefix = p.TickPrefix
		ch.TickSuffix = p.TickSuffix
	case influxdb.GeoViewProperties:
		ch.Kind = chartKindGeo
		ch.Queries = convertQueries(p.Queries)
		ch.Zoom = p.Zoom
		ch.Center = center{Lat: p.Center.Lat, Lon: p.Center.Lon}
		ch.MapStyle = p.MapStyle
		ch.AllowPanAndZoom = p.AllowPanAndZoom
		ch.DetectCoordinateFields = p.DetectCoordinateFields
		ch.Colors = convertColors(p.ViewColor)
		ch.GeoLayers = convertGeoLayers(p.GeoLayers)
		ch.Note = p.Note
		ch.NoteOnEmpty = p.ShowNoteWhenEmpty
	case influxdb.HeatmapViewProperties:
		ch.Kind = chartKindHeatMap
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = stringsToColors(p.ViewColors)
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YCol = p.YColumn
		ch.GenerateYAxisTicks = p.GenerateYAxisTicks
		ch.YTotalTicks = p.YTotalTicks
		ch.YTickStart = p.YTickStart
		ch.YTickStep = p.YTickStep
		ch.Axes = []axis{
			{Label: p.XAxisLabel, Prefix: p.XPrefix, Suffix: p.XSuffix, Name: "x", Domain: p.XDomain},
			{Label: p.YAxisLabel, Prefix: p.YPrefix, Suffix: p.YSuffix, Name: "y", Domain: p.YDomain},
		}
		ch.Note = p.Note
		ch.NoteOnEmpty = p.ShowNoteWhenEmpty
		ch.BinSize = int(p.BinSize)
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.HistogramViewProperties:
		ch.Kind = chartKindHistogram
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = convertColors(p.ViewColors)
		ch.FillColumns = p.FillColumns
		ch.XCol = p.XColumn
		ch.Axes = []axis{{Label: p.XAxisLabel, Name: "x", Domain: p.XDomain}}
		ch.Note = p.Note
		ch.NoteOnEmpty = p.ShowNoteWhenEmpty
		ch.BinCount = p.BinCount
		ch.Position = p.Position
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.MarkdownViewProperties:
		ch.Kind = chartKindMarkdown
		ch.Note = p.Note
	case influxdb.LinePlusSingleStatProperties:
		setCommon(chartKindSingleStatPlusLine, p.ViewColors, p.DecimalPlaces, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix)
		ch.StaticLegend = StaticLegend{}
		setStaticLegend(p.StaticLegend)
		ch.Axes = convertAxes(p.Axes)
		ch.Shade = p.ShadeBelow
		ch.HoverDimension = p.HoverDimension
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YCol = p.YColumn
		ch.GenerateYAxisTicks = p.GenerateYAxisTicks
		ch.YTotalTicks = p.YTotalTicks
		ch.YTickStart = p.YTickStart
		ch.YTickStep = p.YTickStep
		ch.Position = p.Position
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.SingleStatViewProperties:
		setCommon(chartKindSingleStat, p.ViewColors, p.DecimalPlaces, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix)
		ch.TickPrefix = p.TickPrefix
		ch.TickSuffix = p.TickSuffix
	case influxdb.MosaicViewProperties:
		ch.Kind = chartKindMosaic
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = stringsToColors(p.ViewColors)
		ch.HoverDimension = p.HoverDimension
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YLabelColumnSeparator = p.YLabelColumnSeparator
		ch.YLabelColumns = p.YLabelColumns
		ch.YSeriesColumns = p.YSeriesColumns
		ch.Axes = []axis{
			{Label: p.XAxisLabel, Prefix: p.XPrefix, Suffix: p.XSuffix, Name: "x", Domain: p.XDomain},
			{Label: p.YAxisLabel, Prefix: p.YPrefix, Suffix: p.YSuffix, Name: "y", Domain: p.YDomain},
		}
		ch.Note = p.Note
		ch.NoteOnEmpty = p.ShowNoteWhenEmpty
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.ScatterViewProperties:
		ch.Kind = chartKindScatter
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = stringsToColors(p.ViewColors)
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YCol = p.YColumn
		ch.GenerateYAxisTicks = p.GenerateYAxisTicks
		ch.YTotalTicks = p.YTotalTicks
		ch.YTickStart = p.YTickStart
		ch.YTickStep = p.YTickStep
		ch.Axes = []axis{
			{Label: p.XAxisLabel, Prefix: p.XPrefix, Suffix: p.XSuffix, Name: "x", Domain: p.XDomain},
			{Label: p.YAxisLabel, Prefix: p.YPrefix, Suffix: p.YSuffix, Name: "y", Domain: p.YDomain},
		}
		ch.Note = p.Note
		ch.NoteOnEmpty = p.ShowNoteWhenEmpty
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.TableViewProperties:
		setCommon(chartKindTable, p.ViewColors, p.DecimalPlaces, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "")
		ch.TimeFormat = p.TimeFormat
		ch.TableOptions = tableOptions{
			VerticalTimeAxis: p.TableOptions.VerticalTimeAxis,
			SortByField:      p.TableOptions.SortBy.InternalName,
			Wrapping:         p.TableOptions.Wrapping,
			FixFirstColumn:   p.TableOptions.FixFirstColumn,
		}
		for _, fieldOpt := range p.FieldOptions {
			ch.FieldOptions = append(ch.FieldOptions, fieldOption{
				FieldName:   fieldOpt.InternalName,
				DisplayName: fieldOpt.DisplayName,
				Visible:     fieldOpt.Visible,
			})
		}
	case influxdb.BandViewProperties:
		setCommon(chartKindBand, p.ViewColors, influxdb.DecimalPlaces{}, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "")
		ch.StaticLegend = StaticLegend{}
		setStaticLegend(p.StaticLegend)
		ch.Axes = convertAxes(p.Axes)
		ch.Geom = p.Geom
		ch.HoverDimension = p.HoverDimension
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YCol = p.YColumn
		ch.GenerateYAxisTicks = p.GenerateYAxisTicks
		ch.YTotalTicks = p.YTotalTicks
		ch.YTickStart = p.YTickStart
		ch.YTickStep = p.YTickStep
		ch.UpperColumn = p.UpperColumn
		ch.MainColumn = p.MainColumn
		ch.LowerColumn = p.LowerColumn
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	case influxdb.XYViewProperties:
		setCommon(chartKindXY, p.ViewColors, influxdb.DecimalPlaces{}, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "")
		ch.StaticLegend = StaticLegend{}
		setStaticLegend(p.StaticLegend)
		ch.Axes = convertAxes(p.Axes)
		ch.Geom = p.Geom
		ch.Shade = p.ShadeBelow
		ch.HoverDimension = p.HoverDimension
		ch.XCol = p.XColumn
		ch.GenerateXAxisTicks = p.GenerateXAxisTicks
		ch.XTotalTicks = p.XTotalTicks
		ch.XTickStart = p.XTickStart
		ch.XTickStep = p.XTickStep
		ch.YCol = p.YColumn
		ch.GenerateYAxisTicks = p.GenerateYAxisTicks
		ch.YTotalTicks = p.YTotalTicks
		ch.YTickStart = p.YTickStart
		ch.YTickStep = p.YTickStep
		ch.Position = p.Position
		ch.LegendColorizeRows = p.LegendColorizeRows
		ch.LegendHide = p.LegendHide
		ch.LegendOpacity = float64(p.LegendOpacity)
		ch.LegendOrientationThreshold = int(p.LegendOrientationThreshold)
	}

	sort.Slice(ch.Axes, func(i, j int) bool {
		return ch.Axes[i].Name < ch.Axes[j].Name
	})
	return ch
}

func convertChartToResource(ch chart) Resource {
	r := Resource{
		fieldKind:        ch.Kind.title(),
		fieldName:        ch.Name,
		fieldChartHeight: ch.Height,
		fieldChartWidth:  ch.Width,
	}
	var qq []Resource
	for _, q := range ch.Queries {
		qq = append(qq, Resource{
			fieldQuery: q.DashboardQuery(),
		})
	}
	if len(qq) > 0 {
		r[fieldChartQueries] = qq
	}
	if len(ch.Colors) > 0 {
		r[fieldChartColors] = ch.Colors
	}
	if len(ch.Axes) > 0 {
		r[fieldChartAxes] = ch.Axes
	}
	if len(ch.YLabelColumns) > 0 {
		r[fieldChartYLabelColumns] = ch.YLabelColumns
	}
	if len(ch.YSeriesColumns) > 0 {
		r[fieldChartYSeriesColumns] = ch.YSeriesColumns
	}
	if len(ch.UpperColumn) > 0 {
		r[fieldChartUpperColumn] = ch.UpperColumn
	}
	if len(ch.MainColumn) > 0 {
		r[fieldChartMainColumn] = ch.MainColumn
	}
	if len(ch.LowerColumn) > 0 {
		r[fieldChartLowerColumn] = ch.LowerColumn
	}
	if ch.EnforceDecimals {
		r[fieldChartDecimalPlaces] = ch.DecimalPlaces
	}

	if len(ch.FillColumns) > 0 {
		r[fieldChartFillColumns] = ch.FillColumns
	}

	if len(ch.GenerateXAxisTicks) > 0 {
		r[fieldChartGenerateXAxisTicks] = ch.GenerateXAxisTicks
	}

	if len(ch.GenerateYAxisTicks) > 0 {
		r[fieldChartGenerateYAxisTicks] = ch.GenerateYAxisTicks
	}

	if ch.StaticLegend.HeightRatio >= 0 && ch.StaticLegend.WidthRatio >= 0 {
		r[fieldChartStaticLegend] = ch.StaticLegend
	}

	if len(ch.GeoLayers) > 0 {
		geoLayers := make([]Resource, 0, len(ch.GeoLayers))
		for _, l := range ch.GeoLayers {
			lRes := make(Resource)
			geoLayers = append(geoLayers, lRes)
			assignNonZeroStrings(lRes, map[string]string{
				fieldChartGeoLayerType:           l.Type,
				fieldChartGeoLayerRadiusField:    l.RadiusField,
				fieldChartGeoLayerIntensityField: l.IntensityField,
				fieldChartGeoLayerColorField:     l.ColorField,
			})
			assignNonZeroInts(lRes, map[string]int{
				fieldChartGeoLayerRadius:     int(l.Radius),
				fieldChartGeoLayerBlur:       int(l.Blur),
				fieldChartGeoLayerSpeed:      int(l.Speed),
				fieldChartGeoLayerTrackWidth: int(l.TrackWidth),
			})
			assignNonZeroBools(lRes, map[string]bool{
				fieldChartGeoLayerRandomColors:      l.RandomColors,
				fieldChartGeoLayerIsClustered:       l.IsClustered,
				fieldChartGeoLayerInterpolateColors: l.InterpolateColors,
			})
			if len(l.ViewColors) > 0 {
				lRes[fieldChartGeoLayerViewColors] = l.ViewColors
			}
			if l.RadiusDimension != nil {
				lRes[fieldChartGeoLayerRadiusDimension] = l.RadiusDimension
			}
			if l.ColorDimension != nil {
				lRes[fieldChartGeoLayerColorDimension] = l.ColorDimension
			}
			if l.IntensityDimension != nil {
				lRes[fieldChartGeoLayerIntensityDimension] = l.IntensityDimension
			}
		}
		r[fieldChartGeoLayers] = geoLayers
	}

	if zero := new(tableOptions); ch.TableOptions != *zero {
		tRes := make(Resource)
		assignNonZeroBools(tRes, map[string]bool{
			fieldChartTableOptionVerticalTimeAxis: ch.TableOptions.VerticalTimeAxis,
			fieldChartTableOptionFixFirstColumn:   ch.TableOptions.FixFirstColumn,
		})
		assignNonZeroStrings(tRes, map[string]string{
			fieldChartTableOptionSortBy:   ch.TableOptions.SortByField,
			fieldChartTableOptionWrapping: ch.TableOptions.Wrapping,
		})
		r[fieldChartTableOptions] = tRes
	}

	if len(ch.FieldOptions) > 0 {
		fieldOpts := make([]Resource, 0, len(ch.FieldOptions))
		for _, fo := range ch.FieldOptions {
			fRes := make(Resource)
			assignNonZeroBools(fRes, map[string]bool{
				fieldChartFieldOptionVisible: fo.Visible,
			})
			assignNonZeroStrings(fRes, map[string]string{
				fieldChartFieldOptionDisplayName: fo.DisplayName,
				fieldChartFieldOptionFieldName:   fo.FieldName,
			})
			fieldOpts = append(fieldOpts, fRes)
		}
		r[fieldChartFieldOptions] = fieldOpts
	}

	assignNonZeroBools(r, map[string]bool{
		fieldChartNoteOnEmpty:               ch.NoteOnEmpty,
		fieldChartShade:                     ch.Shade,
		fieldChartLegendColorizeRows:        ch.LegendColorizeRows,
		fieldChartLegendHide:                ch.LegendHide,
		fieldChartStaticLegendColorizeRows:  ch.StaticLegend.ColorizeRows,
		fieldChartStaticLegendShow:          ch.StaticLegend.Show,
		fieldChartGeoAllowPanAndZoom:        ch.AllowPanAndZoom,
		fieldChartGeoDetectCoordinateFields: ch.DetectCoordinateFields,
	})

	assignNonZeroStrings(r, map[string]string{
		fieldChartNote:                  ch.Note,
		fieldPrefix:                     ch.Prefix,
		fieldSuffix:                     ch.Suffix,
		fieldChartGeom:                  ch.Geom,
		fieldChartXCol:                  ch.XCol,
		fieldChartYCol:                  ch.YCol,
		fieldChartPosition:              ch.Position,
		fieldChartTickPrefix:            ch.TickPrefix,
		fieldChartTickSuffix:            ch.TickSuffix,
		fieldChartTimeFormat:            ch.TimeFormat,
		fieldChartHoverDimension:        ch.HoverDimension,
		fieldChartYLabelColumnSeparator: ch.YLabelColumnSeparator,
		fieldChartStaticLegendValueAxis: ch.StaticLegend.ValueAxis,
		fieldChartGeoMapStyle:           ch.MapStyle,
	})

	assignNonZeroInts(r, map[string]int{
		fieldChartXPos:                             ch.XPos,
		fieldChartXTotalTicks:                      ch.XTotalTicks,
		fieldChartYPos:                             ch.YPos,
		fieldChartYTotalTicks:                      ch.YTotalTicks,
		fieldChartBinCount:                         ch.BinCount,
		fieldChartBinSize:                          ch.BinSize,
		fieldChartLegendOrientationThreshold:       ch.LegendOrientationThreshold,
		fieldChartStaticLegendOrientationThreshold: ch.StaticLegend.OrientationThreshold,
	})

	assignNonZeroFloats(r, map[string]float64{
		fieldChartLegendOpacity:           ch.LegendOpacity,
		fieldChartStaticLegendOpacity:     ch.StaticLegend.Opacity,
		fieldChartStaticLegendHeightRatio: ch.StaticLegend.HeightRatio,
		fieldChartStaticLegendWidthRatio:  ch.StaticLegend.WidthRatio,
		fieldChartXTickStart:              ch.XTickStart,
		fieldChartXTickStep:               ch.XTickStep,
		fieldChartYTickStart:              ch.YTickStart,
		fieldChartYTickStep:               ch.YTickStep,
		fieldChartGeoCenterLon:            ch.Center.Lon,
		fieldChartGeoCenterLat:            ch.Center.Lat,
		fieldChartGeoZoom:                 ch.Zoom,
	})

	return r
}

func convertAxis(name string, a influxdb.Axis) *axis {
	return &axis{
		Base:   a.Base,
		Label:  a.Label,
		Name:   name,
		Prefix: a.Prefix,
		Scale:  a.Scale,
		Suffix: a.Suffix,
	}
}

func convertAxes(iAxes map[string]influxdb.Axis) axes {
	out := make(axes, 0, len(iAxes))
	for name, a := range iAxes {
		out = append(out, *convertAxis(name, a))
	}
	return out
}

func convertColors(iColors []influxdb.ViewColor) colors {
	out := make(colors, 0, len(iColors))
	for _, ic := range iColors {
		out = append(out, &color{
			ID:    ic.ID,
			Name:  ic.Name,
			Type:  ic.Type,
			Hex:   ic.Hex,
			Value: flt64Ptr(ic.Value),
		})
	}
	return out
}

func convertQueries(iQueries []influxdb.DashboardQuery) queries {
	out := make(queries, 0, len(iQueries))
	for _, iq := range iQueries {
		out = append(out, query{Query: strings.TrimSpace(iq.Text)})
	}
	return out
}

func convertGeoLayers(iLayers []influxdb.GeoLayer) geoLayers {
	out := make(geoLayers, 0, len(iLayers))
	for _, ic := range iLayers {
		out = append(out, &geoLayer{
			Type:               ic.Type,
			RadiusField:        ic.RadiusField,
			ColorField:         ic.ColorField,
			IntensityField:     ic.IntensityField,
			ViewColors:         convertColors(ic.ViewColors),
			Radius:             ic.Radius,
			Blur:               ic.Blur,
			RadiusDimension:    convertAxis("radius", ic.RadiusDimension),
			ColorDimension:     convertAxis("color", ic.ColorDimension),
			IntensityDimension: convertAxis("intensity", ic.IntensityDimension),
			InterpolateColors:  ic.InterpolateColors,
			TrackWidth:         ic.TrackWidth,
			Speed:              ic.Speed,
			RandomColors:       ic.RandomColors,
			IsClustered:        ic.IsClustered,
		})
	}
	return out
}

// DashboardToObject converts an influxdb.Dashboard to an Object.
func DashboardToObject(name string, dash influxdb.Dashboard) Object {
	if name == "" {
		name = dash.Name
	}

	sort.Slice(dash.Cells, func(i, j int) bool {
		ic, jc := dash.Cells[i], dash.Cells[j]
		if ic.X == jc.X {
			return ic.Y < jc.Y
		}
		return ic.X < jc.X
	})

	charts := make([]Resource, 0, len(dash.Cells))
	for _, cell := range dash.Cells {
		if cell.View == nil {
			continue
		}
		ch := convertCellView(*cell)
		if !ch.Kind.ok() {
			continue
		}
		charts = append(charts, convertChartToResource(ch))
	}

	o := newObject(KindDashboard, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldDescription: dash.Description,
	})
	o.Spec[fieldDashCharts] = charts
	return o
}

// LabelToObject converts an influxdb.Label to an Object.
func LabelToObject(name string, l influxdb.Label) Object {
	if name == "" {
		name = l.Name
	}

	o := newObject(KindLabel, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldDescription: l.Properties["description"],
		fieldLabelColor:  l.Properties["color"],
	})
	return o
}

// NotificationEndpointToObject converts an notification endpoint into a pkger Object.
func NotificationEndpointToObject(name string, e influxdb.NotificationEndpoint) Object {
	if name == "" {
		name = e.GetName()
	}

	o := newObject(KindNotificationEndpoint, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldDescription: e.GetDescription(),
		fieldStatus:      string(e.GetStatus()),
	})

	switch actual := e.(type) {
	case *endpoint.HTTP:
		o.Kind = KindNotificationEndpointHTTP
		o.Spec[fieldNotificationEndpointHTTPMethod] = actual.Method
		o.Spec[fieldNotificationEndpointURL] = actual.URL
		o.Spec[fieldType] = actual.AuthMethod
		assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{
			fieldNotificationEndpointPassword: actual.Password,
			fieldNotificationEndpointToken:    actual.Token,
			fieldNotificationEndpointUsername: actual.Username,
		})
	case *endpoint.PagerDuty:
		o.Kind = KindNotificationEndpointPagerDuty
		o.Spec[fieldNotificationEndpointURL] = actual.ClientURL
		assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{
			fieldNotificationEndpointRoutingKey: actual.RoutingKey,
		})
	case *endpoint.Slack:
		o.Kind = KindNotificationEndpointSlack
		o.Spec[fieldNotificationEndpointURL] = actual.URL
		assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{
			fieldNotificationEndpointToken: actual.Token,
		})
	}

	return o
}

// NotificationRuleToObject converts an notification rule into a pkger Object.
func NotificationRuleToObject(name, endpointPkgName string, iRule influxdb.NotificationRule) Object {
	if name == "" {
		name = iRule.GetName()
	}

	o := newObject(KindNotificationRule, name)
	o.Spec[fieldNotificationRuleEndpointName] = endpointPkgName
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldDescription: iRule.GetDescription(),
	})

	assignBase := func(base rule.Base) {
		assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{
			fieldEvery:  base.Every,
			fieldOffset: base.Offset,
		})

		var tagRes []Resource
		for _, tRule := range base.TagRules {
			tagRes = append(tagRes, Resource{
				fieldKey:      tRule.Key,
				fieldValue:    tRule.Value,
				fieldOperator: tRule.Operator.String(),
			})
		}
		if len(tagRes) > 0 {
			o.Spec[fieldNotificationRuleTagRules] = tagRes
		}

		var statusRuleRes []Resource
		for _, sRule := range base.StatusRules {
			sRes := Resource{
				fieldNotificationRuleCurrentLevel: sRule.CurrentLevel.String(),
			}
			if sRule.PreviousLevel != nil {
				sRes[fieldNotificationRulePreviousLevel] = sRule.PreviousLevel.String()
			}
			statusRuleRes = append(statusRuleRes, sRes)
		}
		if len(statusRuleRes) > 0 {
			o.Spec[fieldNotificationRuleStatusRules] = statusRuleRes
		}
	}

	switch t := iRule.(type) {
	case *rule.HTTP:
		assignBase(t.Base)
	case *rule.PagerDuty:
		assignBase(t.Base)
		o.Spec[fieldNotificationRuleMessageTemplate] = t.MessageTemplate
	case *rule.Slack:
		assignBase(t.Base)
		o.Spec[fieldNotificationRuleMessageTemplate] = t.MessageTemplate
		assignNonZeroStrings(o.Spec, map[string]string{fieldNotificationRuleChannel: t.Channel})
	}

	return o
}

// regex used to rip out the hard coded task option stuffs
var taskFluxRegex = regexp.MustCompile(`option task = {(.|\n)*?}`)

// TaskToObject converts an influxdb.Task into a pkger.Object.
func TaskToObject(name string, t taskmodel.Task) Object {
	if name == "" {
		name = t.Name
	}

	query := strings.TrimSpace(taskFluxRegex.ReplaceAllString(t.Flux, ""))

	o := newObject(KindTask, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldTaskCron:    t.Cron,
		fieldDescription: t.Description,
		fieldEvery:       t.Every,
		fieldOffset:      durToStr(t.Offset),
		fieldQuery:       strings.TrimSpace(query),
	})
	return o
}

// TelegrafToObject converts an influxdb.TelegrafConfig into a pkger.Object.
func TelegrafToObject(name string, t influxdb.TelegrafConfig) Object {
	if name == "" {
		name = t.Name
	}

	o := newObject(KindTelegraf, name)
	assignNonZeroStrings(o.Spec, map[string]string{
		fieldTelegrafConfig: t.Config,
		fieldDescription:    t.Description,
	})
	return o
}

// VariableToObject converts an influxdb.Variable to a pkger.Object.
func VariableToObject(name string, v influxdb.Variable) Object {
	if name == "" {
		name = v.Name
	}

	o := newObject(KindVariable, name)

	assignNonZeroStrings(o.Spec, map[string]string{fieldDescription: v.Description})

	if len(v.Selected) > 0 {
		o.Spec[fieldVariableSelected] = v.Selected
	}

	args := v.Arguments
	if args == nil {
		return o
	}
	o.Spec[fieldType] = args.Type

	switch args.Type {
	case fieldArgTypeConstant:
		vals, ok := args.Values.(influxdb.VariableConstantValues)
		if ok {
			o.Spec[fieldValues] = []string(vals)
		}
	case fieldArgTypeMap:
		vals, ok := args.Values.(influxdb.VariableMapValues)
		if ok {
			o.Spec[fieldValues] = map[string]string(vals)
		}
	case fieldArgTypeQuery:
		vals, ok := args.Values.(influxdb.VariableQueryValues)
		if ok {
			o.Spec[fieldLanguage] = vals.Language
			o.Spec[fieldQuery] = strings.TrimSpace(vals.Query)
		}
	}

	return o
}

func newObject(kind Kind, name string) Object {
	return Object{
		APIVersion: APIVersion,
		Kind:       kind,
		Metadata: Resource{
			// this timestamp is added to make the resource unique. Should also indicate
			// to the end user that this is machine readable and the spec.name field is
			// the one they want to edit when a name change is desired.
			fieldName: strings.ToLower(idGenerator.ID().String()),
		},
		Spec: Resource{
			fieldName: name,
		},
	}
}

func assignNonZeroFluxDurs(r Resource, m map[string]*notification.Duration) {
	for field, dur := range m {
		if dur == nil {
			continue
		}
		if dur.TimeDuration() == 0 {
			continue
		}
		r[field] = dur.TimeDuration().String()
	}
}

func assignNonZeroBools(r Resource, m map[string]bool) {
	for k, v := range m {
		if v {
			r[k] = v
		}
	}
}

func assignNonZeroInts(r Resource, m map[string]int) {
	for k, v := range m {
		if v != 0 {
			r[k] = v
		}
	}
}

func assignNonZeroFloats(r Resource, m map[string]float64) {
	for k, v := range m {
		if v != 0 {
			r[k] = v
		}
	}
}

func assignNonZeroStrings(r Resource, m map[string]string) {
	for k, v := range m {
		if v != "" {
			r[k] = v
		}
	}
}

func assignNonZeroSecrets(r Resource, m map[string]influxdb.SecretField) {
	for field, secret := range m {
		if secret.Key == "" {
			continue
		}
		r[field] = Resource{
			fieldReferencesSecret: Resource{
				fieldKey: secret.Key,
			},
		}
	}
}

func stringsToColors(clrs []string) colors {
	newColors := make(colors, 0)
	for _, x := range clrs {
		newColors = append(newColors, &color{Hex: x})
	}
	return newColors
}
