package docker

import (
	"context"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"io"
	"net"
	"net/http"
	"os"
	"time"
)

const (
	esVersion        = "7.15.1"
	dockerLabelKey   = "component"
	dockerLabelValue = "test-es-docker"
)

func AvailablePort() int {
	ln, err := net.Listen("tcp", ":0")
	if err != nil {
		panic(err)
	}
	port := ln.Addr().(*net.TCPAddr).Port
	if err := ln.Close(); err != nil {
		panic(err)
	}
	return port
}

func IsPortAvailable(port int) bool {
	ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
	if err != nil {
		log.Info("cat not listen the port", zap.Int("port", port), zap.Error(err))
		return false
	}
	err = ln.Close()
	if err != nil {
		log.Warn("Couldn't stop listening on port", zap.Int("port", port), zap.Error(err))
		return false
	}
	return true
}

func GetDockerClient() (*client.Client, error) {
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		return nil, err
	}
	err = client.WithVersion("1.39")(cli)
	if err != nil {
		return nil, err
	}
	return cli, nil
}

func StartES() (port int, err error) {
	port = 31000
	for !IsPortAvailable(port) {
		if isESUp(port) {
			return port, nil
		}
		port++
	}
	return port, StartESOnPort(port)
}

func StartESOnPort(port int) error {
	ctx := context.Background()
	cli, err := GetDockerClient()
	if err != nil {
		return err
	}

	imageName := fmt.Sprintf("docker.elastic.co/elasticsearch/elasticsearch:%s", esVersion)
	images, err := cli.ImageList(ctx, types.ImageListOptions{
		All: false,
		Filters: filters.NewArgs(filters.KeyValuePair{
			Key:   "reference",
			Value: imageName,
		}),
	})
	if err != nil {
		return err
	}

	if len(images) < 1 {
		log.Info("image not found, pull image", zap.String("version", esVersion))
		out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
		if err != nil {
			return nil
		}
		log.Info("pull es image done", zap.String("version", esVersion))
		_, _ = io.Copy(os.Stdout, out)
	}

	hostConfig := &container.HostConfig{
		PortBindings: nat.PortMap{
			"9200/tcp": []nat.PortBinding{
				{
					HostIP:   "127.0.0.1",
					HostPort: fmt.Sprintf("%d", port),
				},
			},
		},
	}

	resp, err := cli.ContainerCreate(ctx, &container.Config{
		Image:        imageName,
		Env:          []string{"discovery.type=single-node"},
		Labels:       map[string]string{dockerLabelKey: dockerLabelValue},
		ExposedPorts: map[nat.Port]struct{}{nat.Port(fmt.Sprintf("%d/%s", 9200, "tcp")): {}},
	}, hostConfig, nil, nil, "")
	if err != nil {
		return err
	}

	err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
	if err != nil {
		return err
	}

	for !isESUp(port) {
		log.Info("waiting for es", zap.String("version", esVersion), zap.Int("port", port))
		time.Sleep(time.Second * 2)
	}

	return nil
}

func CleanESKibanaDocker() {
	fmt.Println("release all es and kibana docker")
	ctx := context.Background()
	cli, err := GetDockerClient()
	if err != nil {
		log.Warn("failed to get the client", zap.Error(err))
	}
	cs, err := cli.ContainerList(ctx, types.ContainerListOptions{
		Filters: filters.NewArgs(filters.KeyValuePair{
			Key:   "label",
			Value: fmt.Sprintf("%s=%s", dockerLabelKey, dockerLabelValue),
		}),
	})
	if err != nil {
		log.Warn("failed to get the client", zap.Error(err))
	}
	fmt.Println("> Clean es and kibana Docker")
	for _, item := range cs {
		fmt.Printf("removing container %s\n", item.ID)
		_ = cli.ContainerRemove(ctx, item.ID, types.ContainerRemoveOptions{Force: true})
	}
	fmt.Println("> Clean successful")
}

func isESUp(port int) bool {
	httpCli := http.Client{
		Timeout: 1 * time.Second,
	}
	resp, err := httpCli.Get(fmt.Sprintf("http://127.0.0.1:%d", port))
	if err != nil {
		log.Warn("waiting for es", zap.Error(err))
		return false
	}
	return resp.StatusCode == http.StatusOK
}
