package pkger

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/influxdata/flux/ast"
	"github.com/influxdata/flux/ast/edit"
	fluxurl "github.com/influxdata/flux/dependencies/url"
	"github.com/influxdata/flux/parser"
	errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
	caperr "github.com/influxdata/influxdb/v2/pkg/errors"
	"github.com/influxdata/influxdb/v2/pkg/fs"
	"github.com/influxdata/influxdb/v2/pkg/jsonnet"
	"github.com/influxdata/influxdb/v2/task/options"
	"gopkg.in/yaml.v3"
)

type (
	// ReaderFn is used for functional inputs to abstract the individual
	// entrypoints for the reader itself.
	ReaderFn func() (r io.Reader, source string, err error)

	// Encoder is an encodes a type.
	Encoder interface {
		Encode(v interface{}) error
	}

	// Encoding describes the encoding for the raw package data. The
	// encoding determines how the raw data is parsed.
	Encoding int
)

// encoding types
const (
	EncodingUnknown Encoding = iota
	EncodingJSON
	EncodingJsonnet
	EncodingSource // EncodingSource draws the encoding type by inferring it from the source.
	EncodingYAML
)

// String provides the string representation of the encoding.
func (e Encoding) String() string {
	switch e {
	case EncodingJSON:
		return "json"
	case EncodingJsonnet:
		return "jsonnet"
	case EncodingSource:
		return "source"
	case EncodingYAML:
		return "yaml"
	default:
		return "unknown"
	}
}

// ErrInvalidEncoding indicates the encoding is invalid type for the parser.
var ErrInvalidEncoding = errors.New("invalid encoding provided")

// Parse parses a pkg defined by the encoding and readerFns. As of writing this
// we can parse both a YAML, JSON, and Jsonnet formats of the Template model.
func Parse(encoding Encoding, readerFn ReaderFn, opts ...ValidateOptFn) (*Template, error) {
	r, source, err := readerFn()
	if err != nil {
		return nil, err
	}

	var pkgFn func(io.Reader, ...ValidateOptFn) (*Template, error)
	switch encoding {
	case EncodingJSON:
		pkgFn = parseJSON
	case EncodingJsonnet:
		pkgFn = parseJsonnet
	case EncodingSource:
		pkgFn = parseSource
	case EncodingYAML:
		pkgFn = parseYAML
	default:
		return nil, ErrInvalidEncoding
	}

	pkg, err := pkgFn(r, opts...)
	if err != nil {
		return nil, err
	}
	pkg.sources = []string{source}

	return pkg, nil
}

// limitReadFileMaxSize is the maximum file size that limitReadFile will read.
const limitReadFileMaxSize int64 = 2 * 1024 * 1024

// limitReadFile operates like ioutil.ReadFile() in that it reads the contents
// of a file into RAM, but will only read regular files up to the specified
// max. limitReadFile reads the file named by filename and returns the
// contents. A successful call returns err == nil, not err == EOF. Because
// limitReadFile reads the whole file, it does not treat an EOF from Read as an
// error to be reported.
func limitReadFile(name string) (buf []byte, rErr error) {
	// use os.Open() to avoid TOCTOU
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}
	defer caperr.Capture(&rErr, f.Close)()

	// Check that properties of file are OK.
	st, err := f.Stat()
	if err != nil {
		return nil, err
	}

	// Disallow reading from special file systems (e.g. /proc, /sys/, /dev).
	if special, err := fs.IsSpecialFSFromFileInfo(st); err != nil {
		return nil, fmt.Errorf("%w: %q", err, name)
	} else if special {
		return nil, fmt.Errorf("file in special file system: %q", name)
	}

	// only support reading regular files
	if st.Mode()&os.ModeType != 0 {
		return nil, fmt.Errorf("not a regular file: %q", name)
	}

	// limit how much we read into RAM
	var size int
	size64 := st.Size()
	if limitReadFileMaxSize > 0 && size64 > limitReadFileMaxSize {
		return nil, fmt.Errorf("file too big: %q", name)
	} else if size64 == 0 {
		return nil, fmt.Errorf("file empty: %q", name)
	}
	size = int(size64)

	// Read file
	data := make([]byte, size)
	b, err := f.Read(data)
	if err != nil {
		return nil, err
	} else if b != size {
		return nil, fmt.Errorf("short read: %q", name)
	}

	return data, nil
}

// fromFile reads a file from disk and provides a reader from it.
func FromFile(filePath string, extraFileChecks bool) ReaderFn {
	return func() (io.Reader, string, error) {
		u, err := url.Parse(filePath)
		if err != nil {
			return nil, filePath, &errors2.Error{
				Code: errors2.EInvalid,
				Msg:  "invalid filepath provided",
				Err:  err,
			}
		}
		if u.Scheme == "" {
			u.Scheme = "file"
		}

		// not using os.Open to avoid having to deal with closing the file in here
		var b []byte
		var rerr error
		if extraFileChecks {
			b, rerr = limitReadFile(u.Path)
		} else {
			b, rerr = os.ReadFile(u.Path)
		}
		if rerr != nil {
			return nil, filePath, rerr
		}

		return bytes.NewBuffer(b), u.String(), nil
	}
}

// FromReader simply passes the reader along. Useful when consuming
// this from an HTTP request body. There are a number of other useful
// places for this functional input.
func FromReader(r io.Reader, sources ...string) ReaderFn {
	return func() (io.Reader, string, error) {
		source := "byte stream"
		if len(sources) > 0 {
			source = formatSources(sources)
		}
		return r, source, nil
	}
}

// FromString parses a pkg from a raw string value. This is very useful
// in tests.
func FromString(s string) ReaderFn {
	return func() (io.Reader, string, error) {
		return strings.NewReader(s), "string", nil
	}
}

// NewDefaultHTTPClient creates a client with the specified flux IP validator.
// This is copied from flux/dependencies/http/http.go
func NewDefaultHTTPClient(urlValidator fluxurl.Validator) *http.Client {
	// Control is called after DNS lookup, but before the network
	// connection is initiated.
	control := func(network, address string, c syscall.RawConn) error {
		host, _, err := net.SplitHostPort(address)
		if err != nil {
			return err
		}

		ip := net.ParseIP(host)
		return urlValidator.ValidateIP(ip)
	}

	dialer := &net.Dialer{
		Timeout: time.Minute,
		Control: control,
		// DualStack is deprecated
	}

	return &http.Client{
		Transport: &http.Transport{
			DialContext: dialer.DialContext,
		},
	}
}

// FromHTTPRequest parses a pkg from the request body of a HTTP request. This is
// very useful when using packages that are hosted..
func FromHTTPRequest(addr string, client *http.Client) ReaderFn {
	return func() (io.Reader, string, error) {
		resp, err := client.Get(normalizeGithubURLToContent(addr))
		if err != nil {
			return nil, addr, err
		}
		defer resp.Body.Close()

		var buf bytes.Buffer
		if _, err := io.Copy(&buf, resp.Body); err != nil {
			return nil, addr, err
		}

		if resp.StatusCode/100 != 2 {
			return nil, addr, fmt.Errorf(
				"bad response: address=%s status_code=%d body=%q",
				addr, resp.StatusCode, strings.TrimSpace(buf.String()),
			)
		}

		return &buf, addr, nil
	}
}

const (
	githubRawContentHost = "raw.githubusercontent.com"
	githubHost           = "github.com"
)

func normalizeGithubURLToContent(addr string) string {
	u, err := url.Parse(addr)
	if err != nil {
		return addr
	}

	if u.Host == githubHost {
		switch path.Ext(u.Path) {
		case ".yaml", ".yml", ".json", ".jsonnet":
		default:
			return u.String()
		}

		parts := strings.Split(u.Path, "/")
		if len(parts) < 4 {
			return u.String()
		}
		u.Host = githubRawContentHost
		u.Path = path.Join(append(parts[:3], parts[4:]...)...)
	}

	return u.String()
}

func parseJSON(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
	return parse(json.NewDecoder(r), opts...)
}

func parseJsonnet(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
	opt := &validateOpt{}
	for _, o := range opts {
		o(opt)
	}
	// For security, we'll default to disabling parsing jsonnet but allow callers to override the behavior via
	// EnableJsonnet(). Enabling jsonnet might be useful for client code where parsing jsonnet could be acceptable.
	if opt.enableJsonnet {
		return parse(jsonnet.NewDecoder(r), opts...)
	}
	return nil, fmt.Errorf("%s: jsonnet", ErrInvalidEncoding)
}

func parseSource(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
	var b []byte
	if byter, ok := r.(interface{ Bytes() []byte }); ok {
		b = byter.Bytes()
	} else {
		bb, err := io.ReadAll(r)
		if err != nil {
			return nil, fmt.Errorf("failed to decode pkg source: %s", err)
		}
		b = bb
	}

	contentType := http.DetectContentType(b[:min(len(b), 512)])
	switch {
	case strings.Contains(contentType, "jsonnet"):
		// highly unlikely to fall in here with supported content type detection as is
		return parseJsonnet(bytes.NewReader(b), opts...)
	case strings.Contains(contentType, "json"):
		return parseJSON(bytes.NewReader(b), opts...)
	case strings.Contains(contentType, "yaml"),
		strings.Contains(contentType, "yml"):
		return parseYAML(bytes.NewReader(b), opts...)
	default:
		return parseYAML(bytes.NewReader(b), opts...)
	}
}

