package deploy

import (
	"context"
	"errors"
	"sort"
	"strings"

	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/envfile"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	"coopcloud.tech/abra/pkg/recipe"
	"coopcloud.tech/abra/pkg/secret"

	"github.com/distribution/reference"
	composetypes "github.com/docker/cli/cli/compose/types"
	"github.com/docker/docker/api/types/swarm"
	dockerClient "github.com/docker/docker/client"
)

// MergeAbraShEnv merges abra.sh env vars into the app env vars.
func MergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
	abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
	if err != nil {
		return err
	}

	for k, v := range abraShEnv {
		log.Debugf(i18n.G("read v:%s k: %s", v, k))
		env[k] = v
	}

	return nil
}

// GetEntityNameAndVersion parses a full name like `app_example_com_someconf_v1` to extract name and version, ("someconf", "v1")
func GetEntityNameAndVersion(fullName string, stackName string) (string, string, error) {
	name := strings.TrimPrefix(fullName, stackName+"_")
	if lastUnderscore := strings.LastIndex(name, "_"); lastUnderscore != -1 {
		return name[0:lastUnderscore], name[lastUnderscore+1:], nil
	}
	return "", "", errors.New(i18n.G("can't parse version from '%s'", fullName))
}

func GetSecretsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
	filters, err := app.Filters(false, false)
	if err != nil {
		return nil, err
	}

	// List all services in the stack
	// NOTE: we could do cl.SecretList, but we want to know which secrets are actually attached
	services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
		Filters: filters,
	})
	if err != nil {
		return nil, err
	}

	secrets := make(map[string]string)

	for _, service := range services {
		if service.Spec.TaskTemplate.ContainerSpec.Secrets != nil {
			for _, secretRef := range service.Spec.TaskTemplate.ContainerSpec.Secrets {
				secretName := secretRef.SecretName
				if secretName == "" {
					continue
				}
				secretBaseName, secretVersion, err := GetEntityNameAndVersion(secretName, app.StackName())
				if err != nil {
					log.Warn(err)
					continue
				}

				existingSecretVersion, exists := secrets[secretBaseName]
				if !exists {
					// First time seeing this, add to map
					secrets[secretBaseName] = secretVersion
				} else {
					// Just make sure the versions are the same..
					if existingSecretVersion != secretVersion {
						log.Warnf(i18n.G("different versions for secret '%s', '%s' and %s'", secretBaseName, existingSecretVersion, secretVersion))
					}
				}
			}
		}
	}
	return secrets, nil
}

// GetConfigsForStack retrieves all Docker configs attached to services in a given stack.
func GetConfigsForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
	filters, err := app.Filters(false, false)
	if err != nil {
		return nil, err
	}

	// List all services in the stack
	services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
		Filters: filters,
	})
	if err != nil {
		return nil, err
	}

	// Collect unique config names with versions
	configs := make(map[string]string)
	for _, service := range services {
		if service.Spec.TaskTemplate.ContainerSpec != nil {
			for _, configRef := range service.Spec.TaskTemplate.ContainerSpec.Configs {
				configName := configRef.ConfigName
				if configName == "" {
					continue
				}
				configBaseName, configVersion, err := GetEntityNameAndVersion(configName, app.StackName())
				if err != nil {
					log.Warn(err)
					continue
				}

				existingConfigVersion, ok := configs[configBaseName]
				if !ok {
					// First time seeing this, add to map
					configs[configBaseName] = configVersion
				} else {
					// Just make sure the versions are the same..
					if existingConfigVersion != configVersion {
						log.Warnf(i18n.G("different versions for config '%s', '%s' and %s'", configBaseName, existingConfigVersion, configVersion))
					}
				}
			}
		}
	}

	return configs, nil
}

// GetImagesForStack retrieves all Docker images for services in a given stack.
func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]string, error) {
	filters, err := app.Filters(false, false)
	if err != nil {
		return nil, err
	}

	// List all services in the stack
	services, err := cl.ServiceList(context.Background(), swarm.ServiceListOptions{
		Filters: filters,
	})
	if err != nil {
		return nil, err
	}

	// Collect unique image names with versions
	images := make(map[string]string)
	for _, service := range services {
		if service.Spec.TaskTemplate.ContainerSpec != nil {
			imageName := service.Spec.TaskTemplate.ContainerSpec.Image

			imageParsed, err := reference.ParseNormalizedNamed(imageName)
			if err != nil {
				log.Warn(err)
				continue
			}

			imageBaseName := reference.Path(imageParsed)
			namedTag, ok := imageParsed.(reference.NamedTagged)
			if !ok {
				// This is an image without a tag
				images[imageBaseName] = ""
				continue
			}
			imageTag := namedTag.Tag()

			existingImageVersion, ok := images[imageBaseName]
			if !ok {
				// First time seeing this, add to map
				images[imageBaseName] = imageTag
			} else {
				// Just make sure the versions are the same..
				if existingImageVersion != imageTag {
					log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
				}
			}
		}
	}

	return images, nil
}

