package boilerplate

import (
	"bytes"
	"fmt"
	"html/template"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"code.justin.tv/kdkly/boilerplate-gen/boilerplate/internal/boilertpl"

	"github.com/pkg/errors"
)

type GlobalConfigGo struct {
	LintGolintDisabled         bool
	LintGolintExcludeGenerated bool // ignore '*.pb.go'
	LintErrcheckIgnoreTests    bool
}

type GlobalConfig struct {
	Go GlobalConfigGo

	ProjectName   string // must be the "owner/repo" on git-aws
	FQProjectName string

	PrimaryVariant *VariantConfig

	JenkinsBuildTestJobName  string
	JenkinsSaveArtifact      bool
	JenkinsSaveDirtyArtifact bool   // typically same value as JenkinsSaveArtifact
	JenkinsDeployJobName     string // unset for projects without a deployment job (i.e. library projects)
	DeployArtifactName       string // typically same as ProjectName

	BinaryProduct bool // true for daemon, utility -- anything where binaries will be produced

	CourierEnabled    bool
	CourierDeployPath string
}

type VariantConfigGo struct {
	// XXX: binary mode not implemented yet.
	Binaries   bool
	BinaryHash string
	Version    string
	Repository string
	Tags       []string
}

type VariantConfig struct {
	Go VariantConfigGo

	MantaBaseImage  string
	MantaFilePath   string
	MantaOutputPath string

	BuildTargetName string
	BuildTargetDeps []string
}

type ConfigBundle struct {
	Global   *GlobalConfig
	Variants []*VariantConfig
}

type variantTemplateParams struct {
	Global  *GlobalConfig
	Variant *VariantConfig
}

type singletonTemplateParams struct {
	Global   *GlobalConfig
	Variants []*VariantConfig
}

func detectProjectName() (string, error) {
	return "", fmt.Errorf("not implemented!")
}

func buildVariantConfig(goCfg *ConfigFileGoConfig) (*VariantConfig, error) {

	// e.g. "go1.7.1" or "go1234567"
	goVersionSlug := goCfg.Version

	return &VariantConfig{
		Go: VariantConfigGo{
			Binaries: false, // XXX: not impl
			// BinaryHash  // XXX: not impl
			Version: goCfg.Version,
			// Repository  // XXX: not yet impl
			Tags: []string{"netgo"}, // XXX: not yet impl
		},
		MantaBaseImage:  "ubuntu:precise", // XXX: not impl; make this per-variant configurable?
		MantaFilePath:   filepath.Join(".manta.json.d", fmt.Sprintf("%s.json", goVersionSlug)),
		MantaOutputPath: filepath.Join(".manta-output.d", goVersionSlug),
		BuildTargetName: goVersionSlug,
		BuildTargetDeps: []string{},
	}, nil
}