func parseYAML(r io.Reader, opts ...ValidateOptFn) (*Template, error) {
	dec := yaml.NewDecoder(r)

	var pkg Template
	for {
		// forced to use this for loop b/c the yaml dependency does not
		// decode multi documents.
		var k Object
		err := dec.Decode(&k)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		pkg.Objects = append(pkg.Objects, k)
	}

	if err := pkg.Validate(opts...); err != nil {
		return nil, err
	}

	return &pkg, nil
}

type decoder interface {
	Decode(interface{}) error
}

func parse(dec decoder, opts ...ValidateOptFn) (*Template, error) {
	var pkg Template
	if err := dec.Decode(&pkg.Objects); err != nil {
		return nil, err
	}

	if err := pkg.Validate(opts...); err != nil {
		return nil, err
	}

	return &pkg, nil
}

// Object describes the metadata and raw spec for an entity of a package kind.
type Object struct {
	APIVersion string   `json:"apiVersion" yaml:"apiVersion"`
	Kind       Kind     `json:"kind" yaml:"kind"`
	Metadata   Resource `json:"metadata" yaml:"metadata"`
	Spec       Resource `json:"spec" yaml:"spec"`
}

// Name returns the name of the kind.
func (k Object) Name() string {
	return k.Metadata.references(fieldName).String()
}

// ObjectAssociation is an association for an object. The supported types
// at this time are KindLabel.
type ObjectAssociation struct {
	Kind     Kind
	MetaName string
}

// AddAssociations adds an association to the object.
func (k Object) AddAssociations(associations ...ObjectAssociation) {
	if len(associations) == 0 {
		return
	}

	if k.Spec == nil {
		k.Spec = make(Resource)
	}

	existingAss := k.Spec.slcResource(fieldAssociations)
	for _, ass := range associations {
		existingAss = append(existingAss, Resource{
			fieldKind: ass.Kind,
			fieldName: ass.MetaName,
		})
	}
	sort.Slice(existingAss, func(i, j int) bool {
		iPkgName, jPkgName := existingAss[i].Name(), existingAss[j].Name()
		return iPkgName < jPkgName
	})
	if existingAss == nil {
		return
	}

	k.Spec[fieldAssociations] = existingAss
}

// SetMetadataName sets the metadata.name field.
func (k Object) SetMetadataName(name string) {
	if k.Metadata == nil {
		k.Metadata = make(Resource)
	}
	k.Metadata[fieldName] = name
}

// Template is the model for a package. The resources are more generic that one might
// expect at first glance. This was done on purpose. The way json/yaml/toml or
// w/e scripting you want to use, can have very different ways of parsing. The
// different parsers are limited for the parsers that do not come from the std
// lib (looking at you yaml/v2). This allows us to parse it and leave the matching
// to another power, the graphing of the package is handled within itself.
type Template struct {
	Objects []Object `json:"-" yaml:"-"`
	sources []string

	mLabels                map[string]*label
	mBuckets               map[string]*bucket
	mChecks                map[string]*check
	mDashboards            map[string]*dashboard
	mNotificationEndpoints map[string]*notificationEndpoint
	mNotificationRules     map[string]*notificationRule
	mTasks                 map[string]*task
	mTelegrafs             map[string]*telegraf
	mVariables             map[string]*variable

	mEnv     map[string]bool
	mEnvVals map[string]interface{}
	mSecrets map[string]bool

	isParsed bool // indicates the pkg has been parsed and all resources graphed accordingly
}

