package dockerapi

import (
	"context"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/yusing/go-proxy/agent/pkg/agent"
	apitypes "github.com/yusing/go-proxy/internal/api/types"
	config "github.com/yusing/go-proxy/internal/config/types"
	"github.com/yusing/go-proxy/internal/docker"
	"github.com/yusing/go-proxy/internal/gperr"
	"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
	"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
)

type (
	DockerClients     map[string]*docker.SharedClient
	ResultType[T any] interface {
		map[string]T | []T
	}
)

// getDockerClients returns a map of docker clients for the current config.
//
// Returns a map of docker clients by server name and an error if any.
//
// Even if there are errors, the map of docker clients might not be empty.
func getDockerClients() (DockerClients, gperr.Error) {
	cfg := config.GetInstance()

	dockerHosts := cfg.Value().Providers.Docker
	dockerClients := make(DockerClients)

	connErrs := gperr.NewBuilder("failed to connect to docker")

	for name, host := range dockerHosts {
		dockerClient, err := docker.NewClient(host)
		if err != nil {
			connErrs.Add(err)
			continue
		}
		dockerClients[name] = dockerClient
	}

	for _, agent := range agent.ListAgents() {
		dockerClient, err := docker.NewClient(agent.FakeDockerHost())
		if err != nil {
			connErrs.Add(err)
			continue
		}
		dockerClients[agent.Name] = dockerClient
	}

	return dockerClients, connErrs.Error()
}

func getDockerClient(server string) (*docker.SharedClient, bool, error) {
	cfg := config.GetInstance()
	var host string
	for name, h := range cfg.Value().Providers.Docker {
		if name == server {
			host = h
			break
		}
	}
	if host == "" {
		for _, agent := range agent.ListAgents() {
			if agent.Name == server {
				host = agent.FakeDockerHost()
				break
			}
		}
	}
	if host == "" {
		return nil, false, nil
	}
	dockerClient, err := docker.NewClient(host)
	if err != nil {
		return nil, false, err
	}
	return dockerClient, true, nil
}

// closeAllClients closes all docker clients after a delay.
//
// This is used to ensure that all docker clients are closed after the http handler returns.
func closeAllClients(dockerClients DockerClients) {
	for _, dockerClient := range dockerClients {
		dockerClient.Close()
	}
}

func handleResult[V any, T ResultType[V]](c *gin.Context, errs error, result T) {
	if errs != nil {
		if len(result) == 0 {
			c.Error(apitypes.InternalServerError(errs, "docker errors"))
			return
		}
	}
	c.JSON(http.StatusOK, result)
}

func serveHTTP[V any, T ResultType[V]](c *gin.Context, getResult func(ctx context.Context, dockerClients DockerClients) (T, gperr.Error)) {
	dockerClients, err := getDockerClients()
	if err != nil {
		handleResult[V, T](c, err, nil)
		return
	}
	defer closeAllClients(dockerClients)

	if httpheaders.IsWebsocket(c.Request.Header) {
		websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
			return getResult(c.Request.Context(), dockerClients)
		})
	} else {
		result, err := getResult(c.Request.Context(), dockerClients)
		handleResult[V](c, err, result)
	}
}
