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/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"
)

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   influxdb.ID `json:"id"`
	Name string      `json:"name"`
}

// OK validates a resource clone is viable.
func (r ResourceToClone) OK() error {
	if err := r.Kind.OK(); err != nil {
		return err
	}
	if r.ID == influxdb.ID(0) {
		return errors.New("must provide an ID")
	}
	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 influxdb.ID
	id    influxdb.ID
	name  string
	kind  Kind
}

func newExportKey(orgID, id influxdb.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     influxdb.TaskService
	teleSVC     influxdb.TelegrafConfigStore
	varSVC      influxdb.VariableService

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

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),
	}
}

func (ex *resourceExporter) Export(ctx context.Context, resourcesToClone []ResourceToClone, labelNames ...string) error {
	cloneAssFn, err := ex.resourceCloneAssociationsGen(ctx, 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)
	}

	sort.Slice(objects, func(i, j int) bool {
		iName, jName := objects[i].Name(), objects[j].Name()
		iKind, jKind := objects[i].Kind, objects[j].Kind

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

	return objects
}

func (ex *resourceExporter) uniqByNameResID() influxdb.ID {
	// 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 = 0
	return uniqByNameResID
}

type cloneAssociationsFn func(context.Context, ResourceToClone) (associations []Resource, 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 influxdb.ID, k Kind, object Object) {
		// overwrite the default metadata.name field with export generated one here
		object.Metadata[fieldName] = ex.uniqName()

		if len(ass) > 0 {
			object.Spec[fieldAssociations] = ass
		}
		key := newExportKey(orgID, uniqResID, k, object.Spec.stringShort(fieldName))
		ex.mObjects[key] = object
	}

	uniqByNameResID := ex.uniqByNameResID()

	switch {
	case r.Kind.is(KindBucket):
		bkt, err := ex.bucketSVC.FindBucketByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(bkt.OrgID, uniqByNameResID, KindBucket, BucketToObject(r.Name, *bkt))
	case r.Kind.is(KindCheck),
		r.Kind.is(KindCheckDeadman),
		r.Kind.is(KindCheckThreshold):
		ch, err := ex.checkSVC.FindCheckByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(ch.GetOrgID(), uniqByNameResID, KindCheck, CheckToObject(r.Name, ch))
	case r.Kind.is(KindDashboard):
		dash, err := ex.findDashboardByIDFull(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(dash.OrganizationID, dash.ID, KindDashboard, DashboardToObject(r.Name, *dash))
	case r.Kind.is(KindLabel):
		l, err := ex.labelSVC.FindLabelByID(ctx, r.ID)
		if err != nil {
			return err
		}
		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):
		e, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(e.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject(r.Name, e))
	case r.Kind.is(KindNotificationRule):
		rule, ruleEndpoint, err := ex.getEndpointRule(ctx, r.ID)
		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):
		t, err := ex.taskSVC.FindTaskByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t))
	case r.Kind.is(KindTelegraf):
		t, err := ex.teleSVC.FindTelegrafConfigByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t))
	case r.Kind.is(KindVariable):
		v, err := ex.varSVC.FindVariableByID(ctx, r.ID)
		if err != nil {
			return err
		}
		mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v))
	default:
		return errors.New("unsupported kind provided: " + string(r.Kind))
	}

	return nil
}

func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, 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) ([]Resource, 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
		}

		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 []Resource
		for _, l := range labels {
			if len(mLabelNames) > 0 {
				if _, ok := mLabelNames[l.Name]; !ok {
					continue
				}
			}

			labelObject := LabelToObject("", *l)
			labelObject.Metadata[fieldName] = ex.uniqName()

			k := newExportKey(l.OrgID, ex.uniqByNameResID(), KindLabel, l.Name)
			existing, ok := ex.mObjects[k]
			if ok {
				associations = append(associations, Resource{
					fieldKind: KindLabel.String(),
					fieldName: existing.Name(),
				})
				continue
			}
			associations = append(associations, Resource{
				fieldKind: KindLabel.String(),
				fieldName: labelObject.Name(),
			})
			ex.mObjects[k] = labelObject
		}
		return associations, false, nil
	}

	return cloneFn, nil
}

func (ex *resourceExporter) getEndpointRule(ctx context.Context, id influxdb.ID) (influxdb.NotificationRule, influxdb.NotificationEndpoint, error) {
	rule, err := ex.ruleSVC.FindNotificationRuleByID(ctx, id)
	if err != nil {
		return nil, nil, err
	}

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

	return rule, ruleEndpoint, nil
}

