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"
	"firewall_containers/network-go/logger"
)

// 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)
	IsConnected(ctx context.Context, containerName, networkName, expectedIP string) bool
}

// 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) {
	logger.Info("DOCKER: creating Docker client (using DOCKER_HOST env)")
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		return nil, fmt.Errorf("failed to create Docker client: %w", err)
	}
	ping, err := cli.Ping(context.Background())
	if err != nil {
		logger.Warn("DOCKER: Docker daemon ping failed: %v", err)
	} else {
		logger.Info("DOCKER: connected to Docker daemon (API version=%s, OS=%s)", ping.APIVersion, ping.OSType)
	}
	return &Client{cli: cli}, nil
}

// Close closes the Docker client
func (c *Client) Close() error {
	logger.Debug("DOCKER: closing Docker client")
	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 {
	logger.Debug("DOCKER: checking if network %q exists", netCfg.NetworkName)
	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 {
			logger.Debug("DOCKER: network %q already exists", 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,
	}

	logger.Info("DOCKER: creating network %q (subnet=%s, gateway=%s)", netCfg.NetworkName, netCfg.Subnet, netCfg.Gateway)
	resp, err := c.cli.NetworkCreate(ctx, netCfg.NetworkName, createOpts)
	if err != nil {
		return fmt.Errorf("failed to create network %s: %w", netCfg.NetworkName, err)
	}

	logger.Info("DOCKER: network %q created (ID=%s)", netCfg.NetworkName, resp.ID)
	return nil
}

// RemoveNetwork removes a Docker network
func (c *Client) RemoveNetwork(ctx context.Context, networkName string) error {
	logger.Info("DOCKER: removing network %q", networkName)
	err := c.cli.NetworkRemove(ctx, networkName)
	if err != nil {
		return fmt.Errorf("failed to remove network %s: %w", networkName, err)
	}
	logger.Info("DOCKER: network %q removed", networkName)
	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 {
	logger.Info("DOCKER: connecting container %q to network %q with IP %s", containerName, networkName, ip)
	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") {
			logger.Info("DOCKER: container %q already connected to network %q", containerName, networkName)
			return nil
		}
		return fmt.Errorf("failed to connect container %s to network %s: %w", containerName, networkName, err)
	}
	logger.Info("DOCKER: container %q connected to network %q with IP %s", containerName, networkName, ip)
	return nil
}

// DisconnectContainer disconnects a container from a network
func (c *Client) DisconnectContainer(ctx context.Context, containerName, networkName string) error {
	logger.Info("DOCKER: disconnecting container %q from network %q", containerName, networkName)
	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)
	}
	logger.Info("DOCKER: container %q disconnected from network %q", containerName, networkName)
	return nil
}

// InspectContainer returns the container's details
func (c *Client) InspectContainer(ctx context.Context, containerName string) (*types.ContainerJSON, error) {
	logger.Debug("DOCKER: inspecting container %q", containerName)
	container, err := c.cli.ContainerInspect(ctx, containerName)
	if err != nil {
		return nil, fmt.Errorf("failed to inspect container %s: %w", containerName, err)
	}
	logger.Debug("DOCKER: container %q inspected (status=%s, PID=%d)",
		containerName, container.State.Status, container.State.Pid)
	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 {
	logger.Info("DOCKER: waiting for container %q to be running (timeout=%s)", containerName, timeout)
	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 {
				logger.Debug("DOCKER: container %q inspect failed (not ready yet): %v", containerName, err)
				continue
			}
			if container.State != nil && container.State.Running {
				logger.Info("DOCKER: container %q is now running", containerName)
				return nil
			}
			logger.Debug("DOCKER: container %q status=%s, not running yet", containerName, container.State.Status)
		}
	}
}

// GetContainerPID returns the PID of a container for nsenter operations
func (c *Client) GetContainerPID(ctx context.Context, containerName string) (int, error) {
	logger.Info("DOCKER: getting PID for container %q", containerName)
	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)
	}
	logger.Info("DOCKER: container %q PID=%d", containerName, cont.State.Pid)
	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)
	}

	logger.Info("DOCKER: adding route in container %q (PID=%d): %s via %s", containerName, pid, network, gateway)
	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))
	}
	logger.Info("DOCKER: route added in container %q: %s via %s", containerName, network, gateway)
	logger.Debug("DOCKER: nsenter output: %s", strings.TrimSpace(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) {
	logger.Info("DOCKER: finding container: name=%q selector=%q", name, selector)

	// 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], "/")
		logger.Info("DOCKER: found container by exact name match: %q", cName)
		return cName, nil
	}

	// Try exact selector match
	if selector != "" && selector != name {
		logger.Debug("DOCKER: trying selector match: %q", selector)
		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], "/")
			logger.Info("DOCKER: found container by selector match: %q", cName)
			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, "-")]
		}
		logger.Debug("DOCKER: trying prefix match: candidate=%q prefix=%q", candidate, prefix)

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

		for _, c := range containers {
			for _, cName := range c.Names {
				cName = strings.TrimPrefix(cName, "/")
				logger.Info("DOCKER: found container by prefix match %q-: %q", prefix, cName)
				return cName, nil
			}
		}
	}

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

// IsConnected checks if a container is already connected to a network with the expected IP.
// Returns true if the connection already exists with the correct IP (stateful check).
func (c *Client) IsConnected(ctx context.Context, containerName, networkName, expectedIP string) bool {
	logger.Debug("DOCKER: checking if container %q is connected to %q with IP %s", containerName, networkName, expectedIP)

	containers, err := c.cli.ContainerList(ctx, container.ListOptions{
		Filters: filters.NewArgs(
			filters.Arg("name", "^/?"+regexp.QuoteMeta(containerName)+"$"),
			filters.Arg("status", "running"),
		),
	})
	if err != nil || len(containers) == 0 {
		logger.Debug("DOCKER: container %q not found or error: %v", containerName, err)
		return false
	}

	// Check the network settings for the expected IP
	inspect, err := c.cli.ContainerInspect(ctx, containerName)
	if err != nil {
		logger.Debug("DOCKER: inspect failed for %q: %v", containerName, err)
		return false
	}

	if inspect.NetworkSettings != nil && inspect.NetworkSettings.Networks != nil {
		if netSettings, ok := inspect.NetworkSettings.Networks[networkName]; ok {
			currentIP := netSettings.IPAddress
			logger.Debug("DOCKER: container %q on network %q has IP=%s (expected=%s)", containerName, networkName, currentIP, expectedIP)
			if currentIP == expectedIP {
				logger.Debug("DOCKER: container %q already correctly connected to %q with IP %s", containerName, networkName, expectedIP)
				return true
			}
		}
	}

	logger.Debug("DOCKER: container %q not yet correctly connected to %q", containerName, networkName)
	return false
}
