package pkger

import (
	"reflect"
	"sort"

	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform"
	"github.com/influxdata/influxdb/v2/notification/rule"
	"github.com/influxdata/influxdb/v2/task/taskmodel"
)

type stateCoordinator struct {
	mBuckets    map[string]*stateBucket
	mChecks     map[string]*stateCheck
	mDashboards map[string]*stateDashboard
	mEndpoints  map[string]*stateEndpoint
	mLabels     map[string]*stateLabel
	mRules      map[string]*stateRule
	mTasks      map[string]*stateTask
	mTelegrafs  map[string]*stateTelegraf
	mVariables  map[string]*stateVariable

	labelMappings         []stateLabelMapping
	labelMappingsToRemove []stateLabelMappingForRemoval
}

func newStateCoordinator(template *Template, acts resourceActions) *stateCoordinator {
	state := stateCoordinator{
		mBuckets:    make(map[string]*stateBucket),
		mChecks:     make(map[string]*stateCheck),
		mDashboards: make(map[string]*stateDashboard),
		mEndpoints:  make(map[string]*stateEndpoint),
		mLabels:     make(map[string]*stateLabel),
		mRules:      make(map[string]*stateRule),
		mTasks:      make(map[string]*stateTask),
		mTelegrafs:  make(map[string]*stateTelegraf),
		mVariables:  make(map[string]*stateVariable),
	}

	// labels are done first to validate dependencies are accounted for.
	// when a label is skipped by an action, this will still be accurate
	// for hte individual labels, and cascades to the resources that are
	// associated to a label.
	for _, l := range template.labels() {
		if acts.skipResource(KindLabel, l.MetaName()) {
			continue
		}
		state.mLabels[l.MetaName()] = &stateLabel{
			parserLabel: l,
			stateStatus: StateStatusNew,
		}
	}
	for _, b := range template.buckets() {
		if acts.skipResource(KindBucket, b.MetaName()) {
			continue
		}
		state.mBuckets[b.MetaName()] = &stateBucket{
			parserBkt:         b,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(b.labels),
		}
	}
	for _, c := range template.checks() {
		if acts.skipResource(KindCheck, c.MetaName()) {
			continue
		}
		state.mChecks[c.MetaName()] = &stateCheck{
			parserCheck:       c,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(c.labels),
		}
	}
	for _, d := range template.dashboards() {
		if acts.skipResource(KindDashboard, d.MetaName()) {
			continue
		}
		state.mDashboards[d.MetaName()] = &stateDashboard{
			parserDash:        d,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(d.labels),
		}
	}
	for _, e := range template.notificationEndpoints() {
		if acts.skipResource(KindNotificationEndpoint, e.MetaName()) {
			continue
		}
		state.mEndpoints[e.MetaName()] = &stateEndpoint{
			parserEndpoint:    e,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(e.labels),
		}
	}
	for _, r := range template.notificationRules() {
		if acts.skipResource(KindNotificationRule, r.MetaName()) {
			continue
		}
		state.mRules[r.MetaName()] = &stateRule{
			parserRule:        r,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(r.labels),
		}
	}
	for _, task := range template.tasks() {
		if acts.skipResource(KindTask, task.MetaName()) {
			continue
		}
		state.mTasks[task.MetaName()] = &stateTask{
			parserTask:        task,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(task.labels),
		}
	}
	for _, tele := range template.telegrafs() {
		if acts.skipResource(KindTelegraf, tele.MetaName()) {
			continue
		}
		state.mTelegrafs[tele.MetaName()] = &stateTelegraf{
			parserTelegraf:    tele,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(tele.labels),
		}
	}
	for _, v := range template.variables() {
		if acts.skipResource(KindVariable, v.MetaName()) {
			continue
		}
		state.mVariables[v.MetaName()] = &stateVariable{
			parserVar:         v,
			stateStatus:       StateStatusNew,
			labelAssociations: state.templateToStateLabels(v.labels),
		}
	}

	return &state
}

func (s *stateCoordinator) buckets() []*stateBucket {
	out := make([]*stateBucket, 0, len(s.mBuckets))
	for _, v := range s.mBuckets {
		out = append(out, v)
	}
	return out
}

func (s *stateCoordinator) checks() []*stateCheck {
	out := make([]*stateCheck, 0, len(s.mChecks))
	for _, v := range s.mChecks {
		out = append(out, v)
	}
	return out
}

func (s *stateCoordinator) dashboards() []*stateDashboard {
	out := make([]*stateDashboard, 0, len(s.mDashboards))
	for _, d := range s.mDashboards {
		out = append(out, d)
	}
	return out
}

func (s *stateCoordinator) endpoints() []*stateEndpoint {
	out := make([]*stateEndpoint, 0, len(s.mEndpoints))
	for _, e := range s.mEndpoints {
		out = append(out, e)
	}
	return out
}

func (s *stateCoordinator) labels() []*stateLabel {
	out := make([]*stateLabel, 0, len(s.mLabels))
	for _, v := range s.mLabels {
		out = append(out, v)
	}
	return out
}

func (s *stateCoordinator) rules() []*stateRule {
	out := make([]*stateRule, 0, len(s.mRules))
	for _, r := range s.mRules {
		out = append(out, r)
	}
	return out
}

func (s *stateCoordinator) tasks() []*stateTask {
	out := make([]*stateTask, 0, len(s.mTasks))
	for _, t := range s.mTasks {
		out = append(out, t)
	}
	return out
}

func (s *stateCoordinator) telegrafConfigs() []*stateTelegraf {
	out := make([]*stateTelegraf, 0, len(s.mTelegrafs))
	for _, t := range s.mTelegrafs {
		out = append(out, t)
	}
	return out
}

