package impl

import (
	"context"
	"fmt"
	"io"
	"net"
	"os"
	"sync"
	"time"

	"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/go-sql-driver/mysql"
	"github.com/pingcap/dbaas/pkg/docker_db/db/iface"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

const (
	defaultTiDBVersion = "v5.2.0"
)

var (
	dbPortStart = uint16(21000 - 1)
	dbMap       = map[string]*iface.DockerDB{}

	lck sync.Mutex
)

func GetDBs() map[string]*iface.DockerDB {
	return dbMap
}

func BorrowDB() (*iface.DockerDB, error) {
	lck.Lock()
	defer lck.Unlock()

	return borrow()
}

func ReleaseDB(id string) {
	lck.Lock()
	defer lck.Unlock()

	ctx := context.Background()
	cli, err := getDockerClient()
	if err != nil {
		log.Warn("failed to get the client", zap.Error(err))
	}
	_ = cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{Force: true})
	delete(dbMap, id)
}

func ReleaseAll() {
	fmt.Println("release all databases")
	for _, item := range dbMap {
		ReleaseDB(item.ID)
	}
	stopAllDBContainer()
}

func borrow() (*iface.DockerDB, error) {
	for {
		dbPortStart++
		if IsPortAvailable(dbPortStart) {
			return startDB(defaultTiDBVersion, dbPortStart)
		}
		log.Info("port is not available, try another one", zap.Uint16("port", dbPortStart))
	}
}

func startDB(version string, port uint16) (*iface.DockerDB, error) {
	log.Info("starting a new database", zap.String("version", version), zap.Uint16("port", port), zap.Int("pid", os.Getpid()))
	ctx := context.Background()
	cli, err := getDockerClient()
	if err != nil {
		return nil, err
	}

	imageName := fmt.Sprintf("gcr.io/pingcap-public/dbaas/tidb:%s", version)
	images, err := cli.ImageList(ctx, types.ImageListOptions{
		All: false,
		Filters: filters.NewArgs(filters.KeyValuePair{
			Key:   "reference",
			Value: imageName,
		}),
	})
	if err != nil {
		return nil, err
	}

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

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

	resp, err := cli.ContainerCreate(ctx, &container.Config{
		Image:        imageName,
		Labels:       map[string]string{"component": "docker-db"},
		ExposedPorts: map[nat.Port]struct{}{nat.Port(fmt.Sprintf("%d/%s", 4000, "tcp")): {}},
	}, hostConfig, nil, nil, "")
	if err != nil {
		return nil, err
	}

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

	container, err := cli.ContainerInspect(ctx, resp.ID)
	if err != nil {
		return nil, err
	}
	ip := container.NetworkSettings.IPAddress
	db := newDockerDB(ip, port, resp.ID)
	for !isDBReady(db) {
		log.Info("waiting for DB", zap.String("version", version), zap.Uint16("port", port))
		time.Sleep(time.Millisecond * 500)
	}
	dbMap[db.ID] = db
	return db, nil
}

func newDockerDB(ip string, port uint16, id string) *iface.DockerDB {
	return &iface.DockerDB{
		Port:     port,
		Host:     "127.0.0.1",
		User:     "root",
		Password: "",
		ID:       id,
		IP:       ip,
	}
}

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

func isDBReady(db *iface.DockerDB) bool {
	dsn := fmt.Sprintf("%s:%s@(%s:%d)/%s?%s", db.User, db.Password, db.Host, db.Port, "test", "charset=utf8mb4&parseTime=True&loc=UTC")
	_, err := gorm.Open(mysql.Open(dsn), nil)
	return err == nil
}

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 stopAllDBContainer() {
	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: "component=docker-db",
		}),
	})
	if err != nil {
		log.Warn("failed to get the client", zap.Error(err))
	}
	fmt.Println("> Clean Docker DB")
	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")
}