func ConfigFromFile(cf *ConfigFile) (*ConfigBundle, error) {
	var err error

	projectName := cf.Project.Name
	if projectName == "" {
		projectName, err = detectProjectName()
		if err != nil {
			return nil, errors.Wrap(err, "failed to detect project name")
		}
	}

	jenkinsJobName := strings.Replace(projectName, "/", "-", -1)
	bundle := &ConfigBundle{
		Global: &GlobalConfig{
			ProjectName:             projectName,
			FQProjectName:           fmt.Sprintf("code.justin.tv/%s", projectName),
			JenkinsBuildTestJobName: jenkinsJobName,
			JenkinsDeployJobName:    jenkinsJobName + "-deploy",

			Go: GlobalConfigGo{
				LintGolintDisabled:         !cf.Golang.Linters.Golint.Enabled,
				LintGolintExcludeGenerated: cf.Golang.Linters.Golint.ExcludeGenerated,
				LintErrcheckIgnoreTests:    cf.Golang.Linters.Errcheck.IgnoreTests,
			},
		},
		Variants: []*VariantConfig{},
	}

	// TODO: Might we sometimes want to save artifacts for utilities?

	projectType := cf.Project.Type
	switch projectType {
	case "library", "utility":
		if cf.Courier.DeployPath != "" {
			return nil, fmt.Errorf("cannot enable Courier deployment for a %s project", projectType)
		}
	case "daemon":
		// XXX: Should support other deployment mechanisms.
		if cf.Courier.DeployPath == "" {
			return nil, fmt.Errorf("must specify Courier deployment path for a daemon project")
		}
		bundle.Global.JenkinsSaveArtifact = true
		bundle.Global.JenkinsSaveDirtyArtifact = true
		bundle.Global.CourierEnabled = true
		bundle.Global.CourierDeployPath = cf.Courier.DeployPath
		bundle.Global.DeployArtifactName = bundle.Global.ProjectName
	default:
		return nil, fmt.Errorf("unexpected project type: %v", projectType)
	}

	switch projectType {
	case "utility", "daemon":
		bundle.Global.BinaryProduct = true
	}

	for _, goCfg := range cf.Golang.Configurations {
		variantConfig, err := buildVariantConfig(goCfg)
		if err != nil {
			return nil, errors.Wrap(err, "failed to build variant configuration")
		}
		if goCfg.Primary {
			if bundle.Global.PrimaryVariant != nil {
				return nil, errors.Wrap(err, "cannot mark more than one variant as primary")
			}
			bundle.Global.PrimaryVariant = variantConfig
		}
		bundle.Variants = append(bundle.Variants, variantConfig)
	}

	// TODO: auto-select latest version?
	if bundle.Global.PrimaryVariant == nil {
		return nil, errors.Wrap(err, "must mark exactly one variant as primary")
	}

	// 	Repository: "https://github.com/golang/go",

	return bundle, nil
}

func (cb *ConfigBundle) WriteBoilerplate(repoPath string) error {
	outputs := make(map[string][]byte)

	// Create a Manta file for each variant.
	for _, vc := range cb.Variants {
		tpl := boilertpl.MantaJSON

		buf := new(bytes.Buffer)
		if err := tpl.Execute(buf, &variantTemplateParams{Global: cb.Global, Variant: vc}); err != nil {
			return errors.Wrapf(err, "failed to evaluate template for file: %v", vc.MantaFilePath)
		}

		outputs[filepath.Join(repoPath, vc.MantaFilePath)] = buf.Bytes()
	}

	// Evaluate each of the singleton templates.
	for _, x := range []struct {
		path string
		tpl  *template.Template
	}{
		{path: boilertpl.ScriptCIPath, tpl: boilertpl.ScriptCI},
		{path: boilertpl.ScriptLintPath, tpl: boilertpl.ScriptLint},
		{path: boilertpl.ScriptBuildPath, tpl: boilertpl.ScriptBuild},
		{path: boilertpl.ScriptTestPath, tpl: boilertpl.ScriptTest},
		{path: boilertpl.ScriptGoPath, tpl: boilertpl.ScriptGo},
		{path: boilertpl.ScriptToolsPath, tpl: boilertpl.ScriptTools},
		{path: boilertpl.MantaMakefilePath, tpl: boilertpl.MantaMakefile},
		{path: boilertpl.JenkinsGroovyPath, tpl: boilertpl.JenkinsGroovy},
	} {
		buf := new(bytes.Buffer)
		if err := x.tpl.Execute(buf, &singletonTemplateParams{Global: cb.Global, Variants: cb.Variants}); err != nil {
			return errors.Wrapf(err, "failed to evaluate template for file: %v", x.path)
		}
		outputs[filepath.Join(repoPath, x.path)] = buf.Bytes()
	}

	// Templates have all been evaluated sucessfully; actually write files to disk.
	for outputPath, data := range outputs {
		outputDir := filepath.Dir(outputPath)
		if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
			return errors.Wrapf(err, "failed to create directory: %v", outputDir)
		}

		if err := ioutil.WriteFile(outputPath, data, os.ModePerm); err != nil {
			return errors.Wrapf(err, "failed to write file: %v", outputPath)
		}
	}
	return nil
}
