package swarm

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	basictypes "github.com/moby/moby/api/types"
	"github.com/moby/moby/api/types/swarm"
	"github.com/moby/moby/v2/daemon/internal/timestamp"
	"github.com/moby/moby/v2/daemon/internal/versions"
	"github.com/moby/moby/v2/daemon/server/backend"
	"github.com/moby/moby/v2/daemon/server/httputils"
	"github.com/moby/moby/v2/daemon/server/httputils/logstream"
	"github.com/moby/moby/v2/errdefs"
)

// swarmLogs takes an http response, request, and selector, and writes the logs
// specified by the selector to the response
func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, selector *backend.LogSelector) error {
	// Args are validated before the stream starts because when it starts we're
	// sending HTTP 200 by writing an empty chunk of data to tell the client that
	// daemon is going to stream. By sending this initial HTTP 200 we can't report
	// any error after the stream starts (i.e. container not found, wrong parameters)
	// with the appropriate status code.
	stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
	if !stdout && !stderr {
		return errdefs.InvalidParameter(errors.New("must specify at least one of 'stdout' or 'stderr'"))
	}

	var since time.Time
	if s := r.Form.Get("since"); s != "" {
		var err error
		since, err = timestamp.ParseUnixTimestamp(s)
		if err != nil {
			return errdefs.InvalidParameter(fmt.Errorf(`invalid value for "since": %w`, err))
		}
	}

	// there is probably a neater way to manufacture the LogsOptions
	// struct, probably in the caller, to eliminate the dependency on net/http
	logsConfig := &backend.ContainerLogsOptions{
		Follow:     httputils.BoolValue(r, "follow"),
		Timestamps: httputils.BoolValue(r, "timestamps"),
		Since:      since,
		Tail:       r.Form.Get("tail"),
		ShowStdout: stdout,
		ShowStderr: stderr,
		Details:    httputils.BoolValue(r, "details"),
	}

	tty := false
	// checking for whether logs are TTY involves iterating over every service
	// and task. idk if there is a better way
	for _, service := range selector.Services {
		s, err := sr.backend.GetService(service, false)
		if err != nil {
			// maybe should return some context with this error?
			return err
		}
		tty = (s.Spec.TaskTemplate.ContainerSpec != nil && s.Spec.TaskTemplate.ContainerSpec.TTY) || tty
	}
	for _, task := range selector.Tasks {
		t, err := sr.backend.GetTask(task)
		if err != nil {
			// as above
			return err
		}
		tty = t.Spec.ContainerSpec.TTY || tty
	}

	msgs, err := sr.backend.ServiceLogs(ctx, selector, logsConfig)
	if err != nil {
		return err
	}

	contentType := basictypes.MediaTypeRawStream
	if !tty && versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.42") {
		contentType = basictypes.MediaTypeMultiplexedStream
	}
	w.Header().Set("Content-Type", contentType)
	logstream.Write(ctx, w, msgs, logsConfig, !tty)
	return nil
}

// adjustForAPIVersion takes a version and service spec and removes fields to
// make the spec compatible with the specified version.
func adjustForAPIVersion(cliVersion string, service *serviceWithLegacy) {
	if cliVersion == "" {
		return
	}

	if versions.LessThan(cliVersion, "1.52") {
		if service.TaskTemplate.Resources != nil {
			service.TaskTemplate.Resources.SwapBytes = nil
			service.TaskTemplate.Resources.MemorySwappiness = nil
		}
	}

	if versions.LessThan(cliVersion, "1.46") {
		if service.TaskTemplate.ContainerSpec != nil {
			for i, mount := range service.TaskTemplate.ContainerSpec.Mounts {
				if mount.TmpfsOptions != nil {
					mount.TmpfsOptions.Options = nil
					service.TaskTemplate.ContainerSpec.Mounts[i] = mount
				}
			}
		}
	}
	if versions.LessThan(cliVersion, "1.40") {
		if service.TaskTemplate.ContainerSpec != nil {
			// Sysctls for docker swarm services weren't supported before
			// API version 1.40
			service.TaskTemplate.ContainerSpec.Sysctls = nil

			if service.TaskTemplate.ContainerSpec.Privileges != nil && service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec != nil {
				// Support for setting credential-spec through configs was added in API 1.40
				service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config = ""
			}
			for _, config := range service.TaskTemplate.ContainerSpec.Configs {
				// support for the Runtime target was added in API 1.40
				config.Runtime = nil
			}
		}

		if service.TaskTemplate.Placement != nil {
			// MaxReplicas for docker swarm services weren't supported before
			// API version 1.40
			service.TaskTemplate.Placement.MaxReplicas = 0
		}
	}
	if versions.LessThan(cliVersion, "1.41") {
		if service.TaskTemplate.ContainerSpec != nil {
			// Capabilities and Ulimits for docker swarm services weren't
			// supported before API version 1.41
			service.TaskTemplate.ContainerSpec.CapabilityAdd = nil
			service.TaskTemplate.ContainerSpec.CapabilityDrop = nil
			service.TaskTemplate.ContainerSpec.Ulimits = nil
		}
		if service.TaskTemplate.Resources != nil && service.TaskTemplate.Resources.Limits != nil {
			// Limits.Pids  not supported before API version 1.41
			service.TaskTemplate.Resources.Limits.Pids = 0
		}

		// jobs were only introduced in API version 1.41. Nil out both Job
		// modes; if the service is one of these modes and subsequently has no
		// mode, then something down the pipe will thrown an error.
		service.Mode.ReplicatedJob = nil
		service.Mode.GlobalJob = nil
	}

	if versions.LessThan(cliVersion, "1.44") {
		if service.TaskTemplate.ContainerSpec != nil {
			// seccomp, apparmor, and no_new_privs were added in 1.44.
			if service.TaskTemplate.ContainerSpec.Privileges != nil {
				service.TaskTemplate.ContainerSpec.Privileges.Seccomp = nil
				service.TaskTemplate.ContainerSpec.Privileges.AppArmor = nil
				service.TaskTemplate.ContainerSpec.Privileges.NoNewPrivileges = false
			}
			if service.TaskTemplate.ContainerSpec.Healthcheck != nil {
				// StartInterval was added in API 1.44
				service.TaskTemplate.ContainerSpec.Healthcheck.StartInterval = 0
			}
		}

		// Always prefer NetworkAttachmentConfigs from TaskTemplate
		// but fallback to service spec for backward compatibility.
		//
		// The ServiceSpec.Networks field was deprecated in API v1.25, with
		// the deprecation notice updated in API v1.44. We only consider this
		// field on API < v1.44 and if the replacement TaskSpec.Networks is
		// not set.
		if len(service.TaskTemplate.Networks) == 0 && len(service.Networks) > 0 {
			service.TaskTemplate.Networks = make([]swarm.NetworkAttachmentConfig, 0, len(service.Networks))
			for _, n := range service.Networks {
				service.TaskTemplate.Networks = append(service.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{
					Target:     n.Target,
					Aliases:    n.Aliases,
					DriverOpts: n.DriverOpts,
				})
			}
		}

	}

	if versions.LessThan(cliVersion, "1.46") {
		if service.TaskTemplate.ContainerSpec != nil && service.TaskTemplate.ContainerSpec.OomScoreAdj != 0 {
			// OomScoreAdj was added in API 1.46
			service.TaskTemplate.ContainerSpec.OomScoreAdj = 0
		}
	}
}