func (s *stateCoordinator) variables() []*stateVariable {
	out := make([]*stateVariable, 0, len(s.mVariables))
	for _, v := range s.mVariables {
		out = append(out, v)
	}
	return out
}

func (s *stateCoordinator) diff() Diff {
	var diff Diff
	for _, b := range s.mBuckets {
		diff.Buckets = append(diff.Buckets, b.diffBucket())
	}
	sort.Slice(diff.Buckets, func(i, j int) bool {
		return diff.Buckets[i].MetaName < diff.Buckets[j].MetaName
	})

	for _, c := range s.mChecks {
		diff.Checks = append(diff.Checks, c.diffCheck())
	}
	sort.Slice(diff.Checks, func(i, j int) bool {
		return diff.Checks[i].MetaName < diff.Checks[j].MetaName
	})

	for _, d := range s.mDashboards {
		diff.Dashboards = append(diff.Dashboards, d.diffDashboard())
	}
	sort.Slice(diff.Dashboards, func(i, j int) bool {
		return diff.Dashboards[i].MetaName < diff.Dashboards[j].MetaName
	})

	for _, e := range s.mEndpoints {
		diff.NotificationEndpoints = append(diff.NotificationEndpoints, e.diffEndpoint())
	}
	sort.Slice(diff.NotificationEndpoints, func(i, j int) bool {
		return diff.NotificationEndpoints[i].MetaName < diff.NotificationEndpoints[j].MetaName
	})

	for _, l := range s.mLabels {
		diff.Labels = append(diff.Labels, l.diffLabel())
	}
	sort.Slice(diff.Labels, func(i, j int) bool {
		return diff.Labels[i].MetaName < diff.Labels[j].MetaName
	})

	for _, r := range s.mRules {
		diff.NotificationRules = append(diff.NotificationRules, r.diffRule())
	}
	sort.Slice(diff.NotificationRules, func(i, j int) bool {
		return diff.NotificationRules[i].MetaName < diff.NotificationRules[j].MetaName
	})

	for _, t := range s.mTasks {
		diff.Tasks = append(diff.Tasks, t.diffTask())
	}
	sort.Slice(diff.Tasks, func(i, j int) bool {
		return diff.Tasks[i].MetaName < diff.Tasks[j].MetaName
	})

	for _, t := range s.mTelegrafs {
		diff.Telegrafs = append(diff.Telegrafs, t.diffTelegraf())
	}
	sort.Slice(diff.Telegrafs, func(i, j int) bool {
		return diff.Telegrafs[i].MetaName < diff.Telegrafs[j].MetaName
	})

	for _, v := range s.mVariables {
		diff.Variables = append(diff.Variables, v.diffVariable())
	}
	sort.Slice(diff.Variables, func(i, j int) bool {
		return diff.Variables[i].MetaName < diff.Variables[j].MetaName
	})

	for _, m := range s.labelMappings {
		diff.LabelMappings = append(diff.LabelMappings, m.diffLabelMapping())
	}
	for _, m := range s.labelMappingsToRemove {
		diff.LabelMappings = append(diff.LabelMappings, m.diffLabelMapping())
	}

	sort.Slice(diff.LabelMappings, func(i, j int) bool {
		n, m := diff.LabelMappings[i], diff.LabelMappings[j]
		if n.ResType < m.ResType {
			return true
		}
		if n.ResType > m.ResType {
			return false
		}
		if n.ResMetaName < m.ResMetaName {
			return true
		}
		if n.ResMetaName > m.ResMetaName {
			return false
		}
		return n.LabelName < m.LabelName
	})

	return diff
}

func (s *stateCoordinator) summary() Summary {
	var sum Summary
	for _, v := range s.mBuckets {
		if IsRemoval(v.stateStatus) {
			continue
		}
		sum.Buckets = append(sum.Buckets, v.summarize())
	}
	sort.Slice(sum.Buckets, func(i, j int) bool {
		return sum.Buckets[i].MetaName < sum.Buckets[j].MetaName
	})

	for _, c := range s.mChecks {
		if IsRemoval(c.stateStatus) {
			continue
		}
		sum.Checks = append(sum.Checks, c.summarize())
	}
	sort.Slice(sum.Checks, func(i, j int) bool {
		return sum.Checks[i].MetaName < sum.Checks[j].MetaName
	})

	for _, d := range s.mDashboards {
		if IsRemoval(d.stateStatus) {
			continue
		}
		sum.Dashboards = append(sum.Dashboards, d.summarize())
	}
	sort.Slice(sum.Dashboards, func(i, j int) bool {
		return sum.Dashboards[i].MetaName < sum.Dashboards[j].MetaName
	})

	for _, e := range s.mEndpoints {
		if IsRemoval(e.stateStatus) {
			continue
		}
		sum.NotificationEndpoints = append(sum.NotificationEndpoints, e.summarize())
	}
	sort.Slice(sum.NotificationEndpoints, func(i, j int) bool {
		return sum.NotificationEndpoints[i].MetaName < sum.NotificationEndpoints[j].MetaName
	})

	for _, v := range s.mLabels {
		if IsRemoval(v.stateStatus) {
			continue
		}
		sum.Labels = append(sum.Labels, v.summarize())
	}
	sort.Slice(sum.Labels, func(i, j int) bool {
		return sum.Labels[i].MetaName < sum.Labels[j].MetaName
	})

	for _, v := range s.mRules {
		if IsRemoval(v.stateStatus) {
			continue
		}
		sum.NotificationRules = append(sum.NotificationRules, v.summarize())
	}
	sort.Slice(sum.NotificationRules, func(i, j int) bool {
		return sum.NotificationRules[i].MetaName < sum.NotificationRules[j].MetaName
	})

	for _, t := range s.mTasks {
		if IsRemoval(t.stateStatus) {
			continue
		}
		sum.Tasks = append(sum.Tasks, t.summarize())
	}
	sort.Slice(sum.Tasks, func(i, j int) bool {
		return sum.Tasks[i].MetaName < sum.Tasks[j].MetaName
	})

	for _, t := range s.mTelegrafs {
		if IsRemoval(t.stateStatus) {
			continue
		}
		sum.TelegrafConfigs = append(sum.TelegrafConfigs, t.summarize())
	}
	sort.Slice(sum.TelegrafConfigs, func(i, j int) bool {
		return sum.TelegrafConfigs[i].MetaName < sum.TelegrafConfigs[j].MetaName
	})

	for _, v := range s.mVariables {
		if IsRemoval(v.stateStatus) {
			continue
		}
		sum.Variables = append(sum.Variables, v.summarize())
	}
	sort.Slice(sum.Variables, func(i, j int) bool {
		return sum.Variables[i].MetaName < sum.Variables[j].MetaName
	})

	for _, v := range s.labelMappings {
		sum.LabelMappings = append(sum.LabelMappings, v.summarize())
	}
	sort.Slice(sum.LabelMappings, func(i, j int) bool {
		n, m := sum.LabelMappings[i], sum.LabelMappings[j]
		if n.ResourceType != m.ResourceType {
			return n.ResourceType < m.ResourceType
		}
		if n.ResourceMetaName != m.ResourceMetaName {
			return n.ResourceMetaName < m.ResourceMetaName
		}
		return n.LabelName < m.LabelName
	})

	return sum
}