// Encode is a helper for encoding the pkg correctly.
func (p *Template) Encode(encoding Encoding) ([]byte, error) {
	if p == nil {
		panic("attempted to encode a nil Template")
	}

	var (
		buf bytes.Buffer
		err error
	)
	switch encoding {
	case EncodingJSON, EncodingJsonnet:
		enc := json.NewEncoder(&buf)
		enc.SetIndent("", "\t")
		err = enc.Encode(p.Objects)
	case EncodingYAML:
		enc := yaml.NewEncoder(&buf)
		for _, k := range p.Objects {
			if err = enc.Encode(k); err != nil {
				break
			}
		}
	default:
		return nil, ErrInvalidEncoding
	}
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func (p *Template) Sources() []string {
	// note: we prevent the internal field from being changed by enabling access
	// 		 to the sources via the exported method here.
	return p.sources
}

// Summary returns a package Summary that describes all the resources and
// associations the pkg contains. It is very useful for informing users of
// the changes that will take place when this pkg would be applied.
func (p *Template) Summary() Summary {
	// ensure zero values for arrays aren't returned, but instead
	// we always returning an initialized slice.
	sum := Summary{
		Buckets:               []SummaryBucket{},
		Checks:                []SummaryCheck{},
		Dashboards:            []SummaryDashboard{},
		NotificationEndpoints: []SummaryNotificationEndpoint{},
		NotificationRules:     []SummaryNotificationRule{},
		Labels:                []SummaryLabel{},
		MissingEnvs:           p.missingEnvRefs(),
		MissingSecrets:        p.missingSecrets(),
		Tasks:                 []SummaryTask{},
		TelegrafConfigs:       []SummaryTelegraf{},
		Variables:             []SummaryVariable{},
	}

	for _, b := range p.buckets() {
		sum.Buckets = append(sum.Buckets, b.summarize())
	}

	for _, c := range p.checks() {
		sum.Checks = append(sum.Checks, c.summarize())
	}

	for _, d := range p.dashboards() {
		sum.Dashboards = append(sum.Dashboards, d.summarize())
	}

	for _, l := range p.labels() {
		sum.Labels = append(sum.Labels, l.summarize())
	}

	sum.LabelMappings = p.labelMappings()

	for _, n := range p.notificationEndpoints() {
		sum.NotificationEndpoints = append(sum.NotificationEndpoints, n.summarize())
	}

	for _, r := range p.notificationRules() {
		sum.NotificationRules = append(sum.NotificationRules, r.summarize())
	}

	for _, t := range p.tasks() {
		sum.Tasks = append(sum.Tasks, t.summarize())
	}

	for _, t := range p.telegrafs() {
		sum.TelegrafConfigs = append(sum.TelegrafConfigs, t.summarize())
	}

	for _, v := range p.variables() {
		sum.Variables = append(sum.Variables, v.summarize())
	}

	return sum
}

func (p *Template) applyEnvRefs(envRefs map[string]interface{}) error {
	if len(envRefs) == 0 {
		return nil
	}

	if p.mEnvVals == nil {
		p.mEnvVals = make(map[string]interface{})
	}

	for k, v := range envRefs {
		p.mEnvVals[k] = v
	}

	return p.Validate()
}

func (p *Template) applySecrets(secrets map[string]string) {
	for k := range secrets {
		p.mSecrets[k] = true
	}
}

// Contains identifies if a pkg contains a given object identified
// by its kind and metadata.Name (MetaName) field.
func (p *Template) Contains(k Kind, pkgName string) bool {
	switch k {
	case KindBucket:
		_, ok := p.mBuckets[pkgName]
		return ok
	case KindCheck, KindCheckDeadman, KindCheckThreshold:
		_, ok := p.mChecks[pkgName]
		return ok
	case KindLabel:
		_, ok := p.mLabels[pkgName]
		return ok
	case KindNotificationEndpoint,
		KindNotificationEndpointHTTP,
		KindNotificationEndpointPagerDuty,
		KindNotificationEndpointSlack:
		_, ok := p.mNotificationEndpoints[pkgName]
		return ok
	case KindNotificationRule:
		_, ok := p.mNotificationRules[pkgName]
		return ok
	case KindTask:
		_, ok := p.mTasks[pkgName]
		return ok
	case KindTelegraf:
		_, ok := p.mTelegrafs[pkgName]
		return ok
	case KindVariable:
		_, ok := p.mVariables[pkgName]
		return ok
	}
	return false
}

// Combine combines pkgs together. Is useful when you want to take multiple disparate pkgs
// and compile them into one to take advantage of the parser and service guarantees.
func Combine(pkgs []*Template, validationOpts ...ValidateOptFn) (*Template, error) {
	newPkg := new(Template)
	for _, p := range pkgs {
		if len(p.Objects) == 0 {
			continue
		}
		newPkg.sources = append(newPkg.sources, p.sources...)
		newPkg.Objects = append(newPkg.Objects, p.Objects...)
	}

	return newPkg, newPkg.Validate(validationOpts...)
}

type (
	validateOpt struct {
		minResources  bool
		skipValidate  bool
		enableJsonnet bool
	}

	// ValidateOptFn provides a means to disable desired validation checks.
	ValidateOptFn func(*validateOpt)
)

// Jsonnet parsing is disabled by default. EnableJsonnet turns it back on.
func EnableJsonnet() ValidateOptFn {
	return func(opt *validateOpt) {
		opt.enableJsonnet = true
	}
}

// ValidWithoutResources ignores the validation check for minimum number
// of resources. This is useful for the service Create to ignore this and
// allow the creation of a pkg without resources.
func ValidWithoutResources() ValidateOptFn {
	return func(opt *validateOpt) {
		opt.minResources = false
	}
}

// ValidSkipParseError ignores the validation check from the  of resources. This
// is useful for the service Create to ignore this and allow the creation of a
// pkg without resources.
func ValidSkipParseError() ValidateOptFn {
	return func(opt *validateOpt) {
		opt.skipValidate = true
	}
}

// Validate will graph all resources and validate every thing is in a useful form.
func (p *Template) Validate(opts ...ValidateOptFn) error {
	opt := &validateOpt{minResources: true}
	for _, o := range opts {
		o(opt)
	}

	var setupFns []func() error
	if opt.minResources {
		setupFns = append(setupFns, p.validResources)
	}
	setupFns = append(setupFns, p.graphResources)

	var pErr parseErr
	for _, fn := range setupFns {
		if err := fn(); err != nil {
			if IsParseErr(err) {
				pErr.append(err.(*parseErr).Resources...)
				continue
			}
			return err
		}
	}

	if len(pErr.Resources) > 0 && !opt.skipValidate {
		return &pErr
	}

	p.isParsed = true
	return nil
}

func (p *Template) buckets() []*bucket {
	buckets := make([]*bucket, 0, len(p.mBuckets))
	for _, b := range p.mBuckets {
		buckets = append(buckets, b)
	}

	sort.Slice(buckets, func(i, j int) bool { return buckets[i].MetaName() < buckets[j].MetaName() })

	return buckets
}

func (p *Template) checks() []*check {
	checks := make([]*check, 0, len(p.mChecks))
	for _, c := range p.mChecks {
		checks = append(checks, c)
	}

	sort.Slice(checks, func(i, j int) bool { return checks[i].MetaName() < checks[j].MetaName() })

	return checks
}

func (p *Template) labels() []*label {
	labels := make(sortedLabels, 0, len(p.mLabels))
	for _, l := range p.mLabels {
		labels = append(labels, l)
	}

	sort.Sort(labels)

	return labels
}

func (p *Template) dashboards() []*dashboard {
	dashes := make([]*dashboard, 0, len(p.mDashboards))
	for _, d := range p.mDashboards {
		dashes = append(dashes, d)
	}
	sort.Slice(dashes, func(i, j int) bool { return dashes[i].MetaName() < dashes[j].MetaName() })
	return dashes
}

func (p *Template) notificationEndpoints() []*notificationEndpoint {
	endpoints := make([]*notificationEndpoint, 0, len(p.mNotificationEndpoints))
	for _, e := range p.mNotificationEndpoints {
		endpoints = append(endpoints, e)
	}
	sort.Slice(endpoints, func(i, j int) bool {
		ei, ej := endpoints[i], endpoints[j]
		if ei.kind == ej.kind {
			return ei.MetaName() < ej.MetaName()
		}
		return ei.kind < ej.kind
	})
	return endpoints
}

func (p *Template) notificationRules() []*notificationRule {
	rules := make([]*notificationRule, 0, len(p.mNotificationRules))
	for _, r := range p.mNotificationRules {
		rules = append(rules, r)
	}
	sort.Slice(rules, func(i, j int) bool { return rules[i].MetaName() < rules[j].MetaName() })
	return rules
}

func (p *Template) missingEnvRefs() []string {
	envRefs := make([]string, 0)
	for envRef, matching := range p.mEnv {
		if !matching {
			envRefs = append(envRefs, envRef)
		}
	}
	sort.Strings(envRefs)
	return envRefs
}

func (p *Template) missingSecrets() []string {
	secrets := make([]string, 0, len(p.mSecrets))
	for secret, foundInPlatform := range p.mSecrets {
		if foundInPlatform {
			continue
		}
		secrets = append(secrets, secret)
	}
	return secrets
}

func (p *Template) tasks() []*task {
	tasks := make([]*task, 0, len(p.mTasks))
	for _, t := range p.mTasks {
		tasks = append(tasks, t)
	}

	sort.Slice(tasks, func(i, j int) bool { return tasks[i].MetaName() < tasks[j].MetaName() })

	return tasks
}

func (p *Template) telegrafs() []*telegraf {
	teles := make([]*telegraf, 0, len(p.mTelegrafs))
	for _, t := range p.mTelegrafs {
		t.config.Name = t.Name()
		teles = append(teles, t)
	}

	sort.Slice(teles, func(i, j int) bool { return teles[i].MetaName() < teles[j].MetaName() })

	return teles
}

func (p *Template) variables() []*variable {
	vars := make([]*variable, 0, len(p.mVariables))
	for _, v := range p.mVariables {
		vars = append(vars, v)
	}

	sort.Slice(vars, func(i, j int) bool { return vars[i].MetaName() < vars[j].MetaName() })

	return vars
}

// labelMappings returns the mappings that will be created for
// valid pairs of labels and resources of which all have IDs.
// If a resource does not exist yet, a label mapping will not
// be returned for it.
func (p *Template) labelMappings() []SummaryLabelMapping {
	labels := p.mLabels
	mappings := make([]SummaryLabelMapping, 0, len(labels))
	for _, l := range labels {
		mappings = append(mappings, l.mappingSummary()...)
	}

	// sort by res type ASC, then res name ASC, then label name ASC
	sort.Slice(mappings, func(i, j int) bool {
		n, m := mappings[i], mappings[j]
		if n.ResourceType < m.ResourceType {
			return true
		}
		if n.ResourceType > m.ResourceType {
			return false
		}
		if n.ResourceName < m.ResourceName {
			return true
		}
		if n.ResourceName > m.ResourceName {
			return false
		}
		return n.LabelName < m.LabelName
	})

	return mappings
}

func (p *Template) validResources() error {
	if len(p.Objects) > 0 {
		return nil
	}

	res := resourceErr{
		Kind: KindPackage.String(),
		RootErrs: []validationErr{{
			Field: "resources",
			Msg:   "at least 1 kind must be provided",
		}},
	}
	var err parseErr
	err.append(res)
	return &err
}

func (p *Template) graphResources() error {
	p.mEnv = make(map[string]bool)
	p.mSecrets = make(map[string]bool)

	graphFns := []func() *parseErr{
		// labels are first, this is to validate associations with other resources
		p.graphLabels,
		p.graphVariables,
		p.graphBuckets,
		p.graphChecks,
		p.graphDashboards,
		p.graphNotificationEndpoints,
		p.graphNotificationRules,
		p.graphTasks,
		p.graphTelegrafs,
	}

	var pErr parseErr
	for _, fn := range graphFns {
		if err := fn(); err != nil {
			pErr.append(err.Resources...)
		}
	}

	if len(pErr.Resources) > 0 {
		sort.Slice(pErr.Resources, func(i, j int) bool {
			ir, jr := pErr.Resources[i], pErr.Resources[j]
			return *ir.Idx < *jr.Idx
		})
		return &pErr
	}

	return nil
}

func (p *Template) graphBuckets() *parseErr {
	p.mBuckets = make(map[string]*bucket)
	tracker := p.trackNames(true)
	return p.eachResource(KindBucket, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		bkt := &bucket{
			identity:    ident,
			Description: o.Spec.stringShort(fieldDescription),
			SchemaType:  o.Spec.stringShort(fieldBucketSchemaType),
		}
		if rules, ok := o.Spec[fieldBucketRetentionRules].(retentionRules); ok {
			bkt.RetentionRules = rules
		} else {
			for _, r := range o.Spec.slcResource(fieldBucketRetentionRules) {
				bkt.RetentionRules = append(bkt.RetentionRules, retentionRule{
					Type:    r.stringShort(fieldType),
					Seconds: r.intShort(fieldRetentionRulesEverySeconds),
				})
			}
		}
		if schemas, ok := o.Spec[fieldMeasurementSchemas].(measurementSchemas); ok {
			bkt.MeasurementSchemas = schemas
		} else {
			for _, sr := range o.Spec.slcResource(fieldMeasurementSchemas) {
				ms := measurementSchema{Name: sr.stringShort(fieldMeasurementSchemaName)}
				for _, scr := range sr.slcResource(fieldMeasurementSchemaColumns) {
					ms.Columns = append(ms.Columns, measurementColumn{
						Name:     scr.stringShort(fieldMeasurementColumnName),
						Type:     scr.stringShort(fieldMeasurementColumnType),
						DataType: scr.stringShort(fieldMeasurementColumnDataType),
					})
				}
				bkt.MeasurementSchemas = append(bkt.MeasurementSchemas, ms)
			}
		}
		p.setRefs(bkt.name, bkt.displayName)

		failures := p.parseNestedLabels(o.Spec, func(l *label) error {
			bkt.labels = append(bkt.labels, l)
			p.mLabels[l.MetaName()].setMapping(bkt, false)
			return nil
		})
		sort.Sort(bkt.labels)

		p.mBuckets[bkt.MetaName()] = bkt

		return append(failures, bkt.valid()...)
	})
}