func (ex *resourceExporter) findDashboardByIDFull(ctx context.Context, id influxdb.ID) (*influxdb.Dashboard, error) {
	dash, err := ex.dashSVC.FindDashboardByID(ctx, id)
	if err != nil {
		return nil, err
	}
	for _, cell := range dash.Cells {
		v, err := ex.dashSVC.GetDashboardCellView(ctx, id, cell.ID)
		if err != nil {
			return nil, err
		}
		cell.View = v
	}
	return dash, nil
}

func (ex *resourceExporter) uniqName() string {
	uuid := strings.ToLower(idGenerator.ID().String())
	for i := 1; i < 250; i++ {
		name := fmt.Sprintf("%s-%s", ex.nameGen(), uuid[10:])
		if !ex.mPkgNames[name] {
			return name
		}
	}
	// if all else fails, generate a UUID for the name
	return uuid
}

func uniqResourcesToClone(resources []ResourceToClone) []ResourceToClone {
	type key struct {
		kind Kind
		id   influxdb.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:
		case ok && 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:      influxdb.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
	}

	setLegend := func(l influxdb.Legend) {
		ch.Legend.Orientation = l.Orientation
		ch.Legend.Type = l.Type
	}

	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.HeatmapViewProperties:
		ch.Kind = chartKindHeatMap
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = stringsToColors(p.ViewColors)
		ch.XCol = p.XColumn
		ch.YCol = p.YColumn
		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)
	case influxdb.HistogramViewProperties:
		ch.Kind = chartKindHistogram
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = convertColors(p.ViewColors)
		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
	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)
		setLegend(p.Legend)
		ch.Axes = convertAxes(p.Axes)
		ch.Shade = p.ShadeBelow
		ch.XCol = p.XColumn
		ch.YCol = p.YColumn
		ch.Position = p.Position
	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.ScatterViewProperties:
		ch.Kind = chartKindScatter
		ch.Queries = convertQueries(p.Queries)
		ch.Colors = stringsToColors(p.ViewColors)
		ch.XCol = p.XColumn
		ch.YCol = p.YColumn
		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
	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.XYViewProperties:
		setCommon(chartKindXY, p.ViewColors, influxdb.DecimalPlaces{}, p.Queries)
		setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "")
		setLegend(p.Legend)
		ch.Axes = convertAxes(p.Axes)
		ch.Geom = p.Geom
		ch.Shade = p.ShadeBelow
		ch.XCol = p.XColumn
		ch.YCol = p.YColumn
		ch.Position = p.Position
	}

	return ch
}

func convertChartToResource(ch chart) Resource {
	r := Resource{
		fieldKind:        ch.Kind.title(),
		fieldName:        ch.Name,
		fieldChartHeight: ch.Height,
		fieldChartWidth:  ch.Width,
	}
	if len(ch.Queries) > 0 {
		r[fieldChartQueries] = ch.Queries
	}
	if len(ch.Colors) > 0 {
		r[fieldChartColors] = ch.Colors
	}
	if len(ch.Axes) > 0 {
		r[fieldChartAxes] = ch.Axes
	}
	if ch.EnforceDecimals {
		r[fieldChartDecimalPlaces] = ch.DecimalPlaces
	}

	if ch.Legend.Type != "" {
		r[fieldChartLegend] = ch.Legend
	}

	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,
	})

	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,
	})

	assignNonZeroInts(r, map[string]int{
		fieldChartXPos:     ch.XPos,
		fieldChartYPos:     ch.YPos,
		fieldChartBinCount: ch.BinCount,
		fieldChartBinSize:  ch.BinSize,
	})

	return r
}

func convertAxes(iAxes map[string]influxdb.Axis) axes {
	out := make(axes, 0, len(iAxes))
	for name, a := range iAxes {
		out = append(out, axis{
			Base:   a.Base,
			Label:  a.Label,
			Name:   name,
			Prefix: a.Prefix,
			Scale:  a.Scale,
			Suffix: a.Suffix,
		})
	}
	return out
}

func convertColors(iColors []influxdb.ViewColor) colors {
	out := make(colors, 0, len(iColors))
	for _, ic := range iColors {
		out = append(out, &color{
			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
}

// 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.ID == influxdb.ID(0) {
			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 influxdb.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})

	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 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
}
