package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"regexp"
	"strings"

	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/ghodss/yaml"
	"github.com/pkg/errors"
	"github.com/urfave/cli/v2"
)

var (
	yamlSeparator = regexp.MustCompile(`(?m:^---[\t ]*)`)
)

type specMetadata struct {
	Kind    string          `json:"kind"`
	Version string          `json:"apiVersion"`
	Spec    json.RawMessage `json:"spec"`
}

type specGroup struct {
	Queries      []*fleet.QuerySpec
	Packs        []*fleet.PackSpec
	Labels       []*fleet.LabelSpec
	AppConfig    *fleet.AppConfigPayload
	EnrollSecret *fleet.EnrollSecretSpec
}

func specGroupFromBytes(b []byte) (*specGroup, error) {
	specs := &specGroup{
		Queries: []*fleet.QuerySpec{},
		Packs:   []*fleet.PackSpec{},
		Labels:  []*fleet.LabelSpec{},
	}

	for _, spec := range splitYaml(string(b)) {
		var s specMetadata
		if err := yaml.Unmarshal([]byte(spec), &s); err != nil {
			return nil, err
		}

		if s.Spec == nil {
			return nil, errors.Errorf("no spec field on %q document", s.Kind)
		}

		kind := strings.ToLower(s.Kind)

		switch kind {
		case fleet.QueryKind:
			var querySpec *fleet.QuerySpec
			if err := yaml.Unmarshal(s.Spec, &querySpec); err != nil {
				return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
			}
			specs.Queries = append(specs.Queries, querySpec)

		case fleet.PackKind:
			var packSpec *fleet.PackSpec
			if err := yaml.Unmarshal(s.Spec, &packSpec); err != nil {
				return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
			}
			specs.Packs = append(specs.Packs, packSpec)

		case fleet.LabelKind:
			var labelSpec *fleet.LabelSpec
			if err := yaml.Unmarshal(s.Spec, &labelSpec); err != nil {
				return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
			}
			specs.Labels = append(specs.Labels, labelSpec)

		case fleet.AppConfigKind:
			if specs.AppConfig != nil {
				return nil, errors.New("config defined twice in the same file")
			}

			var appConfigSpec *fleet.AppConfigPayload
			if err := yaml.Unmarshal(s.Spec, &appConfigSpec); err != nil {
				return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
			}
			specs.AppConfig = appConfigSpec

		case fleet.EnrollSecretKind:
			if specs.AppConfig != nil {
				return nil, errors.New("enroll_secret defined twice in the same file")
			}

			var enrollSecretSpec *fleet.EnrollSecretSpec
			if err := yaml.Unmarshal(s.Spec, &enrollSecretSpec); err != nil {
				return nil, errors.Wrap(err, "unmarshaling "+kind+" spec")
			}
			specs.EnrollSecret = enrollSecretSpec

		default:
			return nil, errors.Errorf("unknown kind %q", s.Kind)
		}
	}

	return specs, nil
}

func applyCommand() *cli.Command {
	var (
		flFilename string
	)
	return &cli.Command{
		Name:      "apply",
		Usage:     "Apply files to declaratively manage osquery configurations",
		UsageText: `fleetctl apply [options]`,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:        "f",
				EnvVars:     []string{"FILENAME"},
				Value:       "",
				Destination: &flFilename,
				Usage:       "A file to apply",
			},
			configFlag(),
			contextFlag(),
			debugFlag(),
		},
		Action: func(c *cli.Context) error {
			if flFilename == "" {
				return errors.New("-f must be specified")
			}

			b, err := ioutil.ReadFile(flFilename)
			if err != nil {
				return err
			}

			fleet, err := clientFromCLI(c)
			if err != nil {
				return err
			}

			specs, err := specGroupFromBytes(b)
			if err != nil {
				return err
			}

			if len(specs.Queries) > 0 {
				if err := fleet.ApplyQueries(specs.Queries); err != nil {
					return errors.Wrap(err, "applying queries")
				}
				fmt.Printf("[+] applied %d queries\n", len(specs.Queries))
			}

			if len(specs.Labels) > 0 {
				if err := fleet.ApplyLabels(specs.Labels); err != nil {
					return errors.Wrap(err, "applying labels")
				}
				fmt.Printf("[+] applied %d labels\n", len(specs.Labels))
			}

			if len(specs.Packs) > 0 {
				if err := fleet.ApplyPacks(specs.Packs); err != nil {
					return errors.Wrap(err, "applying packs")
				}
				fmt.Printf("[+] applied %d packs\n", len(specs.Packs))
			}

			if specs.AppConfig != nil {
				if err := fleet.ApplyAppConfig(specs.AppConfig); err != nil {
					return errors.Wrap(err, "applying fleet config")
				}
				fmt.Printf("[+] applied fleet config\n")

			}

			if specs.EnrollSecret != nil {
				if err := fleet.ApplyEnrollSecretSpec(specs.EnrollSecret); err != nil {
					return errors.Wrap(err, "applying enroll secrets")
				}
				fmt.Printf("[+] applied enroll secrets\n")

			}

			return nil
		},
	}
}