func (p *Template) graphLabels() *parseErr {
	p.mLabels = make(map[string]*label)
	tracker := p.trackNames(true)
	return p.eachResource(KindLabel, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		l := &label{
			identity:    ident,
			Color:       o.Spec.stringShort(fieldLabelColor),
			Description: o.Spec.stringShort(fieldDescription),
		}
		p.mLabels[l.MetaName()] = l
		p.setRefs(l.name, l.displayName)

		return l.valid()
	})
}

func (p *Template) graphChecks() *parseErr {
	p.mChecks = make(map[string]*check)
	// todo: what is the business goal wrt having unique names? (currently duplicates are allowed)
	tracker := p.trackNames(false)

	checkKinds := []struct {
		kind      Kind
		checkKind checkKind
	}{
		{kind: KindCheckThreshold, checkKind: checkKindThreshold},
		{kind: KindCheckDeadman, checkKind: checkKindDeadman},
	}
	var pErr parseErr
	for _, checkKind := range checkKinds {
		err := p.eachResource(checkKind.kind, func(o Object) []validationErr {
			ident, errs := tracker(o)
			if len(errs) > 0 {
				return errs
			}

			ch := &check{
				kind:          checkKind.checkKind,
				identity:      ident,
				description:   o.Spec.stringShort(fieldDescription),
				every:         o.Spec.durationShort(fieldEvery),
				level:         o.Spec.stringShort(fieldLevel),
				offset:        o.Spec.durationShort(fieldOffset),
				query:         strings.TrimSpace(o.Spec.stringShort(fieldQuery)),
				reportZero:    o.Spec.boolShort(fieldCheckReportZero),
				staleTime:     o.Spec.durationShort(fieldCheckStaleTime),
				status:        normStr(o.Spec.stringShort(fieldStatus)),
				statusMessage: o.Spec.stringShort(fieldCheckStatusMessageTemplate),
				timeSince:     o.Spec.durationShort(fieldCheckTimeSince),
			}
			for _, tagRes := range o.Spec.slcResource(fieldCheckTags) {
				ch.tags = append(ch.tags, struct{ k, v string }{
					k: tagRes.stringShort(fieldKey),
					v: tagRes.stringShort(fieldValue),
				})
			}
			for _, th := range o.Spec.slcResource(fieldCheckThresholds) {
				ch.thresholds = append(ch.thresholds, threshold{
					threshType: thresholdType(normStr(th.stringShort(fieldType))),
					allVals:    th.boolShort(fieldCheckAllValues),
					level:      strings.TrimSpace(strings.ToUpper(th.stringShort(fieldLevel))),
					max:        th.float64Short(fieldMax),
					min:        th.float64Short(fieldMin),
					val:        th.float64Short(fieldValue),
				})
			}

			failures := p.parseNestedLabels(o.Spec, func(l *label) error {
				ch.labels = append(ch.labels, l)
				p.mLabels[l.MetaName()].setMapping(ch, false)
				return nil
			})
			sort.Sort(ch.labels)

			p.mChecks[ch.MetaName()] = ch
			p.setRefs(ch.name, ch.displayName)
			return append(failures, ch.valid()...)
		})
		if err != nil {
			pErr.append(err.Resources...)
		}
	}
	if len(pErr.Resources) > 0 {
		return &pErr
	}
	return nil
}

func (p *Template) graphDashboards() *parseErr {
	p.mDashboards = make(map[string]*dashboard)
	tracker := p.trackNames(false)
	return p.eachResource(KindDashboard, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		dash := &dashboard{
			identity:    ident,
			Description: o.Spec.stringShort(fieldDescription),
		}

		failures := p.parseNestedLabels(o.Spec, func(l *label) error {
			dash.labels = append(dash.labels, l)
			p.mLabels[l.MetaName()].setMapping(dash, false)
			return nil
		})
		sort.Sort(dash.labels)

		for i, cr := range o.Spec.slcResource(fieldDashCharts) {
			ch, fails := p.parseChart(dash.MetaName(), i, cr)
			if fails != nil {
				failures = append(failures,
					objectValidationErr(fieldSpec, validationErr{
						Field:  fieldDashCharts,
						Index:  intPtr(i),
						Nested: fails,
					}),
				)
				continue
			}
			dash.Charts = append(dash.Charts, ch)
		}

		p.mDashboards[dash.MetaName()] = dash
		p.setRefs(dash.refs()...)

		return append(failures, dash.valid()...)
	})
}

func (p *Template) graphNotificationEndpoints() *parseErr {
	p.mNotificationEndpoints = make(map[string]*notificationEndpoint)
	tracker := p.trackNames(true)

	notificationKinds := []struct {
		kind             Kind
		notificationKind notificationEndpointKind
	}{
		{
			kind:             KindNotificationEndpointHTTP,
			notificationKind: notificationKindHTTP,
		},
		{
			kind:             KindNotificationEndpointPagerDuty,
			notificationKind: notificationKindPagerDuty,
		},
		{
			kind:             KindNotificationEndpointSlack,
			notificationKind: notificationKindSlack,
		},
	}

	var pErr parseErr
	for _, nk := range notificationKinds {
		err := p.eachResource(nk.kind, func(o Object) []validationErr {
			ident, errs := tracker(o)
			if len(errs) > 0 {
				return errs
			}

			endpoint := &notificationEndpoint{
				kind:        nk.notificationKind,
				identity:    ident,
				description: o.Spec.stringShort(fieldDescription),
				method:      strings.TrimSpace(strings.ToUpper(o.Spec.stringShort(fieldNotificationEndpointHTTPMethod))),
				httpType:    normStr(o.Spec.stringShort(fieldType)),
				password:    o.Spec.references(fieldNotificationEndpointPassword),
				routingKey:  o.Spec.references(fieldNotificationEndpointRoutingKey),
				status:      normStr(o.Spec.stringShort(fieldStatus)),
				token:       o.Spec.references(fieldNotificationEndpointToken),
				url:         o.Spec.stringShort(fieldNotificationEndpointURL),
				username:    o.Spec.references(fieldNotificationEndpointUsername),
			}
			failures := p.parseNestedLabels(o.Spec, func(l *label) error {
				endpoint.labels = append(endpoint.labels, l)
				p.mLabels[l.MetaName()].setMapping(endpoint, false)
				return nil
			})
			sort.Sort(endpoint.labels)

			p.setRefs(
				endpoint.name,
				endpoint.displayName,
				endpoint.password,
				endpoint.routingKey,
				endpoint.token,
				endpoint.username,
			)

			p.mNotificationEndpoints[endpoint.MetaName()] = endpoint
			return append(failures, endpoint.valid()...)
		})
		if err != nil {
			pErr.append(err.Resources...)
		}
	}
	if len(pErr.Resources) > 0 {
		return &pErr
	}
	return nil
}

func (p *Template) graphNotificationRules() *parseErr {
	p.mNotificationRules = make(map[string]*notificationRule)
	tracker := p.trackNames(false)
	return p.eachResource(KindNotificationRule, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		rule := &notificationRule{
			identity:     ident,
			endpointName: p.getRefWithKnownEnvs(o.Spec, fieldNotificationRuleEndpointName),
			description:  o.Spec.stringShort(fieldDescription),
			channel:      o.Spec.stringShort(fieldNotificationRuleChannel),
			every:        o.Spec.durationShort(fieldEvery),
			msgTemplate:  o.Spec.stringShort(fieldNotificationRuleMessageTemplate),
			offset:       o.Spec.durationShort(fieldOffset),
			status:       normStr(o.Spec.stringShort(fieldStatus)),
		}

		for _, sRule := range o.Spec.slcResource(fieldNotificationRuleStatusRules) {
			rule.statusRules = append(rule.statusRules, struct{ curLvl, prevLvl string }{
				curLvl:  strings.TrimSpace(strings.ToUpper(sRule.stringShort(fieldNotificationRuleCurrentLevel))),
				prevLvl: strings.TrimSpace(strings.ToUpper(sRule.stringShort(fieldNotificationRulePreviousLevel))),
			})
		}

		for _, tRule := range o.Spec.slcResource(fieldNotificationRuleTagRules) {
			rule.tagRules = append(rule.tagRules, struct{ k, v, op string }{
				k:  tRule.stringShort(fieldKey),
				v:  tRule.stringShort(fieldValue),
				op: normStr(tRule.stringShort(fieldOperator)),
			})
		}

		rule.associatedEndpoint = p.mNotificationEndpoints[rule.endpointName.String()]

		failures := p.parseNestedLabels(o.Spec, func(l *label) error {
			rule.labels = append(rule.labels, l)
			p.mLabels[l.MetaName()].setMapping(rule, false)
			return nil
		})
		sort.Sort(rule.labels)

		p.mNotificationRules[rule.MetaName()] = rule
		p.setRefs(rule.name, rule.displayName, rule.endpointName)
		return append(failures, rule.valid()...)
	})
}

