/*
   Copyright 2023 Docker Compose CLI authors

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package compose

import (
	"context"
	"fmt"
	"strings"

	"github.com/compose-spec/compose-go/v2/types"
	"github.com/docker/compose/v2/pkg/api"
	"github.com/docker/compose/v2/pkg/utils"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/network"

	"golang.org/x/exp/maps"
)

func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
	filtersListNames := filters.NewArgs()
	filtersListIDs := filters.NewArgs()
	for _, containerName := range options.Containers {
		filtersListNames.Add("name", containerName)
		filtersListIDs.Add("id", containerName)
	}
	containers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
		Filters: filtersListNames,
		All:     true,
	})
	if err != nil {
		return nil, err
	}

	containersByIds, err := s.apiClient().ContainerList(ctx, container.ListOptions{
		Filters: filtersListIDs,
		All:     true,
	})
	if err != nil {
		return nil, err
	}
	for _, ctr := range containersByIds {
		if !utils.Contains(containers, ctr) {
			containers = append(containers, ctr)
		}
	}

	if len(containers) == 0 {
		return nil, fmt.Errorf("no container(s) found with the following name(s): %s", strings.Join(options.Containers, ","))
	}

	return s.createProjectFromContainers(containers, options.ProjectName)
}

func (s *composeService) createProjectFromContainers(containers []container.Summary, projectName string) (*types.Project, error) {
	project := &types.Project{}
	services := types.Services{}
	networks := types.Networks{}
	volumes := types.Volumes{}
	secrets := types.Secrets{}

	if projectName != "" {
		project.Name = projectName
	}

	for _, c := range containers {
		// if the container is from a previous Compose application, use the existing service name
		serviceLabel, ok := c.Labels[api.ServiceLabel]
		if !ok {
			serviceLabel = getCanonicalContainerName(c)
		}
		service, ok := services[serviceLabel]
		if !ok {
			service = types.ServiceConfig{
				Name:   serviceLabel,
				Image:  c.Image,
				Labels: c.Labels,
			}
		}
		service.Scale = increment(service.Scale)

		inspect, err := s.apiClient().ContainerInspect(context.Background(), c.ID)
		if err != nil {
			services[serviceLabel] = service
			continue
		}
		s.extractComposeConfiguration(&service, inspect, volumes, secrets, networks)
		service.Labels = cleanDockerPreviousLabels(service.Labels)
		services[serviceLabel] = service
	}

	project.Services = services
	project.Networks = networks
	project.Volumes = volumes
	project.Secrets = secrets
	return project, nil
}

func (s *composeService) extractComposeConfiguration(service *types.ServiceConfig, inspect container.InspectResponse, volumes types.Volumes, secrets types.Secrets, networks types.Networks) {
	service.Environment = types.NewMappingWithEquals(inspect.Config.Env)
	if inspect.Config.Healthcheck != nil {
		healthConfig := inspect.Config.Healthcheck
		service.HealthCheck = s.toComposeHealthCheck(healthConfig)
	}
	if len(inspect.Mounts) > 0 {
		detectedVolumes, volumeConfigs, detectedSecrets, secretsConfigs := s.toComposeVolumes(inspect.Mounts)
		service.Volumes = append(service.Volumes, volumeConfigs...)
		service.Secrets = append(service.Secrets, secretsConfigs...)
		maps.Copy(volumes, detectedVolumes)
		maps.Copy(secrets, detectedSecrets)
	}
	if len(inspect.NetworkSettings.Networks) > 0 {
		detectedNetworks, networkConfigs := s.toComposeNetwork(inspect.NetworkSettings.Networks)
		service.Networks = networkConfigs
		maps.Copy(networks, detectedNetworks)
	}
	if len(inspect.HostConfig.PortBindings) > 0 {
		for key, portBindings := range inspect.HostConfig.PortBindings {
			for _, portBinding := range portBindings {
				service.Ports = append(service.Ports, types.ServicePortConfig{
					Target:    uint32(key.Int()),
					Published: portBinding.HostPort,
					Protocol:  key.Proto(),
					HostIP:    portBinding.HostIP,
				})
			}
		}
	}
}

func (s *composeService) toComposeHealthCheck(healthConfig *container.HealthConfig) *types.HealthCheckConfig {
	var healthCheck types.HealthCheckConfig
	healthCheck.Test = healthConfig.Test
	if healthConfig.Timeout != 0 {
		timeout := types.Duration(healthConfig.Timeout)
		healthCheck.Timeout = &timeout
	}
	if healthConfig.Interval != 0 {
		interval := types.Duration(healthConfig.Interval)
		healthCheck.Interval = &interval
	}
	if healthConfig.StartPeriod != 0 {
		startPeriod := types.Duration(healthConfig.StartPeriod)
		healthCheck.StartPeriod = &startPeriod
	}
	if healthConfig.StartInterval != 0 {
		startInterval := types.Duration(healthConfig.StartInterval)
		healthCheck.StartInterval = &startInterval
	}
	if healthConfig.Retries != 0 {
		retries := uint64(healthConfig.Retries)
		healthCheck.Retries = &retries
	}
	return &healthCheck
}

func (s *composeService) toComposeVolumes(volumes []container.MountPoint) (map[string]types.VolumeConfig,
	[]types.ServiceVolumeConfig, map[string]types.SecretConfig, []types.ServiceSecretConfig,
) {
	volumeConfigs := make(map[string]types.VolumeConfig)
	secretConfigs := make(map[string]types.SecretConfig)
	var serviceVolumeConfigs []types.ServiceVolumeConfig
	var serviceSecretConfigs []types.ServiceSecretConfig

	for _, volume := range volumes {
		serviceVC := types.ServiceVolumeConfig{
			Type:     string(volume.Type),
			Source:   volume.Source,
			Target:   volume.Destination,
			ReadOnly: !volume.RW,
		}
		switch volume.Type {
		case mount.TypeVolume:
			serviceVC.Source = volume.Name
			vol := types.VolumeConfig{}
			if volume.Driver != "local" {
				vol.Driver = volume.Driver
				vol.Name = volume.Name
			}
			volumeConfigs[volume.Name] = vol
			serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
		case mount.TypeBind:
			if strings.HasPrefix(volume.Destination, "/run/secrets") {
				destination := strings.Split(volume.Destination, "/")
				secret := types.SecretConfig{
					Name: destination[len(destination)-1],
					File: strings.TrimPrefix(volume.Source, "/host_mnt"),
				}
				secretConfigs[secret.Name] = secret
				serviceSecretConfigs = append(serviceSecretConfigs, types.ServiceSecretConfig{
					Source: secret.Name,
					Target: volume.Destination,
				})
			} else {
				serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
			}
		}
	}
	return volumeConfigs, serviceVolumeConfigs, secretConfigs, serviceSecretConfigs
}

func (s *composeService) toComposeNetwork(networks map[string]*network.EndpointSettings) (map[string]types.NetworkConfig, map[string]*types.ServiceNetworkConfig) {
	networkConfigs := make(map[string]types.NetworkConfig)
	serviceNetworkConfigs := make(map[string]*types.ServiceNetworkConfig)

	for name, net := range networks {
		inspect, err := s.apiClient().NetworkInspect(context.Background(), name, network.InspectOptions{})
		if err != nil {
			networkConfigs[name] = types.NetworkConfig{}
		} else {
			networkConfigs[name] = types.NetworkConfig{
				Internal: inspect.Internal,
			}
		}
		serviceNetworkConfigs[name] = &types.ServiceNetworkConfig{
			Aliases: net.Aliases,
		}
	}
	return networkConfigs, serviceNetworkConfigs
}

func cleanDockerPreviousLabels(labels types.Labels) types.Labels {
	cleanedLabels := types.Labels{}
	for key, value := range labels {
		if !strings.HasPrefix(key, "com.docker.compose.") && !strings.HasPrefix(key, "desktop.docker.io") {
			cleanedLabels[key] = value
		}
	}
	return cleanedLabels
}