func (s *stateCoordinator) getLabelByMetaName(metaName string) (*stateLabel, bool) {
	l, ok := s.mLabels[metaName]
	return l, ok
}

func (s *stateCoordinator) templateToStateLabels(labels []*label) []*stateLabel {
	var out []*stateLabel
	for _, l := range labels {
		stLabel, found := s.getLabelByMetaName(l.MetaName())
		if !found {
			continue
		}
		out = append(out, stLabel)
	}
	return out
}

func (s *stateCoordinator) addStackState(stack Stack) {
	reconcilers := []func([]StackResource){
		s.reconcileStackResources,
		s.reconcileLabelMappings,
		s.reconcileNotificationDependencies,
	}
	for _, reconcileFn := range reconcilers {
		reconcileFn(stack.LatestEvent().Resources)
	}
}

func (s *stateCoordinator) reconcileStackResources(stackResources []StackResource) {
	for _, r := range stackResources {
		if !s.Contains(r.Kind, r.MetaName) {
			s.addObjectForRemoval(r.Kind, r.MetaName, r.ID)
			continue
		}
		s.setObjectID(r.Kind, r.MetaName, r.ID)
	}
}

func (s *stateCoordinator) reconcileLabelMappings(stackResources []StackResource) {
	mLabelMetaNameToID := make(map[string]platform.ID)
	for _, r := range stackResources {
		if r.Kind.is(KindLabel) {
			mLabelMetaNameToID[r.MetaName] = r.ID
		}
	}

	for _, r := range stackResources {
		labels := s.labelAssociations(r.Kind, r.MetaName)
		if len(r.Associations) == 0 {
			continue
		}

		// if associations agree => do nothing
		// if associations are new (in state not in stack) => do nothing
		// if associations are not in state and in stack => add them for removal
		mStackAss := make(map[StackResourceAssociation]struct{})
		for _, ass := range r.Associations {
			if ass.Kind.is(KindLabel) {
				mStackAss[ass] = struct{}{}
			}
		}

		for _, l := range labels {
			// we want to keep associations that are from previous application and are not changing
			delete(mStackAss, StackResourceAssociation{
				Kind:     KindLabel,
				MetaName: l.parserLabel.MetaName(),
			})
		}

		// all associations that are in the stack but not in the
		// state fall into here and are marked for removal.
		for assForRemoval := range mStackAss {
			s.labelMappingsToRemove = append(s.labelMappingsToRemove, stateLabelMappingForRemoval{
				LabelMetaName:    assForRemoval.MetaName,
				LabelID:          mLabelMetaNameToID[assForRemoval.MetaName],
				ResourceID:       r.ID,
				ResourceMetaName: r.MetaName,
				ResourceType:     r.Kind.ResourceType(),
			})
		}
	}
}

func (s *stateCoordinator) reconcileNotificationDependencies(stackResources []StackResource) {
	for _, r := range stackResources {
		if r.Kind.is(KindNotificationRule) {
			for _, ass := range r.Associations {
				if ass.Kind.is(KindNotificationEndpoint) {
					s.mRules[r.MetaName].associatedEndpoint = s.mEndpoints[ass.MetaName]
					break
				}
			}
		}
	}
}

func (s *stateCoordinator) get(k Kind, metaName string) (interface{}, bool) {
	switch k {
	case KindBucket:
		v, ok := s.mBuckets[metaName]
		return v, ok
	case KindCheck, KindCheckDeadman, KindCheckThreshold:
		v, ok := s.mChecks[metaName]
		return v, ok
	case KindDashboard:
		v, ok := s.mDashboards[metaName]
		return v, ok
	case KindLabel:
		v, ok := s.mLabels[metaName]
		return v, ok
	case KindNotificationEndpoint,
		KindNotificationEndpointHTTP,
		KindNotificationEndpointPagerDuty,
		KindNotificationEndpointSlack:
		v, ok := s.mEndpoints[metaName]
		return v, ok
	case KindNotificationRule:
		v, ok := s.mRules[metaName]
		return v, ok
	case KindTask:
		v, ok := s.mTasks[metaName]
		return v, ok
	case KindTelegraf:
		v, ok := s.mTelegrafs[metaName]
		return v, ok
	case KindVariable:
		v, ok := s.mVariables[metaName]
		return v, ok
	default:
		return nil, false
	}
}

