package tools

import (
	"context"
	"io"
	"net/netip"
	"strings"
	"time"

	"github.com/moby/go-archive"
	"github.com/moby/moby/api/pkg/stdcopy"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/network"
	"github.com/moby/term"

	"github.com/moby/moby/client"
	"github.com/moby/moby/client/pkg/jsonmessage"
	v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

func Docker() *docker {
	ctx := context.Background()

	return &docker{
		ctx: ctx,
	}
}

type (
	docker struct {
		ctx context.Context
	}
	dockerImage struct {
		docker *docker
		auth   string
	}
	dockerContainer struct {
		docker     *docker
		id         string
		name       string
		config     *container.Config
		hostconfig *container.HostConfig
		network    *network.NetworkingConfig
	}
)

func (t *docker) client() (*client.Client, error) {
	cli, err := client.New(client.FromEnv)
	if err != nil {
		return nil, err
	}

	return cli, nil
}

func (t *docker) ContainerList() ([]*dockerContainer, error) {
	cli, err := t.client()
	if err != nil {
		return nil, err
	}

	defer cli.Close()

	containers, err := cli.ContainerList(context.Background(), client.ContainerListOptions{All: true})
	if err != nil {
		return nil, err
	}

	res := []*dockerContainer{}
	for _, ctr := range containers.Items {
		c, err := t.ContainerGetByID(ctr.ID)
		if err != nil {
			return nil, err
		}
		res = append(res, c)
	}

	return res, nil
}

func (t *docker) ContainerGetByID(id string) (*dockerContainer, error) {
	cli, err := t.client()
	if err != nil {
		return nil, err
	}

	defer cli.Close()

	c, err := cli.ContainerInspect(context.Background(), id, client.ContainerInspectOptions{})
	if err != nil {
		return nil, err
	}

	return &dockerContainer{
		docker:     t,
		id:         id,
		name:       strings.TrimPrefix(c.Container.Name, "/"),
		config:     c.Container.Config,
		hostconfig: c.Container.HostConfig,
		network: &network.NetworkingConfig{
			EndpointsConfig: map[string]*network.EndpointSettings{},
		},
	}, nil
}
func (t *docker) ContainerGetByName(name string) (*dockerContainer, error) {
	cli, err := t.client()
	if err != nil {
		return nil, err
	}

	defer cli.Close()

	filters := client.Filters{}
	filters.Add("name", name)

	containers, err := cli.ContainerList(context.Background(),
		client.ContainerListOptions{
			Filters: filters,
		})
	if err != nil {
		return nil, err
	}

	var id string

	for _, ctr := range containers.Items {
		for _, n := range ctr.Names {
			if strings.TrimPrefix(n, "/") == name {
				id = ctr.ID
			}
		}
	}

	return t.ContainerGetByID(id)
}

func (t *docker) ContainerNew(name string) *dockerContainer {
	return &dockerContainer{
		name:   name,
		docker: t,
		config: &container.Config{
			ExposedPorts: network.PortSet{},
		},
		hostconfig: &container.HostConfig{
			AutoRemove: false,
		},
		network: &network.NetworkingConfig{
			EndpointsConfig: map[string]*network.EndpointSettings{},
		},
	}
}
func (t *dockerContainer) ID() string                               { return t.id }
func (t *dockerContainer) Name() string                             { return t.name }
func (t *dockerContainer) Config() *container.Config                { return t.config }
func (t *dockerContainer) HostConfig() *container.HostConfig        { return t.hostconfig }
func (t *dockerContainer) NetworkConfig() *network.NetworkingConfig { return t.network }
func (t *dockerContainer) Exec()                                    {}
func (t *dockerContainer) Run(destOut io.Writer, destErr io.Writer, timeout time.Duration) (int64, error) {
	err := t.Start()
	if err != nil {
		return -1, err
	}

	res, err := t.Wait(timeout)

	errs := t.Stop()
	if errs != nil {
		destErr.Write([]byte(errs.Error()))
	}
	errs = t.Logs(destOut, destErr)
	if errs != nil {
		destErr.Write([]byte(errs.Error()))
	}
	errs = t.Remove()
	if errs != nil {
		destErr.Write([]byte(errs.Error()))
	}

	return res, err
}
func (t *dockerContainer) Logs(destOut io.Writer, destErr io.Writer) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}
	defer cli.Close()

	res, err := cli.ContainerLogs(t.docker.ctx, t.id, client.ContainerLogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		// Details:    true,
	})
	if err != nil {
		return err
	}

	defer res.Close()

	_, err = stdcopy.StdCopy(destOut, destErr, res)
	if err != nil {
		return err
	}

	return nil
}
func (t *dockerContainer) AddPort(containerPort, address, hostPort string) error {
	cp, err := network.ParsePort(containerPort)
	if err != nil {
		return err
	}
	addr, err := netip.ParseAddr(address)
	if err != nil {
		return err
	}

	if t.hostconfig.PortBindings == nil {
		t.hostconfig.PortBindings = network.PortMap{}
	}

	t.hostconfig.PortBindings[cp] = []network.PortBinding{
		{
			HostPort: hostPort,
			HostIP:   addr,
		},
	}

	return nil
}
func (t *dockerContainer) State() (container.ContainerState, error) {
	cli, err := t.docker.client()
	if err != nil {
		return "", err
	}

	c, err := cli.ContainerInspect(context.Background(), t.id, client.ContainerInspectOptions{})
	if err != nil {
		return "", err
	}

	return c.Container.State.Status, nil
}
func (t *dockerContainer) Wait(timeout time.Duration) (int64, error) {
	cli, err := t.docker.client()
	if err != nil {
		return -1, err
	}
	defer cli.Close()

	ctx, _ := context.WithTimeout(t.docker.ctx, timeout)

	wait := cli.ContainerWait(ctx, t.id, client.ContainerWaitOptions{})
	select {
	case err := <-wait.Error:
		if err != nil {
			return -1, err
		}
	case res := <-wait.Result:
		return res.StatusCode, nil
	}

	return -1, nil
}
func (t *dockerContainer) Create() error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}
	defer cli.Close()

	create, err := cli.ContainerCreate(context.Background(), client.ContainerCreateOptions{
		Name:             t.name,
		Config:           t.config,
		HostConfig:       t.hostconfig,
		NetworkingConfig: t.network,
		Platform:         &v1.Platform{},
	})
	if err != nil {
		return err
	}

	t.id = create.ID

	return nil
}
func (t *dockerContainer) Start() error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	_, err = cli.ContainerStart(t.docker.ctx, t.id, client.ContainerStartOptions{})
	if err != nil {
		return err
	}

	return nil
}
func (t *dockerContainer) Stop() error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	_, err = cli.ContainerStop(t.docker.ctx, t.id, client.ContainerStopOptions{})
	if err != nil {
		return err
	}

	return nil
}
func (t *dockerContainer) Remove() error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	_, err = cli.ContainerRemove(t.docker.ctx, t.id,
		client.ContainerRemoveOptions{
			// RemoveLinks:   true,
			RemoveVolumes: true,
			Force:         true,
		})
	if err != nil {
		return err
	}

	t.id = ""

	return nil
}