func (p *Template) graphTasks() *parseErr {
	p.mTasks = make(map[string]*task)
	tracker := p.trackNames(false)
	return p.eachResource(KindTask, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		t := &task{
			identity:    ident,
			cron:        o.Spec.stringShort(fieldTaskCron),
			description: o.Spec.stringShort(fieldDescription),
			every:       o.Spec.durationShort(fieldEvery),
			offset:      o.Spec.durationShort(fieldOffset),
			status:      normStr(o.Spec.stringShort(fieldStatus)),
		}

		prefix := fmt.Sprintf("tasks[%s].spec", t.MetaName())
		params := o.Spec.slcResource(fieldParams)
		task := o.Spec.slcResource("task")

		var (
			err      error
			failures []validationErr
		)

		t.query, err = p.parseQuery(prefix, o.Spec.stringShort(fieldQuery), params, task)
		if err != nil {
			failures = append(failures, validationErr{
				Field: fieldQuery,
				Msg:   err.Error(),
			})
		}

		if o.APIVersion == APIVersion2 {
			for _, ref := range t.query.task {
				switch ref.EnvRef {
				case prefix + ".task.name", prefix + ".params.name":
					t.displayName = ref
				case prefix + ".task.every":
					every, ok := ref.defaultVal.(time.Duration)
					if ok {
						t.every = every
					} else {
						failures = append(failures, validationErr{
							Field: fieldTask,
							Msg:   "field every is not duration",
						})
					}
				case prefix + ".task.offset":
					offset, ok := ref.defaultVal.(time.Duration)
					if ok {
						t.offset = offset
					} else {
						failures = append(failures, validationErr{
							Field: fieldTask,
							Msg:   "field every is not duration",
						})
					}
				}
			}
		}

		failures = append(failures, p.parseNestedLabels(o.Spec, func(l *label) error {
			t.labels = append(t.labels, l)
			p.mLabels[l.MetaName()].setMapping(t, false)
			return nil
		})...)
		sort.Sort(t.labels)

		p.mTasks[t.MetaName()] = t

		p.setRefs(t.refs()...)
		return append(failures, t.valid()...)
	})
}

func (p *Template) graphTelegrafs() *parseErr {
	p.mTelegrafs = make(map[string]*telegraf)
	tracker := p.trackNames(false)
	return p.eachResource(KindTelegraf, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		tele := &telegraf{
			identity: ident,
		}
		tele.config.Config = o.Spec.stringShort(fieldTelegrafConfig)
		tele.config.Description = o.Spec.stringShort(fieldDescription)

		failures := p.parseNestedLabels(o.Spec, func(l *label) error {
			tele.labels = append(tele.labels, l)
			p.mLabels[l.MetaName()].setMapping(tele, false)
			return nil
		})
		sort.Sort(tele.labels)

		p.mTelegrafs[tele.MetaName()] = tele
		p.setRefs(tele.name, tele.displayName)

		return append(failures, tele.valid()...)
	})
}

func (p *Template) graphVariables() *parseErr {
	p.mVariables = make(map[string]*variable)
	tracker := p.trackNames(true)
	return p.eachResource(KindVariable, func(o Object) []validationErr {
		ident, errs := tracker(o)
		if len(errs) > 0 {
			return errs
		}

		newVar := &variable{
			identity:    ident,
			Description: o.Spec.stringShort(fieldDescription),
			Type:        normStr(o.Spec.stringShort(fieldType)),
			Query:       strings.TrimSpace(o.Spec.stringShort(fieldQuery)),
			Language:    normStr(o.Spec.stringShort(fieldLanguage)),
			ConstValues: o.Spec.slcStr(fieldValues),
			MapValues:   o.Spec.mapStrStr(fieldValues),
		}

		if iSelected, ok := o.Spec[fieldVariableSelected].([]interface{}); ok {
			for _, res := range iSelected {
				newVar.selected = append(newVar.selected, ifaceToReference(res))
			}
		}

		failures := p.parseNestedLabels(o.Spec, func(l *label) error {
			newVar.labels = append(newVar.labels, l)
			p.mLabels[l.MetaName()].setMapping(newVar, false)
			return nil
		})
		sort.Sort(newVar.labels)

		p.mVariables[newVar.MetaName()] = newVar
		p.setRefs(newVar.name, newVar.displayName)
		p.setRefs(newVar.selected...)

		return append(failures, newVar.valid()...)
	})
}

func (p *Template) eachResource(resourceKind Kind, fn func(o Object) []validationErr) *parseErr {
	var pErr parseErr
	for i, k := range p.Objects {
		if err := k.Kind.OK(); err != nil {
			pErr.append(resourceErr{
				Kind: k.Kind.String(),
				Idx:  intPtr(i),
				ValidationErrs: []validationErr{
					{
						Field: fieldKind,
						Msg:   err.Error(),
					},
				},
			})
			continue
		}
		if !k.Kind.is(resourceKind) {
			continue
		}

		if k.APIVersion != APIVersion && k.APIVersion != APIVersion2 {
			pErr.append(resourceErr{
				Kind: k.Kind.String(),
				Idx:  intPtr(i),
				ValidationErrs: []validationErr{
					{
						Field: fieldAPIVersion,
						Msg:   fmt.Sprintf("invalid API version provided %q; must be 1 in [%s, %s]", k.APIVersion, APIVersion, APIVersion2),
					},
				},
			})
			continue
		}

		if errs := isDNS1123Label(k.Name()); len(errs) > 0 {
			pErr.append(resourceErr{
				Kind: k.Kind.String(),
				Idx:  intPtr(i),
				ValidationErrs: []validationErr{
					objectValidationErr(fieldMetadata, validationErr{
						Field: fieldName,
						Msg:   fmt.Sprintf("name %q is invalid; %s", k.Name(), strings.Join(errs, "; ")),
					}),
				},
			})
			continue
		}

		if failures := fn(k); failures != nil {
			err := resourceErr{
				Kind: resourceKind.String(),
				Idx:  intPtr(i),
			}
			for _, f := range failures {
				vErr := validationErr{
					Field:  f.Field,
					Msg:    f.Msg,
					Index:  f.Index,
					Nested: f.Nested,
				}
				if vErr.Field == "associations" {
					err.AssociationErrs = append(err.AssociationErrs, vErr)
					continue
				}
				err.ValidationErrs = append(err.ValidationErrs, vErr)
			}
			pErr.append(err)
		}
	}

	if len(pErr.Resources) > 0 {
		return &pErr
	}
	return nil
}

func (p *Template) parseNestedLabels(r Resource, fn func(lb *label) error) []validationErr {
	nestedLabels := make(map[string]*label)

	var failures []validationErr
	for i, nr := range r.slcResource(fieldAssociations) {
		fail := p.parseNestedLabel(nr, func(l *label) error {
			if _, ok := nestedLabels[l.Name()]; ok {
				return fmt.Errorf("duplicate nested label: %q", l.Name())
			}
			nestedLabels[l.Name()] = l

			return fn(l)
		})
		if fail != nil {
			fail.Index = intPtr(i)
			failures = append(failures, *fail)
		}
	}

	return failures
}

func (p *Template) parseNestedLabel(nr Resource, fn func(lb *label) error) *validationErr {
	k, err := nr.kind()
	if err != nil {
		return &validationErr{
			Field: fieldAssociations,
			Nested: []validationErr{
				{
					Field: fieldKind,
					Msg:   err.Error(),
				},
			},
		}
	}
	if !k.is(KindLabel) {
		return nil
	}

	nameRef := p.getRefWithKnownEnvs(nr, fieldName)
	lb, found := p.mLabels[nameRef.String()]
	if !found {
		return &validationErr{
			Field: fieldAssociations,
			Msg:   fmt.Sprintf("label %q does not exist in pkg", nr.Name()),
		}
	}

	if err := fn(lb); err != nil {
		return &validationErr{
			Field: fieldAssociations,
			Msg:   err.Error(),
		}
	}
	return nil
}

func (p *Template) trackNames(resourceUniqueByName bool) func(Object) (identity, []validationErr) {
	mPkgNames := make(map[string]bool)
	uniqNames := make(map[string]bool)
	return func(o Object) (identity, []validationErr) {
		nameRef := p.getRefWithKnownEnvs(o.Metadata, fieldName)
		if mPkgNames[nameRef.String()] {
			return identity{}, []validationErr{
				objectValidationErr(fieldMetadata, validationErr{
					Field: fieldName,
					Msg:   "duplicate name: " + nameRef.String(),
				}),
			}
		}
		mPkgNames[nameRef.String()] = true

		displayNameRef := p.getRefWithKnownEnvs(o.Spec, fieldName)
		identity := identity{
			name:        nameRef,
			displayName: displayNameRef,
		}
		if !resourceUniqueByName {
			return identity, nil
		}

		name := identity.Name()
		if uniqNames[name] {
			return identity, []validationErr{
				objectValidationErr(fieldSpec, validationErr{
					Field: fieldName,
					Msg:   "duplicate name: " + nameRef.String(),
				}),
			}
		}
		uniqNames[name] = true

		return identity, nil
	}
}