func (s *stateCoordinator) labelAssociations(k Kind, metaName string) []*stateLabel {
	v, _ := s.get(k, metaName)
	labeler, ok := v.(interface {
		labels() []*stateLabel
	})
	if !ok {
		return nil
	}

	return labeler.labels()
}

func (s *stateCoordinator) Contains(k Kind, metaName string) bool {
	_, ok := s.get(k, metaName)
	return ok
}

// setObjectID sets the id for the resource graphed from the object the key identifies.
func (s *stateCoordinator) setObjectID(k Kind, metaName string, id platform.ID) {
	idSetFn, ok := s.getObjectIDSetter(k, metaName)
	if !ok {
		return
	}
	idSetFn(id)
}

// addObjectForRemoval sets the id for the resource graphed from the object the key identifies.
// The metaName and kind are used as the unique identifier, when calling this it will
// overwrite any existing value if one exists. If desired, check for the value by using
// the Contains method.
func (s *stateCoordinator) addObjectForRemoval(k Kind, metaName string, id platform.ID) {
	newIdentity := identity{
		name: &references{val: metaName},
	}

	switch k {
	case KindBucket:
		s.mBuckets[metaName] = &stateBucket{
			id:          id,
			parserBkt:   &bucket{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindCheck, KindCheckDeadman, KindCheckThreshold:
		s.mChecks[metaName] = &stateCheck{
			id:          id,
			parserCheck: &check{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindDashboard:
		s.mDashboards[metaName] = &stateDashboard{
			id:          id,
			parserDash:  &dashboard{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindLabel:
		s.mLabels[metaName] = &stateLabel{
			id:          id,
			parserLabel: &label{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindNotificationEndpoint,
		KindNotificationEndpointHTTP,
		KindNotificationEndpointPagerDuty,
		KindNotificationEndpointSlack:
		s.mEndpoints[metaName] = &stateEndpoint{
			id:             id,
			parserEndpoint: &notificationEndpoint{identity: newIdentity},
			stateStatus:    StateStatusRemove,
		}
	case KindNotificationRule:
		s.mRules[metaName] = &stateRule{
			id:          id,
			parserRule:  &notificationRule{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindTask:
		s.mTasks[metaName] = &stateTask{
			id:          id,
			parserTask:  &task{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	case KindTelegraf:
		s.mTelegrafs[metaName] = &stateTelegraf{
			id:             id,
			parserTelegraf: &telegraf{identity: newIdentity},
			stateStatus:    StateStatusRemove,
		}
	case KindVariable:
		s.mVariables[metaName] = &stateVariable{
			id:          id,
			parserVar:   &variable{identity: newIdentity},
			stateStatus: StateStatusRemove,
		}
	}
}

func (s *stateCoordinator) getObjectIDSetter(k Kind, metaName string) (func(platform.ID), bool) {
	switch k {
	case KindBucket:
		r, ok := s.mBuckets[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindCheck, KindCheckDeadman, KindCheckThreshold:
		r, ok := s.mChecks[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindDashboard:
		r, ok := s.mDashboards[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindLabel:
		r, ok := s.mLabels[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindNotificationEndpoint,
		KindNotificationEndpointHTTP,
		KindNotificationEndpointPagerDuty,
		KindNotificationEndpointSlack:
		r, ok := s.mEndpoints[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindNotificationRule:
		r, ok := s.mRules[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindTask:
		r, ok := s.mTasks[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindTelegraf:
		r, ok := s.mTelegrafs[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	case KindVariable:
		r, ok := s.mVariables[metaName]
		return func(id platform.ID) {
			r.id = id
			r.stateStatus = StateStatusExists
		}, ok
	default:
		return nil, false
	}
}

type stateIdentity struct {
	id           platform.ID
	name         string
	metaName     string
	resourceType influxdb.ResourceType
	stateStatus  StateStatus
}

func (s stateIdentity) exists() bool {
	return IsExisting(s.stateStatus)
}

type stateBucket struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserBkt *bucket
	existing  *influxdb.Bucket
}

func (b *stateBucket) diffBucket() DiffBucket {
	diff := DiffBucket{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindBucket,
			ID:          SafeID(b.ID()),
			StateStatus: b.stateStatus,
			MetaName:    b.parserBkt.MetaName(),
		},
		New: DiffBucketValues{
			Name:           b.parserBkt.Name(),
			Description:    b.parserBkt.Description,
			RetentionRules: b.parserBkt.RetentionRules,
		},
	}
	if e := b.existing; e != nil {
		diff.Old = &DiffBucketValues{
			Name:        e.Name,
			Description: e.Description,
		}
		if e.RetentionPeriod > 0 {
			diff.Old.RetentionRules = retentionRules{newRetentionRule(e.RetentionPeriod)}
		}
	}
	return diff
}

func stateToSummaryLabels(labels []*stateLabel) []SummaryLabel {
	out := make([]SummaryLabel, 0, len(labels))
	for _, l := range labels {
		out = append(out, l.summarize())
	}
	return out
}

func (b *stateBucket) summarize() SummaryBucket {
	sum := b.parserBkt.summarize()
	sum.ID = SafeID(b.ID())
	sum.OrgID = SafeID(b.orgID)
	sum.LabelAssociations = stateToSummaryLabels(b.labelAssociations)
	return sum
}

func (b *stateBucket) ID() platform.ID {
	if !IsNew(b.stateStatus) && b.existing != nil {
		return b.existing.ID
	}
	return b.id
}

func (b *stateBucket) resourceType() influxdb.ResourceType {
	return KindBucket.ResourceType()
}

func (b *stateBucket) labels() []*stateLabel {
	return b.labelAssociations
}

func (b *stateBucket) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           b.ID(),
		name:         b.parserBkt.Name(),
		metaName:     b.parserBkt.MetaName(),
		resourceType: b.resourceType(),
		stateStatus:  b.stateStatus,
	}
}

func (b *stateBucket) shouldApply() bool {
	return IsRemoval(b.stateStatus) ||
		b.existing == nil ||
		b.parserBkt.Description != b.existing.Description ||
		b.parserBkt.Name() != b.existing.Name ||
		b.parserBkt.RetentionRules.RP() != b.existing.RetentionPeriod
}

type stateCheck struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserCheck *check
	existing    influxdb.Check
}

func (c *stateCheck) ID() platform.ID {
	if !IsNew(c.stateStatus) && c.existing != nil {
		return c.existing.GetID()
	}
	return c.id
}

func (c *stateCheck) labels() []*stateLabel {
	return c.labelAssociations
}

func (c *stateCheck) resourceType() influxdb.ResourceType {
	return KindCheck.ResourceType()
}

func (c *stateCheck) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           c.ID(),
		name:         c.parserCheck.Name(),
		metaName:     c.parserCheck.MetaName(),
		resourceType: c.resourceType(),
		stateStatus:  c.stateStatus,
	}
}

func (c *stateCheck) diffCheck() DiffCheck {
	diff := DiffCheck{
		DiffIdentifier: DiffIdentifier{
			ID:          SafeID(c.ID()),
			StateStatus: c.stateStatus,
			MetaName:    c.parserCheck.MetaName(),
		},
	}
	newCheck := c.summarize()
	diff.Kind = newCheck.Kind
	if newCheck.Check != nil {
		diff.New.Check = newCheck.Check
	}
	if c.existing != nil {
		diff.Old = &DiffCheckValues{
			Check: c.existing,
		}
	}
	return diff
}

func (c *stateCheck) summarize() SummaryCheck {
	sum := c.parserCheck.summarize()
	if sum.Check == nil {
		return sum
	}
	sum.Check.SetID(c.id)
	sum.Check.SetOrgID(c.orgID)
	sum.LabelAssociations = stateToSummaryLabels(c.labelAssociations)
	return sum
}

type stateDashboard struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserDash *dashboard
	existing   *influxdb.Dashboard
}

func (d *stateDashboard) ID() platform.ID {
	if !IsNew(d.stateStatus) && d.existing != nil {
		return d.existing.ID
	}
	return d.id
}

func (d *stateDashboard) labels() []*stateLabel {
	return d.labelAssociations
}

func (d *stateDashboard) resourceType() influxdb.ResourceType {
	return KindDashboard.ResourceType()
}

func (d *stateDashboard) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           d.ID(),
		name:         d.parserDash.Name(),
		metaName:     d.parserDash.MetaName(),
		resourceType: d.resourceType(),
		stateStatus:  d.stateStatus,
	}
}

func (d *stateDashboard) diffDashboard() DiffDashboard {
	diff := DiffDashboard{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindDashboard,
			ID:          SafeID(d.ID()),
			StateStatus: d.stateStatus,
			MetaName:    d.parserDash.MetaName(),
		},
		New: DiffDashboardValues{
			Name:   d.parserDash.Name(),
			Desc:   d.parserDash.Description,
			Charts: make([]DiffChart, 0, len(d.parserDash.Charts)),
		},
	}

	for _, c := range d.parserDash.Charts {
		diff.New.Charts = append(diff.New.Charts, DiffChart{
			Properties: c.properties(),
			Height:     c.Height,
			Width:      c.Width,
		})
	}

	if d.existing == nil {
		return diff
	}

	oldDiff := DiffDashboardValues{
		Name:   d.existing.Name,
		Desc:   d.existing.Description,
		Charts: make([]DiffChart, 0, len(d.existing.Cells)),
	}

	for _, c := range d.existing.Cells {
		var props influxdb.ViewProperties
		if c.View != nil {
			props = c.View.Properties
		}

		oldDiff.Charts = append(oldDiff.Charts, DiffChart{
			Properties: props,
			XPosition:  int(c.X),
			YPosition:  int(c.Y),
			Height:     int(c.H),
			Width:      int(c.W),
		})
	}

	diff.Old = &oldDiff

	return diff
}

func (d *stateDashboard) summarize() SummaryDashboard {
	sum := d.parserDash.summarize()
	sum.ID = SafeID(d.ID())
	sum.OrgID = SafeID(d.orgID)
	sum.LabelAssociations = stateToSummaryLabels(d.labelAssociations)
	return sum
}

type stateLabel struct {
	id, orgID   platform.ID
	stateStatus StateStatus

	parserLabel *label
	existing    *influxdb.Label
}

func (l *stateLabel) diffLabel() DiffLabel {
	diff := DiffLabel{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindLabel,
			ID:          SafeID(l.ID()),
			StateStatus: l.stateStatus,
			MetaName:    l.parserLabel.MetaName(),
		},
		New: DiffLabelValues{
			Name:        l.parserLabel.Name(),
			Description: l.parserLabel.Description,
			Color:       l.parserLabel.Color,
		},
	}
	if e := l.existing; e != nil {
		diff.Old = &DiffLabelValues{
			Name:        e.Name,
			Description: e.Properties["description"],
			Color:       e.Properties["color"],
		}
	}
	return diff
}

func (l *stateLabel) summarize() SummaryLabel {
	sum := l.parserLabel.summarize()
	sum.ID = SafeID(l.ID())
	sum.OrgID = SafeID(l.orgID)
	return sum
}

func (l *stateLabel) ID() platform.ID {
	if !IsNew(l.stateStatus) && l.existing != nil {
		return l.existing.ID
	}
	return l.id
}

func (l *stateLabel) Name() string {
	return l.parserLabel.Name()
}

func (l *stateLabel) MetaName() string {
	return l.parserLabel.MetaName()
}

func (l *stateLabel) shouldApply() bool {
	return IsRemoval(l.stateStatus) ||
		l.existing == nil ||
		l.parserLabel.Description != l.existing.Properties["description"] ||
		l.parserLabel.Name() != l.existing.Name ||
		l.parserLabel.Color != l.existing.Properties["color"]
}

func (l *stateLabel) toInfluxLabel() influxdb.Label {
	return influxdb.Label{
		ID:         l.ID(),
		OrgID:      l.orgID,
		Name:       l.parserLabel.Name(),
		Properties: l.properties(),
	}
}

func (l *stateLabel) properties() map[string]string {
	return map[string]string{
		"color":       l.parserLabel.Color,
		"description": l.parserLabel.Description,
	}
}

type stateLabelMapping struct {
	status StateStatus

	resource interface {
		stateIdentity() stateIdentity
	}

	label *stateLabel
}

func (lm stateLabelMapping) diffLabelMapping() DiffLabelMapping {
	ident := lm.resource.stateIdentity()
	return DiffLabelMapping{
		StateStatus:   lm.status,
		ResType:       ident.resourceType,
		ResID:         SafeID(ident.id),
		ResMetaName:   ident.metaName,
		ResName:       ident.name,
		LabelID:       SafeID(lm.label.ID()),
		LabelMetaName: lm.label.parserLabel.MetaName(),
		LabelName:     lm.label.parserLabel.Name(),
	}
}

func (lm stateLabelMapping) summarize() SummaryLabelMapping {
	ident := lm.resource.stateIdentity()
	return SummaryLabelMapping{
		Status:           lm.status,
		ResourceID:       SafeID(ident.id),
		ResourceMetaName: ident.metaName,
		ResourceName:     ident.name,
		ResourceType:     ident.resourceType,
		LabelMetaName:    lm.label.parserLabel.MetaName(),
		LabelName:        lm.label.parserLabel.Name(),
		LabelID:          SafeID(lm.label.ID()),
	}
}

func stateLabelMappingToInfluxLabelMapping(mapping stateLabelMapping) influxdb.LabelMapping {
	ident := mapping.resource.stateIdentity()
	return influxdb.LabelMapping{
		LabelID:      mapping.label.ID(),
		ResourceID:   ident.id,
		ResourceType: ident.resourceType,
	}
}

type stateLabelMappingForRemoval struct {
	LabelID          platform.ID
	LabelMetaName    string
	ResourceID       platform.ID
	ResourceMetaName string
	ResourceType     influxdb.ResourceType
}

func (m *stateLabelMappingForRemoval) diffLabelMapping() DiffLabelMapping {
	return DiffLabelMapping{
		StateStatus:   StateStatusRemove,
		ResType:       m.ResourceType,
		ResID:         SafeID(m.ResourceID),
		ResMetaName:   m.ResourceMetaName,
		LabelID:       SafeID(m.LabelID),
		LabelMetaName: m.LabelMetaName,
	}
}

type stateEndpoint struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserEndpoint *notificationEndpoint
	existing       influxdb.NotificationEndpoint
}

func (e *stateEndpoint) ID() platform.ID {
	if !IsNew(e.stateStatus) && e.existing != nil {
		return e.existing.GetID()
	}
	return e.id
}

func (e *stateEndpoint) diffEndpoint() DiffNotificationEndpoint {
	diff := DiffNotificationEndpoint{
		DiffIdentifier: DiffIdentifier{
			ID:          SafeID(e.ID()),
			StateStatus: e.stateStatus,
			MetaName:    e.parserEndpoint.MetaName(),
		},
	}
	sum := e.summarize()
	diff.Kind = sum.Kind
	if sum.NotificationEndpoint != nil {
		diff.New.NotificationEndpoint = sum.NotificationEndpoint
	}
	if e.existing != nil {
		diff.Old = &DiffNotificationEndpointValues{
			NotificationEndpoint: e.existing,
		}
	}
	return diff
}

func (e *stateEndpoint) labels() []*stateLabel {
	return e.labelAssociations
}

func (e *stateEndpoint) resourceType() influxdb.ResourceType {
	return KindNotificationEndpoint.ResourceType()
}

func (e *stateEndpoint) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           e.ID(),
		name:         e.parserEndpoint.Name(),
		metaName:     e.parserEndpoint.MetaName(),
		resourceType: e.resourceType(),
		stateStatus:  e.stateStatus,
	}
}

func (e *stateEndpoint) summarize() SummaryNotificationEndpoint {
	sum := e.parserEndpoint.summarize()
	if sum.NotificationEndpoint == nil {
		return sum
	}
	if e.ID() != 0 {
		sum.NotificationEndpoint.SetID(e.ID())
	}
	if e.orgID != 0 {
		sum.NotificationEndpoint.SetOrgID(e.orgID)
	}
	sum.LabelAssociations = stateToSummaryLabels(e.labelAssociations)
	return sum
}

type stateRule struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	associatedEndpoint *stateEndpoint

	parserRule *notificationRule
	existing   influxdb.NotificationRule
}

func (r *stateRule) ID() platform.ID {
	if !IsNew(r.stateStatus) && r.existing != nil {
		return r.existing.GetID()
	}
	return r.id
}

func (r *stateRule) endpointAssociation() StackResourceAssociation {
	if r.associatedEndpoint == nil {
		return StackResourceAssociation{}
	}
	return StackResourceAssociation{
		Kind:     KindNotificationEndpoint,
		MetaName: r.endpointTemplateName(),
	}
}

func (r *stateRule) diffRule() DiffNotificationRule {
	sum := DiffNotificationRule{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindNotificationRule,
			ID:          SafeID(r.ID()),
			StateStatus: r.stateStatus,
			MetaName:    r.parserRule.MetaName(),
		},
		New: DiffNotificationRuleValues{
			Name:            r.parserRule.Name(),
			Description:     r.parserRule.description,
			EndpointName:    r.endpointTemplateName(),
			EndpointID:      SafeID(r.endpointID()),
			EndpointType:    r.endpointType(),
			Every:           r.parserRule.every.String(),
			Offset:          r.parserRule.offset.String(),
			MessageTemplate: r.parserRule.msgTemplate,
			StatusRules:     toSummaryStatusRules(r.parserRule.statusRules),
			TagRules:        toSummaryTagRules(r.parserRule.tagRules),
		},
	}

	if r.existing == nil {
		return sum
	}

	sum.Old = &DiffNotificationRuleValues{
		Name:         r.existing.GetName(),
		Description:  r.existing.GetDescription(),
		EndpointName: r.existing.GetName(),
		EndpointID:   SafeID(r.existing.GetEndpointID()),
		EndpointType: r.existing.Type(),
	}

	assignBase := func(b rule.Base) {
		if b.Every != nil {
			sum.Old.Every = b.Every.TimeDuration().String()
		}
		if b.Offset != nil {
			sum.Old.Offset = b.Offset.TimeDuration().String()
		}
		for _, tr := range b.TagRules {
			sum.Old.TagRules = append(sum.Old.TagRules, SummaryTagRule{
				Key:      tr.Key,
				Value:    tr.Value,
				Operator: tr.Operator.String(),
			})
		}
		for _, sr := range b.StatusRules {
			sRule := SummaryStatusRule{CurrentLevel: sr.CurrentLevel.String()}
			if sr.PreviousLevel != nil {
				sRule.PreviousLevel = sr.PreviousLevel.String()
			}
			sum.Old.StatusRules = append(sum.Old.StatusRules, sRule)
		}
	}

	switch p := r.existing.(type) {
	case *rule.HTTP:
		assignBase(p.Base)
	case *rule.Slack:
		assignBase(p.Base)
		sum.Old.MessageTemplate = p.MessageTemplate
	case *rule.PagerDuty:
		assignBase(p.Base)
		sum.Old.MessageTemplate = p.MessageTemplate
	}

	return sum
}

func (r *stateRule) endpointID() platform.ID {
	if r.associatedEndpoint != nil {
		return r.associatedEndpoint.ID()
	}
	return 0
}

func (r *stateRule) endpointTemplateName() string {
	if r.associatedEndpoint != nil && r.associatedEndpoint.parserEndpoint != nil {
		return r.associatedEndpoint.parserEndpoint.MetaName()
	}
	return ""
}

func (r *stateRule) endpointType() string {
	if r.associatedEndpoint != nil {
		return r.associatedEndpoint.parserEndpoint.kind.String()
	}
	return ""
}

func (r *stateRule) labels() []*stateLabel {
	return r.labelAssociations
}

func (r *stateRule) resourceType() influxdb.ResourceType {
	return KindNotificationRule.ResourceType()
}

func (r *stateRule) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           r.ID(),
		name:         r.parserRule.Name(),
		metaName:     r.parserRule.MetaName(),
		resourceType: r.resourceType(),
		stateStatus:  r.stateStatus,
	}
}

func (r *stateRule) summarize() SummaryNotificationRule {
	sum := r.parserRule.summarize()
	sum.ID = SafeID(r.id)
	sum.EndpointID = SafeID(r.associatedEndpoint.ID())
	sum.EndpointMetaName = r.associatedEndpoint.parserEndpoint.MetaName()
	sum.EndpointType = r.associatedEndpoint.parserEndpoint.kind.String()
	sum.LabelAssociations = stateToSummaryLabels(r.labelAssociations)
	return sum
}

func (r *stateRule) toInfluxRule() influxdb.NotificationRule {
	influxRule := r.parserRule.toInfluxRule()
	if r.ID() > 0 {
		influxRule.SetID(r.ID())
	}
	if r.orgID > 0 {
		influxRule.SetOrgID(r.orgID)
	}
	switch e := influxRule.(type) {
	case *rule.HTTP:
		e.EndpointID = r.associatedEndpoint.ID()
	case *rule.PagerDuty:
		e.EndpointID = r.associatedEndpoint.ID()
	case *rule.Slack:
		e.EndpointID = r.associatedEndpoint.ID()
	}

	return influxRule
}

type stateTask struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserTask *task
	existing   *taskmodel.Task
}

