package docker

import (
	"context"
	"fmt"
	"net"
	"os/exec"
	"regexp"
	"strings"
	"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/api/types/network"
	"github.com/docker/docker/client"

	"firewall_containers/network-go/config"
)

// DockerAPI defines the interface for Docker operations, enabling mock implementations for testing
type DockerAPI interface {
	Close() error
	EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error
	RemoveNetwork(ctx context.Context, networkName string) error
	ConnectContainer(ctx context.Context, containerName, networkName, ip string) error
	DisconnectContainer(ctx context.Context, containerName, networkName string) error
	InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error)
	WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error
	GetContainerPID(ctx context.Context, containerName string) (int, error)
	AddRouteInContainer(ctx context.Context, containerName, network, gateway string) error
	FindContainerName(ctx context.Context, name, selector string) (string, error)
}

// Client wraps the Docker SDK client
type Client struct {
	cli *client.Client
}

// Ensure Client implements DockerAPI
var _ DockerAPI = (*Client)(nil)

// NewClient creates a new Docker client
func NewClient() (*Client, error) {
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return nil, fmt.Errorf("failed to create Docker client: %w", err)
	}
	return &Client{cli: cli}, nil
}

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

// EnsureNetwork creates a Docker network if it does not already exist
func (c *Client) EnsureNetwork(ctx context.Context, netCfg config.NetworkConfig) error {
	existingNetworks, err := c.cli.NetworkList(ctx, network.ListOptions{
		Filters: filters.NewArgs(filters.Arg("name", netCfg.NetworkName)),
	})
	if err != nil {
		return fmt.Errorf("failed to list networks: %w", err)
	}

	for _, n := range existingNetworks {
		if n.Name == netCfg.NetworkName {
			return nil
		}
	}

	_, ipNet, err := net.ParseCIDR(netCfg.Subnet)
	if err != nil {
		return fmt.Errorf("failed to parse subnet %s: %w", netCfg.Subnet, err)
	}

	gatewayIP := net.ParseIP(netCfg.Gateway)
	if gatewayIP == nil {
		return fmt.Errorf("failed to parse gateway IP %s", netCfg.Gateway)
	}

	createOpts := network.CreateOptions{
		Driver: "bridge",
		IPAM: &network.IPAM{
			Config: []network.IPAMConfig{
				{
					Subnet:  ipNet.String(),
					Gateway: gatewayIP.String(),
				},
			},
		},
		Labels: map[string]string{
			"managed-by": "firewall-network-go",
		},
		Attachable: true,
	}

	resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts)
	if err != nil {
		return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err)
	}

	_ = resp
	return nil
}

// RemoveNetwork removes a Docker network
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
	err := c.cli.NetworkRemove(ctx, networkName)
	if err != nil {
		return fmt.Errorf("failed to remove network %s: %w", networkName, err)
	}
	return nil
}

// ConnectContainer connects a container to a network with a specific IP
func (c *Client) ConnectContainer(ctx context.Context, containerName, networkName, ip string) error {
	endpointSettings := &network.EndpointSettings{
		IPAMConfig: &network.EndpointIPAMConfig{
			IPv4Address: ip,
		},
	}

	err := c.cli.NetworkConnect(ctx, networkName, containerName, endpointSettings)
	if err != nil {
		// "endpoint with name ... already exists" is expected on re-reconciliation
		if strings.Contains(err.Error(), "already exists") {
			return nil
		}
		return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err)
	}
	return nil
}

// DisconnectContainer disconnects a container from a network
func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error {
	err := c.cli.NetworkDisconnect(ctx, networkName, containerName, true)
	if err != nil {
		return fmt.Errorf("failed to disconnect container %s from network %s: %w", containerName, networkName, err)
	}
	return nil
}

// InspectContainer returns the container's details
func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) {
	container, err := c.cli.ContainerInspect(ctx, containerName)
	if err != nil {
		return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
	}
	return &container, nil
}

// WaitForContainerRunning waits until a container is in running state, with a timeout
func (c *Client) WaitForContainerRunning(ctx context.Context, containerName string, timeout time.Duration) error {
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	ticker := time.NewTicker(500 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return fmt.Errorf("timeout waiting for container %s to be running", containerName)
		case <-ticker.C:
			container, err := c.cli.ContainerInspect(ctx, containerName)
			if err != nil {
				continue
			}
			if container.State != nil && container.State.Running {
				return nil
			}
		}
	}
}

// GetContainerPID returns the PID of a container for nsenter operations
func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) {
	cont, err := c.cli.ContainerInspect(ctx, containerName)
	if err != nil {
		return 0, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
	}
	if cont.State == nil || !cont.State.Running {
		return 0, fmt.Errorf("container %s is not running", containerName)
	}
	return cont.State.Pid, nil
}

// AddRouteInContainer adds a route inside a container's network namespace using nsenter
func (c *Client) AddRouteInContainer(ctx context.Context, containerName, network, gateway string) error {
	pid, err := c.GetContainerPID(ctx, containerName)
	if err != nil {
		return fmt.Errorf("failed to get PID for container %s: %w", containerName, err)
	}

	args := []string{
		"-t", fmt.Sprintf("%d", pid),
		"-n", "--",
		"ip", "route", "add", network, "via", gateway,
	}

	cmd := exec.Command("nsenter", args...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to add route in container %s: %w\noutput: %s", containerName, err, string(output))
	}
	return nil
}

// FindContainerName attempts to find a running container by name or selector.
// First tries exact name match, then exact selector, then prefix matching
// (matching the old shell script's grep $D"-" behavior).
func (c *Client) FindContainerName(ctx context.Context, name, selector string) (string, error) {
	// Try exact name match using ContainerList with a name filter
	// Docker API name filter does an exact match by default
	containers, err := c.cli.ContainerList(ctx, container.ListOptions{
		Filters: filters.NewArgs(
			filters.Arg("name", "^/?"+regexp.QuoteMeta(name)+"$"),
			filters.Arg("status", "running"),
		),
	})
	if err == nil && len(containers) > 0 {
		cName := strings.TrimPrefix(containers[0].Names[0], "/")
		return cName, nil
	}

	// Try exact selector match
	if selector != "" && selector != name {
		containers, err = c.cli.ContainerList(ctx, container.ListOptions{
			Filters: filters.NewArgs(
				filters.Arg("name", "^/?"+regexp.QuoteMeta(selector)+"$"),
				filters.Arg("status", "running"),
			),
		})
		if err == nil && len(containers) > 0 {
			cName := strings.TrimPrefix(containers[0].Names[0], "/")
			return cName, nil
		}
	}

	// Try prefix matching on both name and selector (shell script behavior: grep $D"-")
	candidates := []string{name, selector}
	for _, candidate := range candidates {
		if candidate == "" {
			continue
		}
		// Extract prefix before first dash if present
		prefix := candidate
		if strings.Contains(candidate, "-") {
			prefix = candidate[:strings.Index(candidate, "-")]
		}

		containers, err = c.cli.ContainerList(ctx, container.ListOptions{
			Filters: filters.NewArgs(
				filters.Arg("name", prefix+"-"),
				filters.Arg("status", "running"),
			),
		})
		if err != nil {
			continue
		}

		for _, c := range containers {
			for _, cName := range c.Names {
				cName = strings.TrimPrefix(cName, "/")
				return cName, nil
			}
		}
	}

	return "", fmt.Errorf("no running container found matching name=%q selector=%q", name, selector)
}