func (t *docker) Image() *dockerImage {
	return &dockerImage{
		docker: t,
	}
}

// RegistryAuth is the base64 encoded credentials for the registry
func (t *dockerImage) Auth(RegistryAuth string) { t.auth = RegistryAuth }
func (t *dockerImage) Build(tag, path, dockerfile string) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	tar, err := archive.TarWithOptions(path, &archive.TarOptions{})
	if err != nil {
		return err
	}

	opts := client.ImageBuildOptions{
		Dockerfile: dockerfile,
		Tags:       []string{tag},
		Remove:     true,
	}

	buildResponse, err := cli.ImageBuild(t.docker.ctx, tar, opts)
	if err != nil {
		return err
	}
	defer buildResponse.Body.Close()

	if log != nil {
		fd, isTerminal := term.GetFdInfo(log)
		return jsonmessage.DisplayJSONMessagesStream(buildResponse.Body, log, fd, isTerminal, nil)
	}

	return nil
}
func (t *dockerImage) Tag(source, target string) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	_, err = cli.ImageTag(t.docker.ctx, client.ImageTagOptions{
		Source: source,
		Target: target,
	})
	if err != nil {
		return err
	}

	return nil
}
func (t *dockerImage) Push(image string) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	res, err := cli.ImagePush(t.docker.ctx, image, client.ImagePushOptions{
		RegistryAuth: t.auth,
	})
	if err != nil {
		return err
	}
	defer res.Close()

	if log != nil {
		fd, isTerminal := term.GetFdInfo(log)
		return jsonmessage.DisplayJSONMessagesStream(res, log, fd, isTerminal, nil)
	}

	return nil

}
func (t *dockerImage) Remove(image string) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	_, err = cli.ImageRemove(t.docker.ctx, image, client.ImageRemoveOptions{})
	if err != nil {
		return err
	}

	return nil
}
func (t *dockerImage) Pull(image string, timeout time.Duration) error {
	cli, err := t.docker.client()
	if err != nil {
		return err
	}

	defer cli.Close()

	ctx, _ := context.WithTimeout(t.docker.ctx, timeout)
	res, err := cli.ImagePull(ctx, image, client.ImagePullOptions{
		RegistryAuth: t.auth,
	})
	if err != nil {
		return err
	}

	defer res.Close()

	if log != nil {
		fd, isTerminal := term.GetFdInfo(log)
		return jsonmessage.DisplayJSONMessagesStream(res, log, fd, isTerminal, nil)
	} else {
		err = res.Wait(ctx)
		if err != nil {
			return err
		}
	}

	return nil
}