func (t *stateTask) ID() platform.ID {
	if !IsNew(t.stateStatus) && t.existing != nil {
		return t.existing.ID
	}
	return t.id
}

func (t *stateTask) diffTask() DiffTask {
	diff := DiffTask{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindTask,
			ID:          SafeID(t.ID()),
			StateStatus: t.stateStatus,
			MetaName:    t.parserTask.MetaName(),
		},
		New: DiffTaskValues{
			Name:        t.parserTask.Name(),
			Cron:        t.parserTask.cron,
			Description: t.parserTask.description,
			Every:       durToStr(t.parserTask.every),
			Offset:      durToStr(t.parserTask.offset),
			Query:       t.parserTask.query.DashboardQuery(),
			Status:      t.parserTask.Status(),
		},
	}

	if t.existing == nil {
		return diff
	}

	diff.Old = &DiffTaskValues{
		Name:        t.existing.Name,
		Cron:        t.existing.Cron,
		Description: t.existing.Description,
		Every:       t.existing.Every,
		Offset:      t.existing.Offset.String(),
		Query:       t.existing.Flux,
		Status:      influxdb.Status(t.existing.Status),
	}

	return diff
}

func (t *stateTask) labels() []*stateLabel {
	return t.labelAssociations
}

func (t *stateTask) resourceType() influxdb.ResourceType {
	return influxdb.TasksResourceType
}