func (p *Template) getRefWithKnownEnvs(r Resource, field string) *references {
	nameRef := r.references(field)
	if v, ok := p.mEnvVals[nameRef.EnvRef]; ok {
		nameRef.val = v
	}
	return nameRef
}

func (p *Template) setRefs(refs ...*references) {
	for _, ref := range refs {
		if ref.Secret != "" {
			p.mSecrets[ref.Secret] = false
		}
		if ref.EnvRef != "" {
			p.mEnv[ref.EnvRef] = p.mEnvVals[ref.EnvRef] != nil
		}
	}
}

func parseAxis(ra Resource, domain []float64) *axis {
	return &axis{
		Base:   ra.stringShort(fieldAxisBase),
		Label:  ra.stringShort(fieldAxisLabel),
		Name:   ra.Name(),
		Prefix: ra.stringShort(fieldPrefix),
		Scale:  ra.stringShort(fieldAxisScale),
		Suffix: ra.stringShort(fieldSuffix),
		Domain: domain,
	}
}

func parseColor(rc Resource) *color {
	return &color{
		ID:    rc.stringShort("id"),
		Name:  rc.Name(),
		Type:  rc.stringShort(fieldType),
		Hex:   rc.stringShort(fieldColorHex),
		Value: flt64Ptr(rc.float64Short(fieldValue)),
	}
}

func (p *Template) parseChart(dashMetaName string, chartIdx int, r Resource) (*chart, []validationErr) {
	ck, err := r.chartKind()
	if err != nil {
		return nil, []validationErr{{
			Field: fieldKind,
			Msg:   err.Error(),
		}}
	}

	c := chart{
		Kind:                       ck,
		Name:                       r.Name(),
		BinSize:                    r.intShort(fieldChartBinSize),
		BinCount:                   r.intShort(fieldChartBinCount),
		Geom:                       r.stringShort(fieldChartGeom),
		Height:                     r.intShort(fieldChartHeight),
		Note:                       r.stringShort(fieldChartNote),
		NoteOnEmpty:                r.boolShort(fieldChartNoteOnEmpty),
		Position:                   r.stringShort(fieldChartPosition),
		Prefix:                     r.stringShort(fieldPrefix),
		Shade:                      r.boolShort(fieldChartShade),
		HoverDimension:             r.stringShort(fieldChartHoverDimension),
		Suffix:                     r.stringShort(fieldSuffix),
		TickPrefix:                 r.stringShort(fieldChartTickPrefix),
		TickSuffix:                 r.stringShort(fieldChartTickSuffix),
		TimeFormat:                 r.stringShort(fieldChartTimeFormat),
		Width:                      r.intShort(fieldChartWidth),
		XCol:                       r.stringShort(fieldChartXCol),
		GenerateXAxisTicks:         r.slcStr(fieldChartGenerateXAxisTicks),
		XTotalTicks:                r.intShort(fieldChartXTotalTicks),
		XTickStart:                 r.float64Short(fieldChartXTickStart),
		XTickStep:                  r.float64Short(fieldChartXTickStep),
		YCol:                       r.stringShort(fieldChartYCol),
		GenerateYAxisTicks:         r.slcStr(fieldChartGenerateYAxisTicks),
		YTotalTicks:                r.intShort(fieldChartYTotalTicks),
		YTickStart:                 r.float64Short(fieldChartYTickStart),
		YTickStep:                  r.float64Short(fieldChartYTickStep),
		XPos:                       r.intShort(fieldChartXPos),
		YPos:                       r.intShort(fieldChartYPos),
		FillColumns:                r.slcStr(fieldChartFillColumns),
		YLabelColumnSeparator:      r.stringShort(fieldChartYLabelColumnSeparator),
		YLabelColumns:              r.slcStr(fieldChartYLabelColumns),
		YSeriesColumns:             r.slcStr(fieldChartYSeriesColumns),
		UpperColumn:                r.stringShort(fieldChartUpperColumn),
		MainColumn:                 r.stringShort(fieldChartMainColumn),
		LowerColumn:                r.stringShort(fieldChartLowerColumn),
		LegendColorizeRows:         r.boolShort(fieldChartLegendColorizeRows),
		LegendHide:                 r.boolShort(fieldChartLegendHide),
		LegendOpacity:              r.float64Short(fieldChartLegendOpacity),
		LegendOrientationThreshold: r.intShort(fieldChartLegendOrientationThreshold),
		Zoom:                       r.float64Short(fieldChartGeoZoom),
		Center:                     center{Lat: r.float64Short(fieldChartGeoCenterLat), Lon: r.float64Short(fieldChartGeoCenterLon)},
		MapStyle:                   r.stringShort(fieldChartGeoMapStyle),
		AllowPanAndZoom:            r.boolShort(fieldChartGeoAllowPanAndZoom),
		DetectCoordinateFields:     r.boolShort(fieldChartGeoDetectCoordinateFields),
	}

	if presStaticLeg, ok := r[fieldChartStaticLegend].(StaticLegend); ok {
		c.StaticLegend = presStaticLeg
	} else {
		if staticLeg, ok := ifaceToResource(r[fieldChartStaticLegend]); ok {
			c.StaticLegend.ColorizeRows = staticLeg.boolShort(fieldChartStaticLegendColorizeRows)
			c.StaticLegend.HeightRatio = staticLeg.float64Short(fieldChartStaticLegendHeightRatio)
			c.StaticLegend.Show = staticLeg.boolShort(fieldChartStaticLegendShow)
			c.StaticLegend.Opacity = staticLeg.float64Short(fieldChartStaticLegendOpacity)
			c.StaticLegend.OrientationThreshold = staticLeg.intShort(fieldChartStaticLegendOrientationThreshold)
			c.StaticLegend.ValueAxis = staticLeg.stringShort(fieldChartStaticLegendValueAxis)
			c.StaticLegend.WidthRatio = staticLeg.float64Short(fieldChartStaticLegendWidthRatio)
		}
	}

	if dp, ok := r.int(fieldChartDecimalPlaces); ok {
		c.EnforceDecimals = true
		c.DecimalPlaces = dp
	}

	var failures []validationErr
	if presentQueries, ok := r[fieldChartQueries].(queries); ok {
		c.Queries = presentQueries
	} else {
		q, vErrs := p.parseChartQueries(dashMetaName, chartIdx, r.slcResource(fieldChartQueries))
		if len(vErrs) > 0 {
			failures = append(failures, validationErr{
				Field:  "queries",
				Nested: vErrs,
			})
		}
		c.Queries = q
	}

	if presentColors, ok := r[fieldChartColors].(colors); ok {
		c.Colors = presentColors
	} else {
		for _, rc := range r.slcResource(fieldChartColors) {
			c.Colors = append(c.Colors, parseColor(rc))
		}
	}

	if presAxes, ok := r[fieldChartAxes].(axes); ok {
		c.Axes = presAxes
	} else {
		for _, ra := range r.slcResource(fieldChartAxes) {
			domain := []float64{}

			if _, ok := ra[fieldChartDomain]; ok {
				for _, str := range ra.slcStr(fieldChartDomain) {
					val, err := strconv.ParseFloat(str, 64)
					if err != nil {
						failures = append(failures, validationErr{
							Field: "axes",
							Msg:   err.Error(),
						})
					}
					domain = append(domain, val)
				}
			}

			c.Axes = append(c.Axes, *parseAxis(ra, domain))
		}
	}

	if presentGeoLayers, ok := r[fieldChartGeoLayers].(geoLayers); ok {
		c.GeoLayers = presentGeoLayers
	} else {
		parseGeoAxis := func(r Resource, field string) *axis {
			if axis, ok := r[field].(*axis); ok {
				return axis
			} else {
				if leg, ok := ifaceToResource(r[field]); ok {
					return parseAxis(leg, nil)
				}
			}
			return nil
		}

		for _, rl := range r.slcResource(fieldChartGeoLayers) {
			gl := geoLayer{
				Type:               rl.stringShort(fieldChartGeoLayerType),
				RadiusField:        rl.stringShort(fieldChartGeoLayerRadiusField),
				ColorField:         rl.stringShort(fieldChartGeoLayerColorField),
				IntensityField:     rl.stringShort(fieldChartGeoLayerIntensityField),
				Radius:             int32(rl.intShort(fieldChartGeoLayerRadius)),
				Blur:               int32(rl.intShort(fieldChartGeoLayerBlur)),
				RadiusDimension:    parseGeoAxis(rl, fieldChartGeoLayerRadiusDimension),
				ColorDimension:     parseGeoAxis(rl, fieldChartGeoLayerColorDimension),
				IntensityDimension: parseGeoAxis(rl, fieldChartGeoLayerIntensityDimension),
				InterpolateColors:  rl.boolShort(fieldChartGeoLayerInterpolateColors),
				TrackWidth:         int32(rl.intShort(fieldChartGeoLayerTrackWidth)),
				Speed:              int32(rl.intShort(fieldChartGeoLayerSpeed)),
				RandomColors:       rl.boolShort(fieldChartGeoLayerRandomColors),
				IsClustered:        rl.boolShort(fieldChartGeoLayerIsClustered),
			}
			if presentColors, ok := rl[fieldChartGeoLayerViewColors].(colors); ok {
				gl.ViewColors = presentColors
			} else {
				for _, rc := range rl.slcResource(fieldChartGeoLayerViewColors) {
					gl.ViewColors = append(gl.ViewColors, parseColor(rc))
				}
			}
			c.GeoLayers = append(c.GeoLayers, &gl)
		}
	}

	if tableOptsRes, ok := ifaceToResource(r[fieldChartTableOptions]); ok {
		c.TableOptions = tableOptions{
			VerticalTimeAxis: tableOptsRes.boolShort(fieldChartTableOptionVerticalTimeAxis),
			SortByField:      tableOptsRes.stringShort(fieldChartTableOptionSortBy),
			Wrapping:         tableOptsRes.stringShort(fieldChartTableOptionWrapping),
			FixFirstColumn:   tableOptsRes.boolShort(fieldChartTableOptionFixFirstColumn),
		}
	}

	for _, fieldOptRes := range r.slcResource(fieldChartFieldOptions) {
		c.FieldOptions = append(c.FieldOptions, fieldOption{
			FieldName:   fieldOptRes.stringShort(fieldChartFieldOptionFieldName),
			DisplayName: fieldOptRes.stringShort(fieldChartFieldOptionDisplayName),
			Visible:     fieldOptRes.boolShort(fieldChartFieldOptionVisible),
		})
	}

	if failures = append(failures, c.validProperties()...); len(failures) > 0 {
		return nil, failures
	}

	return &c, nil
}