func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchanged bool) ([]string, error) {
	// Get current secrets from existing deployment
	currentSecrets, err := GetSecretsForStack(cl, app)
	if err != nil {
		return nil, err
	}

	log.Debugf(i18n.G("current secrets: %v", currentSecrets))

	newSecrets, err := secret.PollSecretsStatus(cl, app)
	if err != nil {
		return nil, err
	}

	log.Debugf(i18n.G("new secrets: %v", newSecrets))

	// Sort secrets to ensure reproducible output
	sort.Slice(newSecrets, func(i, j int) bool {
		return newSecrets[i].LocalName < newSecrets[j].LocalName
	})

	var secretInfo []string

	for _, newSecret := range newSecrets {
		if currentVersion, exists := currentSecrets[newSecret.LocalName]; exists {
			if currentVersion == newSecret.Version {
				if showUnchanged {
					secretInfo = append(secretInfo, i18n.G("%s: %s (unchanged)", newSecret.LocalName, newSecret.Version))
				}
			} else {
				secretInfo = append(secretInfo, i18n.G("%s: %s → %s", newSecret.LocalName, currentVersion, newSecret.Version))
			}
		} else {
			secretInfo = append(secretInfo, i18n.G("%s: %s (new)", newSecret.LocalName, newSecret.Version))
		}
	}

	return secretInfo, nil
}

func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
	// Get current configs from existing deployment
	currentConfigs, err := GetConfigsForStack(cl, app)
	if err != nil {
		return nil, err
	}

	log.Debugf(i18n.G("deployed config names: %v", currentConfigs))

	// Get new configs from the compose specification
	newConfigs := compose.Configs

	var configInfo []string
	for configName := range newConfigs {
		log.Debugf(i18n.G("searching abra.sh for version for %s", configName))
		versionKey := strings.ToUpper(configName) + "_VERSION"
		newVersion, exists := abraShEnv[versionKey]
		if !exists {
			log.Warnf(i18n.G("no version found for config %s", configName))
			configInfo = append(configInfo, i18n.G("%s: ? (missing version)", configName))
			continue
		}

		if currentVersion, exists := currentConfigs[configName]; exists {
			if currentVersion == newVersion {
				if showUnchanged {
					configInfo = append(configInfo, i18n.G("%s: %s (unchanged)", configName, newVersion))
				}
			} else {
				configInfo = append(configInfo, i18n.G("%s: %s → %s", configName, currentVersion, newVersion))
			}
		} else {
			configInfo = append(configInfo, i18n.G("%s: %s (new)", configName, newVersion))
		}
	}

	return configInfo, nil
}

func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, showUnchanged bool) ([]string, error) {
	// Get current images from existing deployment
	currentImages, err := GetImagesForStack(cl, app)
	if err != nil {
		return nil, err
	}

	log.Debugf(i18n.G("deployed images: %v", currentImages))

	// Proposed new images from the compose files
	newImages := make(map[string]string)

	for _, service := range compose.Services {
		imageParsed, err := reference.ParseNormalizedNamed(service.Image)
		if err != nil {
			log.Warn(err)
			continue
		}

		imageBaseName := reference.Path(imageParsed)
		namedTag, ok := imageParsed.(reference.NamedTagged)
		if !ok {
			// This is an image without a tag
			newImages[imageBaseName] = ""
			continue
		}
		imageTag := namedTag.Tag()

		existingImageVersion, ok := newImages[imageBaseName]
		if !ok {
			// First time seeing this, add to map
			newImages[imageBaseName] = imageTag
		} else {
			// Just make sure the versions are the same..
			if existingImageVersion != imageTag {
				log.Warnf(i18n.G("different versions for image '%s', '%s' and %s'", imageBaseName, existingImageVersion, imageTag))
			}
		}
	}
	log.Debugf(i18n.G("proposed images: %v", newImages))

	var imageInfo []string
	for newImageName, newImageVersion := range newImages {
		if currentVersion, exists := currentImages[newImageName]; exists {
			if currentVersion == newImageVersion {
				if showUnchanged {
					imageInfo = append(imageInfo, i18n.G("%s: %s (unchanged)", formatter.StripTagMeta(newImageName), newImageVersion))
				}
			} else {
				imageInfo = append(imageInfo, i18n.G("%s: %s → %s", formatter.StripTagMeta(newImageName), currentVersion, newImageVersion))
			}
		} else {
			imageInfo = append(imageInfo, i18n.G("%s: %s (new)", formatter.StripTagMeta(newImageName), newImageVersion))
		}
	}

	return imageInfo, nil
}
