package boilerplate

import (
	"fmt"
	"io/ioutil"

	"github.com/go-yaml/yaml"
	"github.com/pkg/errors"
)

const (
	// DefaultConfigFileName is the name of the file that `boilerplate-gen` will read in order to discover what
	// configuration the project has specified.  This filename is relative to the root of the project's repository.
	DefaultConfigFileName = ".boilerplate.yaml"
)

// ConfigFile represents the user-specified data loaded from a boilerplate configuration file.
type ConfigFile struct {
	// Version is the semantic version of the configuration file.  If `boilerplate-gen` will generate files that behave
	// differently for the same input, this version number will be incemented.
	Version uint32

	Project struct {
		// Type describes the nature of this project.  The value must be one of {'library', 'daemon', 'utility'}.
		// TODO: Explain how to make this decision.  (Only 'daemon' projects will generate boilerplate related
		//       to deployment; etc.)
		// TODO: What about projects that contain multiple build products (e.g. a library *and* a daemon *and* some
		//       utilities)?
		Type string

		// Name is a value of the form 'owner/repo' corresponding to the repository's name on
		// `git-aws.internal.justin.tv`.  You should not need to specify this value, since `boilerplate-gen` can
		// automatically detect it based on where your the root of your repository in relation to your `GOPATH`.
		Name string `yaml:",omitempty"`
	}

	// Golang must be set for projects that contain code written in Go.
	Golang struct {
		// Configurations is a list of different Go toolchain configurations that the project should be built against;
		// each configuration that you list will produce one build variant.
		Configurations []*ConfigFileGoConfig

		// Linters configures the set of linters that will be run against each build variant.  If you provide no linter
		// configuration, a set of basic linters (`errcheck`, `vendorcheck`, `golint`, and `go vet`) will be run against
		// each variant.  If the linters are not satisfied, your build will fail!
		//
		// A small number of options for relaxing these linters are provided to (i) smooth the transition for legacy
		// projects that would otherwise require maintenance before they could start using `boilerplate-gen`; and (ii)
		// to make things more convenient for developers working on code that is not yet in production.
		//
		// It is not our intention to support this as a long-term path for projects that are in production; you should
		// plan to eliminate the technical debt that necessitates removing these safeguards.
		Linters struct {
			Golint struct {
				// Enabled can be set to false to avoid treating `golint` complaints as linter violations.
				Enabled bool
				// ExcludeGenerated can be set to false if you would prefer not to run `golint` on certain types of
				// generated code that are known not to meet its standards.  Currently, this option will exclude code
				// generated from protobuf message definitions (i.e. files with the suffix '.pb.go').
				ExcludeGenerated bool `yaml:"exclude_generated"`
			}
			Errcheck struct {
				// IgnoreTests can be set to true if you would prefer not to treat an unhandled error in test-only code
				// as a linter violation.
				IgnoreTests bool `yaml:"ignore_tests"`
			}
		}

		// Unsafe contains dangerous options that may be required to build legacy projects to be successfully built.  If
		// you run into a situation where they are necessary, congratulations!  You've uncovered dangerous technical
		// debt.
		//
		// We may decline to support projects that set any of these options.
		//
		// If you set these options, your warranty is void.  You are running with scissors.  You are playing with fire.
		// You are reaching for the third rail.  If the result breaks, you get to keep both pieces.
		Unsafe struct {
			// DataRaceDetector determines if the `-race` flag will be used when running your test suite.  The default
			// is true; set it to false if your project contains data races.
			DataRaceDetector bool `yaml:"data_race_detector"`
		}
	}

	Jenkins struct {
		// JobName is the name of the Jenkins job that builds the project and runs its unit tests.  It is also used, by
		// default, as the stem of the name of other Jenkins jobs related to the project, such as those that deploy the
		// project's artifacts (if any).  You should not need to specify this value, since `boilerplate-gen` will
		// generate an appropriate default value based on `Project.Name`; if you have a legacy job name that you would
		// like to preserve, you may override that default by setting this option.
		JobName string `yaml:",omitempty"`
	}

	Courier struct {
		// Enabled    bool   // default to true for daemons and false otherwise?
		DeployPath string `yaml:"deploy_path,omitempty"` // required if courier is enabled
	}
}

// ConfigFileGoConfig describes a particular version of the Go toolchain and its configuration.  In most cases, you will
// need to specify only Version for each configuration, and to mark one as the Primary configuration.
type ConfigFileGoConfig struct {
	// Version specifies a version of the Go toolchain by commit oid (i.e. SHA digest) or by release tag
	// (e.g. "go1.7.3").  This field is required for each configuration.
	Version string `yaml:",omitempty"`

	// Binary can be set to true or false to force `boilerplate-gen` to generate boilerplate that attempts to download
	// binaries or to build Go from sources.  By default, this value is autodetected; if Version matches one of the
	// release tags that `boilerplate-gen` knows how to fetch binaries for, binaries will be fetched; if Version is a
	// commit oid (i.e. SHA digest) or the name of a tag, the corresponding source will be fetched, built, and used.
	//
	// In effect, this setting is useful only if you would like to force the generated boilerplate to build Go from
	// source even when `boilerplate-gen` knows how to fetch binaries for the Version that you have specified.
	Binary bool

	// Repository is the URI of the repository from which the Go sources will be fetched.  It defaults to the GitHub
	// repository maintained by the upstream Go project; you should only need to specify this value if you would like to
	// use a local mirror or if you would like to use patches that have not yet been merged by the upstream Go project.
	Repository string `yaml:",omitempty"`

	// Primary is used to mark the preferred build configuration.  Deployable artifacts, if any, will be created from
	// the build variant corresponding to this configuration.
	Primary bool `yaml:",omitempty"` // TODO: omit-on-false
}

// LoadConfigFile loads and parses a configuration file and returns a populated *ConfigFile representing the boilerplate
// configuration specified by the user.
func LoadConfigFile(configPath string) (*ConfigFile, error) {
	fileContents, err := ioutil.ReadFile(configPath)
	if err != nil {
		return nil, errors.Wrap(err, fmt.Sprintf("failed to read configuration file: %s", configPath))
	}

	config := &ConfigFile{}
	config.Golang.Linters.Golint.Enabled = true
	config.Golang.Linters.Golint.ExcludeGenerated = true
	config.Golang.Unsafe.DataRaceDetector = true

	if err := yaml.Unmarshal(fileContents, config); err != nil {
		// TODO: Print nicer-looking type errors if appropriate.
		// TODO: This does *not* return an error if there are extra, undefined keys present! That's horrible.
		return nil, errors.Wrap(err, "failed to unmarshal configuration")
	}

	if config.Version != 0 {
		return nil, fmt.Errorf("unexpected boilerplate configuration version: %d", config.Version)
	}

	return config, nil
}
