package infra

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// DockerContainerInfo holds the essential fields for a Docker container,
// mapped from the Engine API /containers/json response.
type DockerContainerInfo struct {
	ID       string            // short id (12 chars)
	Names    []string          // e.g. ["/my-container"]
	Image    string            // image name
	State    string            // running|exited|paused|...
	Status   string            // human label, e.g. "Up 2 hours"
	Ports    []string          // e.g. "0.0.0.0:8080->8080/tcp"
	Networks []string          // network names
	Labels   map[string]string // container labels
}

// DockerContainerListOpts controls which containers are returned and where
// the Docker daemon is reached.
type DockerContainerListOpts struct {
	All        bool     // if true, include stopped/exited containers (default: false = only running)
	Filters    []string // Docker filter expressions, e.g. "label=app=agents_and_robots", "status=running"
	DockerHost string   // default "unix:///var/run/docker.sock". Use "tcp://host:port" for remote.
}

// DockerContainerList lists Docker containers on the local (or remote) host
// by calling the Docker Engine HTTP API directly. No docker CLI required.
//
// The DockerHost field selects the transport:
//   - empty or "unix:///var/run/docker.sock" → unix socket
//   - "tcp://host:port"                       → plain HTTP (no TLS)
func DockerContainerList(opts DockerContainerListOpts) ([]DockerContainerInfo, error) {
	client, baseURL, err := dockerListHTTPClient(opts.DockerHost)
	if err != nil {
		return nil, fmt.Errorf("docker_container_list: build client: %w", err)
	}

	// Build query string
	q := url.Values{}
	if opts.All {
		q.Set("all", "1")
	}
	if len(opts.Filters) > 0 {
		filters := buildDockerFilters(opts.Filters)
		b, err := json.Marshal(filters)
		if err != nil {
			return nil, fmt.Errorf("docker_container_list: marshal filters: %w", err)
		}
		q.Set("filters", string(b))
	}

	endpoint := baseURL + "/containers/json"
	if len(q) > 0 {
		endpoint += "?" + q.Encode()
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("docker_container_list: new request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("docker_container_list: do request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("docker_container_list: read body: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("docker_container_list: daemon returned %d: %s", resp.StatusCode, string(body))
	}

	return parseDockerContainerList(body)
}

// dockerListHTTPClient returns an *http.Client wired for unix socket or TCP,
// and a base URL ("http://localhost" for unix, "http://host:port" for TCP).
func dockerListHTTPClient(dockerHost string) (*http.Client, string, error) {
	if dockerHost == "" {
		dockerHost = "unix:///var/run/docker.sock"
	}

	if strings.HasPrefix(dockerHost, "unix://") {
		sockPath := strings.TrimPrefix(dockerHost, "unix://")
		transport := &http.Transport{
			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
				return (&net.Dialer{}).DialContext(ctx, "unix", sockPath)
			},
		}
		return &http.Client{Transport: transport}, "http://localhost", nil
	}

	if strings.HasPrefix(dockerHost, "tcp://") {
		host := strings.TrimPrefix(dockerHost, "tcp://")
		return &http.Client{}, "http://" + host, nil
	}

	return nil, "", fmt.Errorf("unsupported docker host scheme: %q (use unix:// or tcp://)", dockerHost)
}

// buildDockerFilters converts []string{"label=k=v", "status=running"} into
// the map[string][]string format that the Docker Engine API expects.
func buildDockerFilters(filters []string) map[string][]string {
	m := make(map[string][]string)
	for _, f := range filters {
		idx := strings.IndexByte(f, '=')
		if idx < 0 {
			continue
		}
		key := f[:idx]
		val := f[idx+1:]
		m[key] = append(m[key], val)
	}
	return m
}

// parseDockerContainerList decodes the raw JSON from /containers/json.
func parseDockerContainerList(body []byte) ([]DockerContainerInfo, error) {
	var raw []struct {
		ID              string            `json:"Id"`
		Names           []string          `json:"Names"`
		Image           string            `json:"Image"`
		State           string            `json:"State"`
		Status          string            `json:"Status"`
		Labels          map[string]string `json:"Labels"`
		NetworkSettings struct {
			Networks map[string]json.RawMessage `json:"Networks"`
		} `json:"NetworkSettings"`
		Ports []struct {
			IP          string `json:"IP"`
			PrivatePort uint16 `json:"PrivatePort"`
			PublicPort  uint16 `json:"PublicPort"`
			Type        string `json:"Type"`
		} `json:"Ports"`
	}

	if err := json.Unmarshal(body, &raw); err != nil {
		return nil, fmt.Errorf("docker_container_list: parse response: %w", err)
	}

	result := make([]DockerContainerInfo, 0, len(raw))
	for _, c := range raw {
		id := c.ID
		if len(id) > 12 {
			id = id[:12]
		}

		ports := make([]string, 0, len(c.Ports))
		for _, p := range c.Ports {
			if p.IP != "" && p.PublicPort != 0 {
				ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", p.IP, p.PublicPort, p.PrivatePort, p.Type))
			} else if p.PrivatePort != 0 {
				ports = append(ports, fmt.Sprintf("%d/%s", p.PrivatePort, p.Type))
			}
		}

		networks := make([]string, 0, len(c.NetworkSettings.Networks))
		for name := range c.NetworkSettings.Networks {
			networks = append(networks, name)
		}

		labels := c.Labels
		if labels == nil {
			labels = map[string]string{}
		}

		result = append(result, DockerContainerInfo{
			ID:       id,
			Names:    c.Names,
			Image:    c.Image,
			State:    c.State,
			Status:   c.Status,
			Ports:    ports,
			Networks: networks,
			Labels:   labels,
		})
	}
	return result, nil
}
