package recipe

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/upstream/stack"
	loader "coopcloud.tech/abra/pkg/upstream/stack"
	"github.com/distribution/reference"
	composetypes "github.com/docker/cli/cli/compose/types"
)

// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
	composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
	if !ok {
		if err := ensurePathExists(r.ComposePath); err != nil {
			return []string{}, err
		}
		log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath)
		return []string{r.ComposePath}, nil
	}

	if !strings.Contains(composeFileEnvVar, ":") {
		path := fmt.Sprintf("%s/%s", r.Dir, composeFileEnvVar)
		if err := ensurePathExists(path); err != nil {
			return []string{}, err
		}
		log.Debugf("COMPOSE_FILE detected, loading %s", path)
		return []string{path}, nil
	}

	var composeFiles []string

	numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
	envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
	if len(envVars) != numComposeFiles {
		return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
	}

	for _, file := range envVars {
		path := fmt.Sprintf("%s/%s", r.Dir, file)
		if err := ensurePathExists(path); err != nil {
			return composeFiles, err
		}
		composeFiles = append(composeFiles, path)
	}

	log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
	log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name)

	return composeFiles, nil
}

func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
	pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
	composeFiles, err := filepath.Glob(pattern)
	if err != nil {
		return nil, err
	}

	if len(composeFiles) == 0 {
		return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name)
	}

	if env == nil {
		env, err = r.SampleEnv()
		if err != nil {
			return nil, err
		}
	}

	opts := stack.Deploy{Composefiles: composeFiles}
	config, err := loader.LoadComposefile(opts, env)
	if err != nil {
		return nil, err
	}
	return config, nil
}

// GetVersionLabelLocal retrieves the version label on the local recipe config
func (r Recipe) GetVersionLabelLocal() (string, error) {
	var label string
	config, err := r.GetComposeConfig(nil)
	if err != nil {
		return "", err
	}

	for _, service := range config.Services {
		for label, value := range service.Deploy.Labels {
			if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
				return value, nil
			}
		}
	}

	if label == "" {
		return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name)
	}

	return label, nil
}

// UpdateTag updates an image tag in-place on file system local compose files.
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
	fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
	image = formatter.StripTagMeta(image)

	composeFiles, err := filepath.Glob(fullPattern)
	if err != nil {
		return false, err
	}

	log.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))

	for _, composeFile := range composeFiles {
		opts := stack.Deploy{Composefiles: []string{composeFile}}

		sampleEnv, err := r.SampleEnv()
		if err != nil {
			return false, err
		}

		compose, err := loader.LoadComposefile(opts, sampleEnv)
		if err != nil {
			return false, err
		}

		for _, service := range compose.Services {
			if service.Image == "" {
				continue // may be a compose.$optional.yml file
			}

			img, _ := reference.ParseNormalizedNamed(service.Image)
			if err != nil {
				return false, err
			}

			var composeTag string
			switch img.(type) {
			case reference.NamedTagged:
				composeTag = img.(reference.NamedTagged).Tag()
			default:
				log.Debugf("unable to parse %s, skipping", img)
				continue
			}

			composeImage := formatter.StripTagMeta(reference.Path(img))

			log.Debugf("parsed %s from %s", composeTag, service.Image)

			if image == composeImage {
				bytes, err := ioutil.ReadFile(composeFile)
				if err != nil {
					return false, err
				}

				old := fmt.Sprintf("%s:%s", composeImage, composeTag)
				new := fmt.Sprintf("%s:%s", composeImage, tag)
				replacedBytes := strings.Replace(string(bytes), old, new, -1)

				log.Debugf("updating %s to %s in %s", old, new, compose.Filename)

				if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
					return false, err
				}
			}
		}
	}

	return false, nil
}

// UpdateLabel updates a label in-place on file system local compose files.
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
	fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern)
	composeFiles, err := filepath.Glob(fullPattern)
	if err != nil {
		return err
	}

	log.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))

	for _, composeFile := range composeFiles {
		opts := stack.Deploy{Composefiles: []string{composeFile}}

		sampleEnv, err := r.SampleEnv()
		if err != nil {
			return err
		}

		compose, err := loader.LoadComposefile(opts, sampleEnv)
		if err != nil {
			return err
		}

		serviceExists := false
		var service composetypes.ServiceConfig
		for _, s := range compose.Services {
			if s.Name == serviceName {
				service = s
				serviceExists = true
			}
		}

		if !serviceExists {
			continue
		}

		discovered := false
		for oldLabel, value := range service.Deploy.Labels {
			if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
				discovered = true

				bytes, err := ioutil.ReadFile(composeFile)
				if err != nil {
					return err
				}

				old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
				replacedBytes := strings.Replace(string(bytes), old, label, -1)

				if old == label {
					log.Warnf("%s is already set, nothing to do?", label)
					return nil
				}

				log.Debugf("updating %s to %s in %s", old, label, compose.Filename)

				if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
					return err
				}

				log.Infof("synced label %s to service %s", label, serviceName)
			}
		}

		if !discovered {
			log.Warn("no existing label found, automagic insertion not supported yet")
			log.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
		}
	}

	return nil
}
