package docker

import (
	"context"
	"fmt"
	"io"
	"os"
	"slices"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/client"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/google/uuid"
	specs "github.com/opencontainers/image-spec/specs-go/v1"
)

type Client struct {
	ID         string       `yaml:"id,omitempty" json:"id,omitempty"`
	Image      string       `yaml:"image,omitempty" json:"image,omitempty"`
	Entrypoint []string     `yaml:"entrypoint,omitempty" json:"entrypoint,omitempty"`
	Cmd        []string     `yaml:"cmd,omitempty" json:"cmd,omitempty"`
	Env        []string     `yaml:"env,omitempty" json:"env,omitempty"`
	Mounts     []HostMounts `yaml:"mounts,omitempty" json:"mounts,omitempty"`
}

type HostMounts struct {
	Source   string
	Target   string
	ReadOnly bool
}

func NewClient(id, image string, entry []string, cmd []string, env []string, mounts []HostMounts) *Client {
	return &Client{
		ID:         id,
		Image:      image,
		Entrypoint: entry,
		Cmd:        cmd,
		Env:        env,
		Mounts:     mounts,
	}
}

func (c *Client) Run(ctx context.Context) error {
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return fmt.Errorf("failed to create docker client: %w", err)
	}

	images, err := cli.ImageList(ctx, image.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list images: %w", err)
	}
	found := false
	for _, image := range images {
		if slices.Contains(image.RepoTags, c.Image) {
			found = true
			break
		}
	}
	if !found {
		reader, err := cli.ImagePull(ctx, c.Image, image.PullOptions{
			// Platform: "linux/amd64",
		})
		if err != nil {
			return err
		}
		defer reader.Close()

		io.Copy(os.Stdout, reader)
	}

	var mounts []mount.Mount
	for _, m := range c.Mounts {
		mounts = append(mounts, mount.Mount{
			Type:     mount.TypeBind,
			Source:   m.Source,
			Target:   m.Target,
			ReadOnly: m.ReadOnly,
			BindOptions: &mount.BindOptions{
				CreateMountpoint: true,
			},
		})
	}

	resp, err := cli.ContainerCreate(ctx,
		&container.Config{
			Image:           c.Image,
			Entrypoint:      c.Entrypoint,
			Cmd:             c.Cmd,
			Env:             c.Env,
			Tty:             true,
			AttachStdout:    true,
			AttachStderr:    true,
			NetworkDisabled: true,
		}, &container.HostConfig{
			AutoRemove: true,
			// Resources: container.Resources{Memory: 1 << 30, MemorySwap: 1 << 30},
			Mounts: mounts,
		}, &network.NetworkingConfig{}, &specs.Platform{
			OS:           "linux",
			Architecture: "amd64",
		}, "ipsw-idapro-"+uuid.New().String())
	if err != nil {
		return fmt.Errorf("failed to create container: %w", err)
	}

	if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
		return fmt.Errorf("failed to start container: %w", err)
	}

	go func() {
		out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{
			ShowStdout: true,
			ShowStderr: true,
			Follow:     true,
			Timestamps: false,
		})
		if err != nil {
			panic(fmt.Errorf("failed to get container logs: %w", err))
		}

		stdcopy.StdCopy(os.Stdout, os.Stderr, out)
	}()

	statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
	select {
	case err := <-errCh:
		if err != nil {
			return fmt.Errorf("error waiting for container: %w", err)
		}
	case <-statusCh:
	}

	return nil
}