func (p *Template) parseChartQueries(dashMetaName string, chartIdx int, resources []Resource) (queries, []validationErr) {
	var (
		q     queries
		vErrs []validationErr
	)
	for i, rq := range resources {
		source := rq.stringShort(fieldQuery)
		if source == "" {
			continue
		}
		prefix := fmt.Sprintf("dashboards[%s].spec.charts[%d].queries[%d]", dashMetaName, chartIdx, i)
		qq, err := p.parseQuery(prefix, source, rq.slcResource(fieldParams), nil)
		if err != nil {
			vErrs = append(vErrs, validationErr{
				Field: "query",
				Index: intPtr(i),
				Msg:   err.Error(),
			})
		}
		q = append(q, qq)
	}
	return q, vErrs
}

func (p *Template) parseQuery(prefix, source string, params, task []Resource) (query, error) {
	files := parser.ParseSource(source).Files
	if len(files) != 1 {
		return query{}, influxErr(errors2.EInvalid, "invalid query source")
	}

	q := query{
		Query: strings.TrimSpace(source),
	}

	mParams := make(map[string]*references)
	tParams := make(map[string]*references)

	paramsOpt, paramsErr := edit.GetOption(files[0], "params")
	taskOpt, taskErr := edit.GetOption(files[0], "task")
	if paramsErr != nil && taskErr != nil {
		return q, nil
	}

	if paramsErr == nil {
		obj, ok := paramsOpt.(*ast.ObjectExpression)
		if ok {
			for _, p := range obj.Properties {
				sl, ok := p.Key.(*ast.Identifier)
				if !ok {
					continue
				}

				mParams[sl.Name] = &references{
					EnvRef:     sl.Name,
					defaultVal: valFromExpr(p.Value),
					valType:    p.Value.Type(),
				}
			}
		}
	}

	if taskErr == nil {
		tobj, ok := taskOpt.(*ast.ObjectExpression)
		if ok {
			for _, p := range tobj.Properties {
				sl, ok := p.Key.(*ast.Identifier)
				if !ok {
					continue
				}

				tParams[sl.Name] = &references{
					EnvRef:     sl.Name,
					defaultVal: valFromExpr(p.Value),
					valType:    p.Value.Type(),
				}
			}
		}
	}

	// override defaults here maybe?
	for _, pr := range params {
		field := pr.stringShort(fieldKey)
		if field == "" {
			continue
		}

		if _, ok := mParams[field]; !ok {
			mParams[field] = &references{EnvRef: field}
		}
		if def, ok := pr[fieldDefault]; ok {
			mParams[field].defaultVal = def
		}
		if valtype, ok := pr.string(fieldType); ok {
			mParams[field].valType = valtype
		}
	}

	var err error
	for _, pr := range task {
		field := pr.stringShort(fieldKey)
		if field == "" {
			continue
		}

		if _, ok := tParams[field]; !ok {
			tParams[field] = &references{EnvRef: field}
		}

		if valtype, ok := pr.string(fieldType); ok {
			tParams[field].valType = valtype
		}

		if def, ok := pr[fieldDefault]; ok {
			switch tParams[field].valType {
			case "duration":
				switch defDur := def.(type) {
				case string:
					tParams[field].defaultVal, err = time.ParseDuration(defDur)
					if err != nil {
						return query{}, influxErr(errors2.EInvalid, err.Error())
					}
				case time.Duration:
					tParams[field].defaultVal = defDur
				}
			default:
				tParams[field].defaultVal = def
			}
		}
	}

	for _, ref := range mParams {
		envRef := fmt.Sprintf("%s.params.%s", prefix, ref.EnvRef)
		q.params = append(q.params, &references{
			EnvRef:     envRef,
			defaultVal: ref.defaultVal,
			val:        p.mEnvVals[envRef],
			valType:    ref.valType,
		})
	}

	for _, ref := range tParams {
		envRef := fmt.Sprintf("%s.task.%s", prefix, ref.EnvRef)
		q.task = append(q.task, &references{
			EnvRef:     envRef,
			defaultVal: ref.defaultVal,
			val:        p.mEnvVals[envRef],
			valType:    ref.valType,
		})
	}
	return q, nil
}

func valFromExpr(p ast.Expression) interface{} {
	switch literal := p.(type) {
	case *ast.CallExpression:
		sl, ok := literal.Callee.(*ast.Identifier)
		if ok && sl.Name == "now" {
			return "now()"
		}
		return nil
	case *ast.DateTimeLiteral:
		return ast.DateTimeFromLiteral(literal)
	case *ast.FloatLiteral:
		return ast.FloatFromLiteral(literal)
	case *ast.IntegerLiteral:
		return ast.IntegerFromLiteral(literal)
	case *ast.DurationLiteral:
		dur, _ := ast.DurationFrom(literal, time.Time{})
		return dur
	case *ast.StringLiteral:
		return ast.StringFromLiteral(literal)
	case *ast.UnaryExpression:
		// a signed duration is represented by a UnaryExpression.
		// it is the only unary expression allowed.
		v := valFromExpr(literal.Argument)
		if dur, ok := v.(time.Duration); ok {
			switch literal.Operator {
			case ast.SubtractionOperator:
				return "-" + dur.String()
			}
		}
		return v
	default:
		return nil
	}
}

// dns1123LabelMaxLength is a label's max length in DNS (RFC 1123)
const dns1123LabelMaxLength int = 63

const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
const dns1123LabelErrMsg string = "a DNS-1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character"

var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$")

// isDNS1123Label tests for a string that conforms to the definition of a label in
// DNS (RFC 1123).
func isDNS1123Label(value string) []string {
	var errs []string
	if len(value) > dns1123LabelMaxLength {
		errs = append(errs, fmt.Sprintf("must be no more than %d characters", dns1123LabelMaxLength))
	}
	if !dns1123LabelRegexp.MatchString(value) {
		errs = append(errs, regexError(dns1123LabelErrMsg, dns1123LabelFmt, "my-name", "123-abc"))
	}
	return errs
}

// regexError returns a string explanation of a regex validation failure.
func regexError(msg string, fmt string, examples ...string) string {
	if len(examples) == 0 {
		return msg + " (regex used for validation is '" + fmt + "')"
	}
	msg += " (e.g. "
	for i := range examples {
		if i > 0 {
			msg += " or "
		}
		msg += "'" + examples[i] + "', "
	}
	msg += "regex used for validation is '" + fmt + "')"
	return msg
}

// Resource is a pkger Resource kind. It can be one of any of
// available kinds that are supported.
type Resource map[string]interface{}

// Name returns the name of the resource.
func (r Resource) Name() string {
	return strings.TrimSpace(r.stringShort(fieldName))
}

func (r Resource) kind() (Kind, error) {
	if k, ok := r[fieldKind].(Kind); ok {
		return k, k.OK()
	}

	resKind, ok := r.string(fieldKind)
	if !ok {
		return KindUnknown, errors.New("no kind provided")
	}

	k := Kind(resKind)
	return k, k.OK()
}

func (r Resource) chartKind() (chartKind, error) {
	ck, _ := r.kind()
	chartKind := chartKind(normStr(string(ck)))
	if !chartKind.ok() {
		return chartKindUnknown, errors.New("invalid chart kind provided: " + string(chartKind))
	}
	return chartKind, nil
}

func (r Resource) bool(key string) (bool, bool) {
	b, ok := r[key].(bool)
	return b, ok
}

func (r Resource) boolShort(key string) bool {
	b, _ := r.bool(key)
	return b
}

