package docker

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/cenkalti/backoff/v4"
	networktypes "github.com/moby/moby/api/types/network"
	swarmtypes "github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/client"
	"github.com/moby/moby/client/pkg/versions"
	"github.com/rs/zerolog/log"
	ptypes "github.com/traefik/paerser/types"
	"github.com/traefik/traefik/v3/pkg/config/dynamic"
	"github.com/traefik/traefik/v3/pkg/job"
	"github.com/traefik/traefik/v3/pkg/observability/logs"
	"github.com/traefik/traefik/v3/pkg/provider"
	"github.com/traefik/traefik/v3/pkg/safe"
)

// SwarmName is the swarm provider name.
const SwarmName = "swarm"

var _ provider.Provider = (*SwarmProvider)(nil)

// SwarmProvider holds configurations of the provider.
type SwarmProvider struct {
	Shared       `yaml:",inline" export:"true"`
	ClientConfig `yaml:",inline" export:"true"`

	RefreshSeconds ptypes.Duration `description:"Polling interval for swarm mode." json:"refreshSeconds,omitempty" toml:"refreshSeconds,omitempty" yaml:"refreshSeconds,omitempty" export:"true"`
}

// SetDefaults sets the default values.
func (p *SwarmProvider) SetDefaults() {
	p.Watch = true
	p.ExposedByDefault = true
	p.Endpoint = "unix:///var/run/docker.sock"
	p.RefreshSeconds = ptypes.Duration(15 * time.Second)
	p.DefaultRule = DefaultTemplateRule
}

// Init the provider.
func (p *SwarmProvider) Init() error {
	defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
	if err != nil {
		return fmt.Errorf("error while parsing default rule: %w", err)
	}

	p.defaultRuleTpl = defaultRuleTpl
	return nil
}

// Provide allows the docker provider to provide configurations to traefik using the given configuration channel.
func (p *SwarmProvider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
	pool.GoCtx(func(routineCtx context.Context) {
		logger := log.Ctx(routineCtx).With().Str(logs.ProviderName, SwarmName).Logger()
		ctxLog := logger.WithContext(routineCtx)

		operation := func() error {
			var err error
			ctx, cancel := context.WithCancel(ctxLog)
			defer cancel()

			ctx = log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger().WithContext(ctx)

			dockerClient, err := p.createClient(ctx)
			if err != nil {
				logger.Error().Err(err).Msg("Failed to create Docker API client")
				return err
			}
			defer func() { _ = dockerClient.Close() }()

			builder := NewDynConfBuilder(p.Shared, dockerClient, true)

			serverVersion, err := dockerClient.ServerVersion(ctx, client.ServerVersionOptions{})
			if err != nil {
				logger.Error().Err(err).Msg("Failed to retrieve information of the docker client and server host")
				return err
			}

			logger.Debug().Msgf("Provider connection established with docker %s (API %s)", serverVersion.Version, serverVersion.APIVersion)

			dockerDataList, err := p.listServices(ctx, dockerClient)
			if err != nil {
				logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
				return err
			}

			configuration := builder.build(ctxLog, dockerDataList)
			configurationChan <- dynamic.Message{
				ProviderName:  SwarmName,
				Configuration: configuration,
			}
			if p.Watch {
				errChan := make(chan error)

				// TODO: This need to be change. Linked to Swarm events docker/docker#23827
				ticker := time.NewTicker(time.Duration(p.RefreshSeconds))

				pool.GoCtx(func(ctx context.Context) {
					logger := log.Ctx(ctx).With().Str(logs.ProviderName, SwarmName).Logger()
					ctx = logger.WithContext(ctx)

					defer close(errChan)
					for {
						select {
						case <-ticker.C:
							services, err := p.listServices(ctx, dockerClient)
							if err != nil {
								logger.Error().Err(err).Msg("Failed to list services for docker swarm mode")
								errChan <- err
								return
							}

							configuration := builder.build(ctx, services)
							if configuration != nil {
								configurationChan <- dynamic.Message{
									ProviderName:  SwarmName,
									Configuration: configuration,
								}
							}

						case <-ctx.Done():
							ticker.Stop()
							return
						}
					}
				})
				if err, ok := <-errChan; ok {
					return err
				}
				// channel closed
			}
			return nil
		}

		notify := func(err error, time time.Duration) {
			logger.Error().Err(err).Msgf("Provider error, retrying in %s", time)
		}
		err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), ctxLog), notify)
		if err != nil {
			logger.Error().Err(err).Msg("Cannot retrieve data")
		}
	})

	return nil
}

func (p *SwarmProvider) createClient(ctx context.Context) (*client.Client, error) {
	return createClient(ctx, p.ClientConfig)
}

