package bake

import (
	"fmt"
	"os"
	"reflect"
	"strings"

	"github.com/compose-spec/compose-go/loader"
	compose "github.com/compose-spec/compose-go/types"
	"github.com/pkg/errors"
)

func parseCompose(dt []byte) (*compose.Project, error) {
	return loader.Load(compose.ConfigDetails{
		ConfigFiles: []compose.ConfigFile{
			{
				Content: dt,
			},
		},
		Environment: envMap(os.Environ()),
	}, func(options *loader.Options) {
		options.SkipNormalization = true
	})
}

func envMap(env []string) map[string]string {
	result := make(map[string]string, len(env))
	for _, s := range env {
		kv := strings.SplitN(s, "=", 2)
		if len(kv) != 2 {
			continue
		}
		result[kv[0]] = kv[1]
	}
	return result
}

func ParseCompose(dt []byte) (*Config, error) {
	cfg, err := parseCompose(dt)
	if err != nil {
		return nil, err
	}

	var c Config
	var zeroBuildConfig compose.BuildConfig
	if len(cfg.Services) > 0 {
		c.Groups = []*Group{}
		c.Targets = []*Target{}

		g := &Group{Name: "default"}

		for _, s := range cfg.Services {

			if s.Build == nil || reflect.DeepEqual(s.Build, zeroBuildConfig) {
				// if not make sure they're setting an image or it's invalid d-c.yml
				if s.Image == "" {
					return nil, fmt.Errorf("compose file invalid: service %s has neither an image nor a build context specified. At least one must be provided", s.Name)
				}
				continue
			}

			if err = validateTargetName(s.Name); err != nil {
				return nil, errors.Wrapf(err, "invalid service name %q", s.Name)
			}

			var contextPathP *string
			if s.Build.Context != "" {
				contextPath := s.Build.Context
				contextPathP = &contextPath
			}
			var dockerfilePathP *string
			if s.Build.Dockerfile != "" {
				dockerfilePath := s.Build.Dockerfile
				dockerfilePathP = &dockerfilePath
			}

			var secrets []string
			for _, bs := range s.Build.Secrets {
				secret, err := composeToBuildkitSecret(bs, cfg.Secrets[bs.Source])
				if err != nil {
					return nil, err
				}
				secrets = append(secrets, secret)
			}

			g.Targets = append(g.Targets, s.Name)
			t := &Target{
				Name:       s.Name,
				Context:    contextPathP,
				Dockerfile: dockerfilePathP,
				Labels:     s.Build.Labels,
				Args: flatten(s.Build.Args.Resolve(func(val string) (string, bool) {
					if val, ok := s.Environment[val]; ok && val != nil {
						return *val, true
					}
					val, ok := cfg.Environment[val]
					return val, ok
				})),
				CacheFrom:   s.Build.CacheFrom,
				NetworkMode: &s.Build.Network,
				Secrets:     secrets,
			}
			if err = t.composeExtTarget(s.Build.Extensions); err != nil {
				return nil, err
			}
			if s.Build.Target != "" {
				target := s.Build.Target
				t.Target = &target
			}
			if len(t.Tags) == 0 && s.Image != "" {
				t.Tags = []string{s.Image}
			}
			c.Targets = append(c.Targets, t)
		}
		c.Groups = append(c.Groups, g)

	}

	return &c, nil
}

func flatten(in compose.MappingWithEquals) compose.Mapping {
	if len(in) == 0 {
		return nil
	}
	out := compose.Mapping{}
	for k, v := range in {
		if v == nil {
			continue
		}
		out[k] = *v
	}
	return out
}

// composeExtTarget converts Compose build extension x-bake to bake Target
// https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension
func (t *Target) composeExtTarget(exts map[string]interface{}) error {
	if ext, ok := exts["x-bake"]; ok {
		for key, val := range ext.(map[string]interface{}) {
			switch key {
			case "tags":
				if res, k := val.(string); k {
					t.Tags = append(t.Tags, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.Tags = append(t.Tags, res.(string))
					}
				}
			case "cache-from":
				t.CacheFrom = []string{} // Needed to override the main field
				if res, k := val.(string); k {
					t.CacheFrom = append(t.CacheFrom, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.CacheFrom = append(t.CacheFrom, res.(string))
					}
				}
			case "cache-to":
				if res, k := val.(string); k {
					t.CacheTo = append(t.CacheTo, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.CacheTo = append(t.CacheTo, res.(string))
					}
				}
			case "secret":
				if res, k := val.(string); k {
					t.Secrets = append(t.Secrets, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.Secrets = append(t.Secrets, res.(string))
					}
				}
			case "ssh":
				if res, k := val.(string); k {
					t.SSH = append(t.SSH, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.SSH = append(t.SSH, res.(string))
					}
				}
			case "platforms":
				if res, k := val.(string); k {
					t.Platforms = append(t.Platforms, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.Platforms = append(t.Platforms, res.(string))
					}
				}
			case "output":
				if res, k := val.(string); k {
					t.Outputs = append(t.Outputs, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.Outputs = append(t.Outputs, res.(string))
					}
				}
			case "pull":
				if res, ok := val.(bool); ok {
					t.Pull = &res
				}
			case "no-cache":
				if res, ok := val.(bool); ok {
					t.NoCache = &res
				}
			case "no-cache-filter":
				if res, k := val.(string); k {
					t.NoCacheFilter = append(t.NoCacheFilter, res)
				} else {
					for _, res := range val.([]interface{}) {
						t.NoCacheFilter = append(t.NoCacheFilter, res.(string))
					}
				}
			default:
				return fmt.Errorf("compose file invalid: unkwown %s field for x-bake", key)
			}
		}
	}
	return nil
}

// composeToBuildkitSecret converts secret from compose format to buildkit's
// csv format.
func composeToBuildkitSecret(inp compose.ServiceSecretConfig, psecret compose.SecretConfig) (string, error) {
	if psecret.External.External {
		return "", errors.Errorf("unsupported external secret %s", psecret.Name)
	}

	var bkattrs []string
	if inp.Source != "" {
		bkattrs = append(bkattrs, "id="+inp.Source)
	}
	if psecret.File != "" {
		bkattrs = append(bkattrs, "src="+psecret.File)
	}

	return strings.Join(bkattrs, ","), nil
}
