package config

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"sort"
	"strings"

	"gopkg.in/yaml.v2"

	"github.com/bazelbuild/continuous-integration/pipegen/proxy"
)

type Config struct {
	Pipelines []Pipeline `yaml:"pipelines"`
}

type Pipeline struct {
	Name          string         `yaml:"name"`
	Slug          string         `yaml:"slug"`
	Description   string         `yaml:"description,omitempty"`
	Configuration string         `yaml:"configuration,omitempty"`
	Public        bool           `yaml:"public"`
	Steps         []Step         `yaml:"steps,omitempty"`
	Teams         []TeamAccess   `yaml:"teams,omitempty"`
	Schedules     []Schedule     `yaml:"schedules,omitempty"`
	BuildSkipping *BuildSkipping `yaml:"build_skipping,omitempty"`
	Repository    *Repository    `yaml:"repository,omitempty"`
	Provider      *Provider      `yaml:"provider,omitempty"`
}

type Step struct {
	Name                 string            `yaml:"name"`
	Type                 string            `yaml:"type"`
	Command              string            `yaml:"command,omitempty"`
	Agents               []string          `yaml:"agents,omitempty"`
	Branches             string            `yaml:"branches,omitempty"`
	ArtifactPaths        string            `yaml:"artifact_paths,omitempty"`
	Timeout              int               `yaml:"timeout,omitempty"`
	Concurrency          int               `yaml:"concurrency,omitempty"`
	Parallelism          int               `yaml:"parallelism,omitempty"`
	EnvironmentVariables map[string]string `yaml:"env,omitempty"`
}

type BuildSkipping struct {
	SkipIntermediateBuilds   bool   `yaml:"skip_queued_branch_builds,omitempty"`
	SkipBranches             string `yaml:"skip_queued_branch_builds_filter,omitempty"`
	CancelIntermediateBuilds bool   `yaml:"cancel_running_branch_builds,omitempty"`
	CancelBranches           string `yaml:"cancel_running_branch_builds_filter,omitempty"`
}

type Repository struct {
	RepositoryUrl  string `yaml:"repository,omitempty"`
	DefaultBranch  string `yaml:"default_branch,omitempty"`
	BranchLimiting string `yaml:"branch_limiting,omitempty"`
}

type TeamAccess struct {
	Slug        string `yaml:"slug"`
	AccessLevel string `yaml:"access_level"`
}

type Schedule struct {
	CronInterval         string   `yaml:"cron_interval,omitempty"`
	Description          string   `yaml:"description,omitempty"`
	BuildMessage         string   `yaml:"build_message,omitempty"`
	Commit               string   `yaml:"commit,omitempty"`
	Branch               string   `yaml:"branch,omitempty"`
	EnvironmentVariables []string `yaml:"env,omitempty"`
	Enabled              bool     `yaml:"enabled"`
}

// TODO(fwe): Get url and name from GraphQL?
type Provider struct {
	WebhookUrl string                 `yaml:"webhook_url"`
	Settings   map[string]interface{} `yaml:"settings"`
}

func ReadConfig(path string) (*Config, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("Failed to open config '%s': %v", path, err)
	}
	defer file.Close()

	content, err := ioutil.ReadAll(file)
	if err != nil {
		return nil, fmt.Errorf("Failed to read config '%s': %v", path, err)
	}

	c := Config{}
	err = yaml.Unmarshal(content, &c)
	if err != nil {
		return nil, fmt.Errorf("Config '%s' is not valid YAML: %v", path, err)
	}
	c.sortPipelines()
	return &c, nil
}

func ReadFromBuildkite(org, apiToken string, debug bool) (*Config, error) {
	p, err := proxy.CreateProxy(org, apiToken, debug)
	if err != nil {
		log.Fatalf("Cannot connect to Buildkite: %s", err)
	}

	allPipelines, err := p.GetPipelines()
	if err != nil {
		return nil, fmt.Errorf("Cannot retrieve pipelines: %s", err)
	}

	c := Config{make([]Pipeline, len(allPipelines))}
	for ip, pipeline := range allPipelines {
		c.Pipelines[ip] = *convertPipeline(pipeline)
	}
	c.sortPipelines()

	return &c, nil
}

func (config *Config) sortPipelines() {
	sort.Slice(config.Pipelines, func(i, j int) bool { return config.Pipelines[i].Slug < config.Pipelines[j].Slug })
}