func (t *stateTask) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           t.ID(),
		name:         t.parserTask.Name(),
		metaName:     t.parserTask.MetaName(),
		resourceType: t.resourceType(),
		stateStatus:  t.stateStatus,
	}
}

func (t *stateTask) summarize() SummaryTask {
	sum := t.parserTask.summarize()
	sum.ID = SafeID(t.id)
	sum.LabelAssociations = stateToSummaryLabels(t.labelAssociations)
	return sum
}

type stateTelegraf struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserTelegraf *telegraf
	existing       *influxdb.TelegrafConfig
}

func (t *stateTelegraf) ID() platform.ID {
	if !IsNew(t.stateStatus) && t.existing != nil {
		return t.existing.ID
	}
	return t.id
}

func (t *stateTelegraf) diffTelegraf() DiffTelegraf {
	return DiffTelegraf{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindTelegraf,
			ID:          SafeID(t.ID()),
			StateStatus: t.stateStatus,
			MetaName:    t.parserTelegraf.MetaName(),
		},
		New: t.parserTelegraf.config,
		Old: t.existing,
	}
}

func (t *stateTelegraf) labels() []*stateLabel {
	return t.labelAssociations
}

func (t *stateTelegraf) resourceType() influxdb.ResourceType {
	return influxdb.TelegrafsResourceType
}