func (p *SwarmProvider) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
	logger := log.Ctx(ctx)

	serviceList, err := dockerClient.ServiceList(ctx, client.ServiceListOptions{})
	if err != nil {
		return nil, err
	}

	serverVersion, err := dockerClient.ServerVersion(ctx, client.ServerVersionOptions{})
	if err != nil {
		return nil, err
	}

	networkListArgs := client.Filters{}
	// https://docs.docker.com/engine/api/v1.29/#tag/Network (Docker 17.06)
	if versions.GreaterThanOrEqualTo(serverVersion.APIVersion, "1.29") {
		networkListArgs.Add("scope", "swarm")
	} else {
		networkListArgs.Add("driver", "overlay")
	}

	networkList, err := dockerClient.NetworkList(ctx, client.NetworkListOptions{
		Filters: networkListArgs,
	})
	if err != nil {
		logger.Debug().Err(err).Msg("Failed to network inspect on client for docker")
		return nil, err
	}

	networkMap := make(map[string]*networktypes.Summary)
	for _, network := range networkList.Items {
		networkMap[network.ID] = &network
	}

	var dockerDataList []dockerData
	var dockerDataListTasks []dockerData

	for _, service := range serviceList.Items {
		dData, err := p.parseService(ctx, service, networkMap)
		if err != nil {
			logger.Error().Err(err).Msgf("Skip container %s", getServiceName(dData))
			continue
		}

		if dData.ExtraConf.LBSwarm {
			if len(dData.NetworkSettings.Networks) > 0 {
				dockerDataList = append(dockerDataList, dData)
			}
		} else {
			isGlobalSvc := service.Spec.Mode.Global != nil
			dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dData, networkMap, isGlobalSvc)
			if err != nil {
				logger.Warn().Err(err).Send()
			} else {
				dockerDataList = append(dockerDataList, dockerDataListTasks...)
			}
		}
	}

	return dockerDataList, err
}

func (p *SwarmProvider) parseService(ctx context.Context, service swarmtypes.Service, networkMap map[string]*networktypes.Summary) (dockerData, error) {
	logger := log.Ctx(ctx)

	dData := dockerData{
		ID:              service.ID,
		ServiceName:     service.Spec.Annotations.Name,
		Name:            service.Spec.Annotations.Name,
		Labels:          service.Spec.Annotations.Labels,
		NetworkSettings: networkSettings{},
	}

	extraConf, err := p.extractSwarmLabels(dData)
	if err != nil {
		return dockerData{}, err
	}
	dData.ExtraConf = extraConf

	if service.Spec.EndpointSpec == nil {
		return dData, nil
	}
	if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeDNSRR {
		if dData.ExtraConf.LBSwarm {
			logger.Warn().Msgf("Ignored %s endpoint-mode not supported, service name: %s. Fallback to Traefik load balancing", swarmtypes.ResolutionModeDNSRR, service.Spec.Annotations.Name)
		}
	} else if service.Spec.EndpointSpec.Mode == swarmtypes.ResolutionModeVIP {
		dData.NetworkSettings.Networks = make(map[string]*networkData)
		for _, virtualIP := range service.Endpoint.VirtualIPs {
			networkService := networkMap[virtualIP.NetworkID]
			if networkService == nil {
				logger.Debug().Msgf("Network not found, id: %s", virtualIP.NetworkID)
				continue
			}
			if !virtualIP.Addr.Addr().IsValid() {
				logger.Debug().Msgf("No virtual IPs found in network %s", virtualIP.NetworkID)
				continue
			}
			network := &networkData{
				Name: networkService.Name,
				ID:   virtualIP.NetworkID,
				Addr: virtualIP.Addr.Addr().String(),
			}
			dData.NetworkSettings.Networks[network.Name] = network
		}
	}
	return dData, nil
}

func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
	serviceDockerData dockerData, networkMap map[string]*networktypes.Summary, isGlobalSvc bool,
) ([]dockerData, error) {
	taskList, err := dockerClient.TaskList(ctx, client.TaskListOptions{
		Filters: make(client.Filters).Add("service", serviceID).Add("desired-state", "running"),
	})
	if err != nil {
		return nil, err
	}

	var dockerDataList []dockerData
	for _, task := range taskList.Items {
		if task.Status.State != swarmtypes.TaskStateRunning {
			continue
		}
		dData, err := parseTasks(ctx, dockerClient, task, serviceDockerData, networkMap, isGlobalSvc)
		if err != nil {
			log.Ctx(ctx).Warn().Err(err).Msgf("Error while parsing task %s", getServiceName(dData))
			continue
		}
		if len(dData.NetworkSettings.Networks) > 0 {
			dockerDataList = append(dockerDataList, dData)
		}
	}
	return dockerDataList, err
}

func parseTasks(ctx context.Context, dockerClient client.APIClient, task swarmtypes.Task, serviceDockerData dockerData,
	networkMap map[string]*networktypes.Summary, isGlobalSvc bool,
) (dockerData, error) {
	dData := dockerData{
		ID:              task.ID,
		ServiceName:     serviceDockerData.Name,
		Name:            serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
		Labels:          serviceDockerData.Labels,
		ExtraConf:       serviceDockerData.ExtraConf,
		NetworkSettings: networkSettings{},
	}

	if isGlobalSvc {
		dData.Name = serviceDockerData.Name + "." + task.ID
	}

	if task.NodeID != "" {
		res, err := dockerClient.NodeInspect(ctx, task.NodeID, client.NodeInspectOptions{})
		if err != nil {
			return dockerData{}, fmt.Errorf("inspecting node %s: %w", task.NodeID, err)
		}
		dData.NodeIP = res.Node.Status.Addr
	}

	if task.NetworksAttachments != nil {
		dData.NetworkSettings.Networks = make(map[string]*networkData)
		for _, virtualIP := range task.NetworksAttachments {
			if networkService, present := networkMap[virtualIP.Network.ID]; present {
				if len(virtualIP.Addresses) > 0 {
					// Not sure about this next loop - when would a task have multiple IP's for the same network?
					for _, addr := range virtualIP.Addresses {
						var ip string
						if addr.IsValid() {
							ip = addr.Addr().String()
						}
						network := &networkData{
							ID:   virtualIP.Network.ID,
							Name: networkService.Name,
							Addr: ip,
						}
						dData.NetworkSettings.Networks[network.Name] = network
					}
				} else {
					log.Ctx(ctx).Debug().Msgf("No IP addresses found for network %s", virtualIP.Network.ID)
				}
			}
		}
	}
	return dData, nil
}