func convertPipeline(pipeline *proxy.Pipeline) *Pipeline {
	steps := make([]Step, len(pipeline.Steps))
	for is, step := range pipeline.Steps {
		steps[is] = Step{Name: getStringValue(step.Name),
			Type:                 getStringValue(step.Type),
			Command:              getStringValue(step.Command),
			Agents:               step.AgentQueryRules,
			Branches:             getStringValue(step.BranchConfiguration),
			ArtifactPaths:        getStringValue(step.ArtifactPaths),
			Timeout:              getIntValue(step.TimeoutInMinutes),
			Concurrency:          getIntValue(step.Concurrency),
			Parallelism:          getIntValue(step.Parallelism),
			EnvironmentVariables: step.Env,
		}
	}

	teams := make([]TeamAccess, len(pipeline.Details.Access))
	for i, t := range pipeline.Details.Access {
		teams[i] = TeamAccess{
			Slug:        t.TeamSlug,
			AccessLevel: t.AccessLevel}
	}

	schedules := make([]Schedule, len(pipeline.Details.Schedules))
	for i, s := range pipeline.Details.Schedules {
		schedules[i] = Schedule{
			Description:          s.Label,
			CronInterval:         s.Cronline,
			BuildMessage:         s.Message,
			Commit:               s.Commit,
			Branch:               s.Branch,
			EnvironmentVariables: s.Env,
			Enabled:              s.Enabled}
	}

	repo := Repository{
		RepositoryUrl:  getStringValue(pipeline.Repository),
		DefaultBranch:  getStringValue(pipeline.DefaultBranch),
		BranchLimiting: getStringValue(pipeline.BranchConfiguration)}
	provider := Provider{
		WebhookUrl: getStringValue(pipeline.Provider.WebhookURL),
		Settings:   pipeline.Provider.Settings}
	skip := BuildSkipping{
		SkipIntermediateBuilds:   pipeline.SkipQueuedBranchBuilds,
		SkipBranches:             getStringValue(pipeline.SkipBueuedBranchBuildsFilter),
		CancelIntermediateBuilds: pipeline.CancelRunningBranchBuilds,
		CancelBranches:           getStringValue(pipeline.CancelRunningBranchBuildsFilter)}

	return &Pipeline{Name: getStringValue(pipeline.Name),
		Slug:          getStringValue(pipeline.Slug),
		Description:   getStringValue(pipeline.Description),
		Public:        pipeline.Details.Public,
		Steps:         steps,
		Configuration: getStringValue(pipeline.Configuration),
		Teams:         teams,
		Schedules:     schedules,
		BuildSkipping: &skip,
		Repository:    &repo,
		Provider:      &provider}
}

func getStringValue(ptr *string) string {
	if ptr == nil {
		return ""
	}
	return *ptr
}

func getIntValue(ptr *int) int {
	if ptr == nil {
		return 0
	}
	return *ptr
}

func (config *Config) Yaml(pipelines ...string) (string, error) {
	toExport, err := config.filterPipelines(pipelines)
	if err != nil {
		return "", fmt.Errorf("Cannot filter pipelines: %v", err)
	}

	s, err := yaml.Marshal(toExport)
	if err != nil {
		return "", fmt.Errorf("Cannot convert configuration to YAML: %v", err)
	}
	return string(s), nil
}

func (config *Config) filterPipelines(pipelines []string) ([]Pipeline, error) {
	if len(pipelines) == 0 {
		return config.Pipelines, nil
	}

	filter := make(map[string]bool)
	for _, p := range pipelines {
		filter[p] = false
	}

	result := make([]Pipeline, 0, len(pipelines))
	for _, p := range config.Pipelines {
		if _, ok := filter[p.Slug]; ok {
			result = append(result, p)
			delete(filter, p.Slug)
		}
	}

	if len(filter) > 0 {
		unknown := make([]string, 0, len(filter))
		for key, _ := range filter {
			unknown = append(unknown, key)
		}
		format := func(p []string) string {
			return strings.Join(p, "\n\t- ")
		}

		return nil, fmt.Errorf("Unknown pipelines:\n\t- %s.\nAvailable pipelines:\n\t- %s", format(unknown), format(config.getSlugs()))
	}

	return result, nil
}

func (config *Config) getSlugs() []string {
	return getSlugs(config.Pipelines)
}

func getSlugs(pipelines []Pipeline) []string {
	slugs := make([]string, len(pipelines))
	for i, p := range pipelines {
		slugs[i] = p.Slug
	}
	return slugs
}

func (config *Config) String() string {
	return fmt.Sprintf("Found %d pipelines:\n\t- %s", len(config.Pipelines), strings.Join(config.getSlugs(), "\n\t- "))
}

func (config *Config) Compare(other *Config) (string, error) {
	if other == nil {
		return "", fmt.Errorf("Cannot compare to a nil config.")
	} else if config == other {
		return "", nil
	}

	var c, o int
	var added, missing, same []Pipeline
	for c < len(config.Pipelines) && o < len(other.Pipelines) {
		configPipe := config.Pipelines[c]
		otherPipe := other.Pipelines[o]

		log.Print(configPipe)

		if configPipe.Slug < otherPipe.Slug {
			added = append(added, configPipe)
			c += 1
		} else if configPipe.Slug == otherPipe.Slug {
			same = append(same, configPipe)
			c += 1
			o += 1
		} else {
			missing = append(missing, otherPipe)
			o += 1
		}
	}

	formatter := func(pipelines []Pipeline) string { return strings.Join(getSlugs(pipelines), ", ") }

	// TODO(fweikert): actually print added, missing and same.
	result := fmt.Sprintf("Additional pipelines: %s\nMissing pipelines: %s\nSame pipelines: %s\n", formatter(added), formatter(missing), formatter(same))
	return result, nil
}
