/*
   Copyright 2020 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 bridge

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strconv"

	"github.com/compose-spec/compose-go/v2/types"
	"github.com/containerd/errdefs"
	"github.com/docker/cli/cli/command"
	cli "github.com/docker/cli/cli/command/container"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/image"
	"github.com/moby/moby/api/types/network"
	"github.com/moby/moby/client"
	"github.com/moby/moby/client/pkg/jsonmessage"
	"github.com/sirupsen/logrus"
	"go.yaml.in/yaml/v4"

	"github.com/docker/compose/v5/pkg/api"
	"github.com/docker/compose/v5/pkg/utils"
)

type ConvertOptions struct {
	Output          string
	Templates       string
	Transformations []string
}

func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
	if len(opts.Transformations) == 0 {
		opts.Transformations = []string{DefaultTransformerImage}
	}
	// Load image references, secrets and configs, also expose ports
	project, err := LoadAdditionalResources(ctx, dockerCli, project)
	if err != nil {
		return err
	}
	// for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
	raw, err := project.MarshalYAML(types.WithSecretContent)
	// Marshall to YAML
	if err != nil {
		return fmt.Errorf("cannot render project into yaml: %w", err)
	}
	var model map[string]any
	err = yaml.Unmarshal(raw, &model)
	if err != nil {
		return fmt.Errorf("cannot render project into yaml: %w", err)
	}

	if opts.Output != "" {
		_ = os.RemoveAll(opts.Output)
		err := os.MkdirAll(opts.Output, 0o744)
		if err != nil && !os.IsExist(err) {
			return fmt.Errorf("cannot create output folder: %w", err)
		}
	}
	// Run Transformers images
	return convert(ctx, dockerCli, model, opts)
}

func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
	raw, err := yaml.Marshal(model)
	if err != nil {
		return err
	}

	dir, err := os.MkdirTemp("", "compose-convert-*")
	if err != nil {
		return err
	}
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			logrus.Warnf("failed to remove temp dir %s: %v", dir, err)
		}
	}()

	composeYaml := filepath.Join(dir, "compose.yaml")
	err = os.WriteFile(composeYaml, raw, 0o600)
	if err != nil {
		return err
	}

	out, err := filepath.Abs(opts.Output)
	if err != nil {
		return err
	}
	binds := []string{
		fmt.Sprintf("%s:%s", dir, "/in"),
		fmt.Sprintf("%s:%s", out, "/out"),
	}
	if opts.Templates != "" {
		templateDir, err := filepath.Abs(opts.Templates)
		if err != nil {
			return err
		}
		binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
	}

	for _, transformation := range opts.Transformations {
		_, err = inspectWithPull(ctx, dockerCli, transformation)
		if err != nil {
			return err
		}

		containerConfig := &container.Config{
			Image: transformation,
			Env:   []string{"LICENSE_AGREEMENT=true"},
		}
		// On POSIX systems, this is a decimal number representing the uid.
		// On Windows, this is a security identifier (SID) in a string format and the engine isn't able to manage it
		if runtime.GOOS != "windows" {
			usr, err := user.Current()
			if err != nil {
				return err
			}
			containerConfig.User = usr.Uid
		}
		created, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
			Config: containerConfig,
			HostConfig: &container.HostConfig{
				Binds:      binds,
				AutoRemove: true,
			},
			NetworkingConfig: &network.NetworkingConfig{},
		})
		if err != nil {
			return err
		}

		err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
			Attach:     true,
			Containers: []string{created.ID},
		})
		if err != nil {
			return err
		}
	}
	return nil
}

// LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
func LoadAdditionalResources(ctx context.Context, dockerCLI command.Cli, project *types.Project) (*types.Project, error) {
	for name, service := range project.Services {
		imageName := api.GetImageNameOrDefault(service, project.Name)

		inspect, err := inspectWithPull(ctx, dockerCLI, imageName)
		if err != nil {
			return nil, err
		}
		service.Image = imageName
		exposed := utils.Set[string]{}
		exposed.AddAll(service.Expose...)
		for port := range inspect.Config.ExposedPorts {
			p, err := network.ParsePort(port)
			if err != nil {
				return nil, err
			}
			exposed.Add(strconv.Itoa(int(p.Num())))
		}
		for _, port := range service.Ports {
			exposed.Add(strconv.Itoa(int(port.Target)))
		}
		service.Expose = exposed.Elements()
		project.Services[name] = service
	}

	for name, secret := range project.Secrets {
		f, err := loadFileObject(types.FileObjectConfig(secret))
		if err != nil {
			return nil, err
		}
		project.Secrets[name] = types.SecretConfig(f)
	}

	for name, config := range project.Configs {
		f, err := loadFileObject(types.FileObjectConfig(config))
		if err != nil {
			return nil, err
		}
		project.Configs[name] = types.ConfigObjConfig(f)
	}

	return project, nil
}

func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
	if !conf.External {
		switch {
		case conf.Environment != "":
			conf.Content = os.Getenv(conf.Environment)
		case conf.File != "":
			bytes, err := os.ReadFile(conf.File)
			if err != nil {
				return conf, err
			}
			conf.Content = string(bytes)
		}
	}
	return conf, nil
}

func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
	inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
	if errdefs.IsNotFound(err) {
		var stream io.ReadCloser
		stream, err = dockerCli.Client().ImagePull(ctx, imageName, client.ImagePullOptions{})
		if err != nil {
			return image.InspectResponse{}, err
		}
		defer func() { _ = stream.Close() }()

		out := dockerCli.Out()
		err = jsonmessage.DisplayJSONMessagesStream(stream, out, out.FD(), out.IsTerminal(), nil)
		if err != nil {
			return image.InspectResponse{}, err
		}
		if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
			return image.InspectResponse{}, err
		}
	}
	return inspect.InspectResponse, err
}