func (r Resource) duration(key string) (time.Duration, bool) {
	astDur, err := options.ParseSignedDuration(r.stringShort(key))
	if err != nil {
		return time.Duration(0), false
	}

	dur, err := ast.DurationFrom(astDur, time.Time{})
	return dur, err == nil
}

func (r Resource) durationShort(key string) time.Duration {
	dur, _ := r.duration(key)
	return dur
}

func (r Resource) float64(key string) (float64, bool) {
	f, ok := r[key].(float64)
	if ok {
		return f, true
	}

	i, ok := r[key].(int)
	if ok {
		return float64(i), true
	}
	return 0, false
}

func (r Resource) float64Short(key string) float64 {
	f, _ := r.float64(key)
	return f
}

func (r Resource) int(key string) (int, bool) {
	i, ok := r[key].(int)
	if ok {
		return i, true
	}

	f, ok := r[key].(float64)
	if ok {
		return int(f), true
	}
	return 0, false
}

func (r Resource) intShort(key string) int {
	i, _ := r.int(key)
	return i
}

func (r Resource) references(key string) *references {
	v, ok := r[key]
	if !ok {
		return &references{}
	}
	return ifaceToReference(v)
}

func (r Resource) string(key string) (string, bool) {
	return ifaceToStr(r[key])
}

func (r Resource) stringShort(key string) string {
	s, _ := r.string(key)
	return s
}

func (r Resource) slcResource(key string) []Resource {
	v, ok := r[key]
	if !ok {
		return nil
	}

	if resources, ok := v.([]Resource); ok {
		return resources
	}

	iFaceSlc, ok := v.([]interface{})
	if !ok {
		return nil
	}

	var newResources []Resource
	for _, iFace := range iFaceSlc {
		r, ok := ifaceToResource(iFace)
		if !ok {
			continue
		}
		newResources = append(newResources, r)
	}

	return newResources
}

func (r Resource) slcStr(key string) []string {
	v, ok := r[key]
	if !ok {
		return nil
	}

	if strSlc, ok := v.([]string); ok {
		return strSlc
	}

	iFaceSlc, ok := v.([]interface{})
	if !ok {
		return nil
	}

	var out []string
	for _, iface := range iFaceSlc {
		s, ok := ifaceToStr(iface)
		if !ok {
			continue
		}
		out = append(out, s)
	}

	return out
}

func (r Resource) mapStrStr(key string) map[string]string {
	v, ok := r[key]
	if !ok {
		return nil
	}

	if m, ok := v.(map[string]string); ok {
		return m
	}

	res, ok := ifaceToResource(v)
	if !ok {
		return nil
	}

	m := make(map[string]string)
	for k, v := range res {
		s, ok := ifaceToStr(v)
		if !ok {
			continue
		}
		m[k] = s
	}
	return m
}

func ifaceToResource(i interface{}) (Resource, bool) {
	if i == nil {
		return nil, false
	}

	if res, ok := i.(Resource); ok {
		return res, true
	}

	if m, ok := i.(map[string]interface{}); ok {
		return m, true
	}

	m, ok := i.(map[interface{}]interface{})
	if !ok {
		return nil, false
	}

	newRes := make(Resource)
	for k, v := range m {
		s, ok := k.(string)
		if !ok {
			continue
		}
		newRes[s] = v
	}
	return newRes, true
}

func ifaceToReference(i interface{}) *references {
	var ref references
	for _, f := range []string{fieldReferencesSecret, fieldReferencesEnv} {
		resBody, ok := ifaceToResource(i)
		if !ok {
			continue
		}
		if keyRes, ok := ifaceToResource(resBody[f]); ok {
			switch f {
			case fieldReferencesEnv:
				ref.EnvRef = keyRes.stringShort(fieldKey)
				ref.defaultVal = keyRes[fieldDefault]
			case fieldReferencesSecret:
				ref.Secret = keyRes.stringShort(fieldKey)
			}
		}
	}
	if ref.hasValue() {
		return &ref
	}

	return &references{val: i}
}

func ifaceToStr(v interface{}) (string, bool) {
	if v == nil {
		return "", false
	}

	if s, ok := v.(string); ok {
		return s, true
	}

	if i, ok := v.(int); ok {
		return strconv.Itoa(i), true
	}

	if f, ok := v.(float64); ok {
		return strconv.FormatFloat(f, 'f', -1, 64), true
	}

	return "", false
}

// ParseError is the error from parsing the given package. The ParseError
// behavior provides a list of resources that failed and all validations
// that failed for that resource. A resource can multiple errors, and
// a parseErr can have multiple resources which themselves can have
// multiple validation failures.
type ParseError interface {
	ValidationErrs() []ValidationErr
}

// NewParseError creates a new parse error from existing validation errors.
func NewParseError(errs ...ValidationErr) error {
	if len(errs) == 0 {
		return nil
	}
	return &parseErr{rawErrs: errs}
}

type (
	parseErr struct {
		Resources []resourceErr
		rawErrs   []ValidationErr
	}

	// resourceErr describes the error for a particular resource. In
	// which it may have numerous validation and association errors.
	resourceErr struct {
		Kind            string
		Idx             *int
		RootErrs        []validationErr
		AssociationErrs []validationErr
		ValidationErrs  []validationErr
	}

	validationErr struct {
		Field string
		Msg   string
		Index *int

		Nested []validationErr
	}
)

// Error implements the error interface.
func (e *parseErr) Error() string {
	var (
		errMsg   []string
		seenErrs = make(map[string]bool)
	)
	for _, ve := range append(e.ValidationErrs(), e.rawErrs...) {
		msg := ve.Error()
		if seenErrs[msg] {
			continue
		}
		seenErrs[msg] = true
		errMsg = append(errMsg, ve.Error())
	}

	return strings.Join(errMsg, "\n\t")
}

func (e *parseErr) ValidationErrs() []ValidationErr {
	errs := e.rawErrs[:]
	for _, r := range e.Resources {
		rootErr := ValidationErr{
			Kind: r.Kind,
		}
		for _, v := range r.RootErrs {
			errs = append(errs, traverseErrs(rootErr, v)...)
		}

		rootErr.Indexes = []*int{r.Idx}
		rootErr.Fields = []string{"root"}
		for _, v := range append(r.ValidationErrs, r.AssociationErrs...) {
			errs = append(errs, traverseErrs(rootErr, v)...)
		}
	}

	// used to provide a means to == or != in the map lookup
	// to remove duplicate errors
	type key struct {
		kind    string
		fields  string
		indexes string
		reason  string
	}

	m := make(map[key]bool)
	var out []ValidationErr
	for _, verr := range errs {
		k := key{
			kind:   verr.Kind,
			fields: strings.Join(verr.Fields, ":"),
			reason: verr.Reason,
		}
		var indexes []string
		for _, idx := range verr.Indexes {
			if idx == nil {
				continue
			}
			indexes = append(indexes, strconv.Itoa(*idx))
		}
		k.indexes = strings.Join(indexes, ":")
		if m[k] {
			continue
		}
		m[k] = true
		out = append(out, verr)
	}

	return out
}

// ValidationErr represents an error during the parsing of a package.
type ValidationErr struct {
	Kind    string   `json:"kind" yaml:"kind"`
	Fields  []string `json:"fields" yaml:"fields"`
	Indexes []*int   `json:"idxs" yaml:"idxs"`
	Reason  string   `json:"reason" yaml:"reason"`
}

func (v ValidationErr) Error() string {
	fieldPairs := make([]string, 0, len(v.Fields))
	for i, idx := range v.Indexes {
		field := v.Fields[i]
		if idx == nil || *idx == -1 {
			fieldPairs = append(fieldPairs, field)
			continue
		}
		fieldPairs = append(fieldPairs, fmt.Sprintf("%s[%d]", field, *idx))
	}

	return fmt.Sprintf("kind=%s field=%s reason=%q", v.Kind, strings.Join(fieldPairs, "."), v.Reason)
}

func traverseErrs(root ValidationErr, vErr validationErr) []ValidationErr {
	root.Fields = append(root.Fields, vErr.Field)
	root.Indexes = append(root.Indexes, vErr.Index)
	if len(vErr.Nested) == 0 {
		root.Reason = vErr.Msg
		return []ValidationErr{root}
	}

	var errs []ValidationErr
	for _, n := range vErr.Nested {
		errs = append(errs, traverseErrs(root, n)...)
	}
	return errs
}

func (e *parseErr) append(errs ...resourceErr) {
	e.Resources = append(e.Resources, errs...)
}

// IsParseErr inspects a given error to determine if it is
// a parseErr. If a parseErr it is, it will return it along
// with the confirmation boolean. If the error is not a parseErr
// it will return nil values for the parseErr, making it unsafe
// to use.
func IsParseErr(err error) bool {
	if _, ok := err.(*parseErr); ok {
		return true
	}

	iErr, ok := err.(*errors2.Error)
	if !ok {
		return false
	}
	return IsParseErr(iErr.Err)
}

func objectValidationErr(field string, vErrs ...validationErr) validationErr {
	return validationErr{
		Field:  field,
		Nested: vErrs,
	}
}

func normStr(s string) string {
	return strings.TrimSpace(strings.ToLower(s))
}