func (t *stateTelegraf) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           t.ID(),
		name:         t.parserTelegraf.Name(),
		metaName:     t.parserTelegraf.MetaName(),
		resourceType: t.resourceType(),
		stateStatus:  t.stateStatus,
	}
}

func (t *stateTelegraf) summarize() SummaryTelegraf {
	sum := t.parserTelegraf.summarize()
	sum.TelegrafConfig.ID = t.id
	sum.TelegrafConfig.OrgID = t.orgID
	sum.LabelAssociations = stateToSummaryLabels(t.labelAssociations)
	return sum
}

type stateVariable struct {
	id, orgID         platform.ID
	stateStatus       StateStatus
	labelAssociations []*stateLabel

	parserVar *variable
	existing  *influxdb.Variable
}

func (v *stateVariable) ID() platform.ID {
	if !IsNew(v.stateStatus) && v.existing != nil {
		return v.existing.ID
	}
	return v.id
}

func (v *stateVariable) diffVariable() DiffVariable {
	diff := DiffVariable{
		DiffIdentifier: DiffIdentifier{
			Kind:        KindVariable,
			ID:          SafeID(v.ID()),
			StateStatus: v.stateStatus,
			MetaName:    v.parserVar.MetaName(),
		},
		New: DiffVariableValues{
			Name:        v.parserVar.Name(),
			Description: v.parserVar.Description,
			Args:        v.parserVar.influxVarArgs(),
		},
	}
	if iv := v.existing; iv != nil {
		diff.Old = &DiffVariableValues{
			Name:        iv.Name,
			Description: iv.Description,
			Args:        iv.Arguments,
		}
	}

	return diff
}

