package docker

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
)

const MinecraftImage = "itzg/minecraft-server:java21"

// MinecraftServerSpec describes a Minecraft container to create.
type MinecraftServerSpec struct {
	ServerID         string
	MinecraftVersion string
	SoftwareFamily   string
	HostPort         int
	RamMb            int
	DataPath         string
	EulaAccepted     bool
}

// ContainerState summarizes a Docker container's runtime status.
type ContainerState struct {
	ID      string
	Status  string
	Running bool
}

// Client wraps the Docker Engine API for game server containers.
type Client struct {
	cli *client.Client
}

// New creates a Docker client connected via the given socket URL.
func New(socket string) (*Client, error) {
	cli, err := client.NewClientWithOpts(
		client.FromEnv,
		client.WithHost(socket),
		client.WithAPIVersionNegotiation(),
	)
	if err != nil {
		return nil, fmt.Errorf("create docker client: %w", err)
	}
	return &Client{cli: cli}, nil
}

// Close releases the underlying Docker client.
func (c *Client) Close() error {
	return c.cli.Close()
}

// Ping verifies connectivity to the Docker daemon.
func (c *Client) Ping(ctx context.Context) error {
	_, err := c.cli.Ping(ctx)
	return err
}

// CreateMinecraftServer creates a non-privileged Minecraft container and returns its ID.
func (c *Client) CreateMinecraftServer(ctx context.Context, spec MinecraftServerSpec) (string, error) {
	if !spec.EulaAccepted {
		return "", fmt.Errorf("eula must be accepted before provisioning")
	}
	if spec.HostPort <= 0 {
		return "", fmt.Errorf("hostPort must be positive")
	}
	if strings.TrimSpace(spec.DataPath) == "" {
		return "", fmt.Errorf("dataPath is required")
	}
	if spec.RamMb <= 0 {
		return "", fmt.Errorf("ramMb must be positive")
	}

	containerPort := nat.Port("25565/tcp")
	portBindings := nat.PortMap{
		containerPort: []nat.PortBinding{{
			HostIP:   "0.0.0.0",
			HostPort: strconv.Itoa(spec.HostPort),
		}},
	}

	version := strings.TrimSpace(spec.MinecraftVersion)
	if version == "" {
		version = "LATEST"
	}

	resp, err := c.cli.ContainerCreate(ctx,
		&container.Config{
			Image: MinecraftImage,
			Env: []string{
				"EULA=TRUE",
				"VERSION=" + version,
				"TYPE=" + mapSoftwareFamily(spec.SoftwareFamily),
				fmt.Sprintf("MEMORY=%dM", spec.RamMb),
			},
			ExposedPorts: nat.PortSet{containerPort: struct{}{}},
		},
		&container.HostConfig{
			PortBindings: portBindings,
			Mounts: []mount.Mount{{
				Type:   mount.TypeBind,
				Source: spec.DataPath,
				Target: "/data",
			}},
			Resources: container.Resources{
				Memory: int64(spec.RamMb) * 1024 * 1024,
			},
			Privileged: false,
		},
		nil,
		nil,
		containerName(spec.ServerID),
	)
	if err != nil {
		return "", fmt.Errorf("create container: %w", err)
	}
	return resp.ID, nil
}

// StartContainer starts a container by ID.
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
	if err := c.cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
		return fmt.Errorf("start container: %w", err)
	}
	return nil
}

// StopContainer stops a running container gracefully.
func (c *Client) StopContainer(ctx context.Context, containerID string) error {
	timeout := 30
	if err := c.cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}); err != nil {
		return fmt.Errorf("stop container: %w", err)
	}
	return nil
}

// RemoveContainer removes a container, forcing removal if still running.
func (c *Client) RemoveContainer(ctx context.Context, containerID string) error {
	if err := c.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}); err != nil {
		return fmt.Errorf("remove container: %w", err)
	}
	return nil
}

// GetContainerState inspects a container and returns its current state.
func (c *Client) GetContainerState(ctx context.Context, containerID string) (*ContainerState, error) {
	inspect, err := c.cli.ContainerInspect(ctx, containerID)
	if err != nil {
		return nil, fmt.Errorf("inspect container: %w", err)
	}
	return &ContainerState{
		ID:      inspect.ID,
		Status:  inspect.State.Status,
		Running: inspect.State.Running,
	}, nil
}

// ListManagedContainers returns container IDs labeled for a given server ID.
func (c *Client) ListManagedContainers(ctx context.Context, serverID string) ([]string, error) {
	containers, err := c.cli.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filters.NewArgs(filters.Arg("name", containerName(serverID))),
	})
	if err != nil {
		return nil, fmt.Errorf("list containers: %w", err)
	}
	ids := make([]string, 0, len(containers))
	for _, ctr := range containers {
		ids = append(ids, ctr.ID)
	}
	return ids, nil
}

func containerName(serverID string) string {
	return "hexahost-" + serverID
}

func mapSoftwareFamily(family string) string {
	switch strings.ToUpper(strings.TrimSpace(family)) {
	case "PAPER":
		return "PAPER"
	default:
		return "VANILLA"
	}
}