func (v *stateVariable) labels() []*stateLabel {
	return v.labelAssociations
}

func (v *stateVariable) resourceType() influxdb.ResourceType {
	return KindVariable.ResourceType()
}

func (v *stateVariable) shouldApply() bool {
	return IsRemoval(v.stateStatus) ||
		v.existing == nil ||
		v.existing.Description != v.parserVar.Description ||
		!reflect.DeepEqual(v.existing.Selected, v.parserVar.Selected()) ||
		v.existing.Arguments == nil ||
		!reflect.DeepEqual(v.existing.Arguments, v.parserVar.influxVarArgs())
}

func (v *stateVariable) stateIdentity() stateIdentity {
	return stateIdentity{
		id:           v.ID(),
		name:         v.parserVar.Name(),
		metaName:     v.parserVar.MetaName(),
		resourceType: v.resourceType(),
		stateStatus:  v.stateStatus,
	}
}

func (v *stateVariable) summarize() SummaryVariable {
	sum := v.parserVar.summarize()
	sum.ID = SafeID(v.ID())
	sum.OrgID = SafeID(v.orgID)
	sum.LabelAssociations = stateToSummaryLabels(v.labelAssociations)
	return sum
}

// IsNew identifies state status as new to the platform.
func IsNew(status StateStatus) bool {
	// defaulting zero value to identify as new
	return status == StateStatusNew || status == ""
}

// IsExisting identifies state status as existing in the platform.
func IsExisting(status StateStatus) bool {
	return status == StateStatusExists
}

// IsRemoval identifies state status as existing resource that will be removed
// from the platform.
func IsRemoval(status StateStatus) bool {
	return status == StateStatusRemove
}

type resourceActions struct {
	skipKinds     map[Kind]bool
	skipResources map[ActionSkipResource]bool
}

func (r resourceActions) skipResource(k Kind, metaName string) bool {
	key := ActionSkipResource{
		Kind:     k,
		MetaName: metaName,
	}
	return r.skipResources[key] || r.skipKinds[k]
}
