package services

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/url"
	"os"
	"slices"
	"strconv"
	"strings"
	"syscall"

	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
)

type DockerUnavailableError struct {
	err     error
	details string
}

func NewDockerUnavailableError(err error, details ...string) *DockerUnavailableError {
	detailMsg := ""
	if len(details) > 0 {
		detailMsg = details[0]
	}

	return &DockerUnavailableError{err: err, details: detailMsg}
}

func (e *DockerUnavailableError) Error() string {
	if e == nil || e.err == nil {
		return "docker unavailable"
	}
	return fmt.Sprintf("docker unavailable: %v", e.err)
}

func (e *DockerUnavailableError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.err
}

func (e *DockerUnavailableError) Details() string {
	if e == nil {
		return ""
	}
	return e.details
}

type DockerPort struct {
	PrivatePort uint16 `json:"private_port"`
	PublicPort  uint16 `json:"public_port"`
	Type        string `json:"type"`
}

type DockerContainer struct {
	ID      string       `json:"id"`
	Names   []string     `json:"names"`
	Image   string       `json:"image"`
	State   string       `json:"state"`
	Status  string       `json:"status"`
	Network string       `json:"network"`
	IP      string       `json:"ip"`
	Ports   []DockerPort `json:"ports"`
}

type DockerService struct {
	client    *client.Client
	initErr   error // Stores initialization error if Docker is unavailable
	localHost string
}

// NewDockerService creates a new Docker service instance.
// If Docker client initialization fails, it returns a stub service that will return
// DockerUnavailableError for all operations. This allows routes to be registered
// and provide helpful error messages to users.
func NewDockerService() *DockerService {
	envHost := strings.TrimSpace(os.Getenv("DOCKER_HOST"))
	localHost := resolveLocalDockerHost()
	if envHost != "" && !strings.HasPrefix(envHost, "unix://") {
		logger.Log().WithFields(map[string]any{"docker_host_env": envHost, "local_host": localHost}).Info("ignoring non-unix DOCKER_HOST for local docker mode")
	}

	cli, err := client.NewClientWithOpts(client.WithHost(localHost), client.WithAPIVersionNegotiation())
	if err != nil {
		logger.Log().WithError(err).Warn("Failed to initialize Docker client - Docker features will be unavailable")
		unavailableErr := NewDockerUnavailableError(err, buildLocalDockerUnavailableDetails(err, localHost))
		return &DockerService{
			client:    nil,
			initErr:   unavailableErr,
			localHost: localHost,
		}
	}
	return &DockerService{client: cli, initErr: nil, localHost: localHost}
}

func (s *DockerService) ListContainers(ctx context.Context, host string) ([]DockerContainer, error) {
	// Check if Docker was available during initialization
	if s.initErr != nil {
		var unavailableErr *DockerUnavailableError
		if errors.As(s.initErr, &unavailableErr) {
			return nil, unavailableErr
		}
		return nil, NewDockerUnavailableError(s.initErr, buildLocalDockerUnavailableDetails(s.initErr, s.localHost))
	}

	var cli *client.Client
	var err error

	if host == "" || host == "local" {
		cli = s.client
	} else {
		cli, err = client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation())
		if err != nil {
			return nil, fmt.Errorf("failed to create remote client: %w", err)
		}
		defer func() {
			if closeErr := cli.Close(); closeErr != nil {
				logger.Log().WithError(closeErr).Warn("failed to close docker client")
			}
		}()
	}

	containers, err := cli.ContainerList(ctx, container.ListOptions{All: false})
	if err != nil {
		if isDockerConnectivityError(err) {
			if host == "" || host == "local" {
				return nil, NewDockerUnavailableError(err, buildLocalDockerUnavailableDetails(err, s.localHost))
			}
			return nil, NewDockerUnavailableError(err)
		}
		return nil, fmt.Errorf("failed to list containers: %w", err)
	}

	var result []DockerContainer
	for _, c := range containers {
		// Get the first network's IP address if available
		networkName := ""
		ipAddress := ""
		if c.NetworkSettings != nil && len(c.NetworkSettings.Networks) > 0 {
			for name, net := range c.NetworkSettings.Networks {
				networkName = name
				ipAddress = net.IPAddress
				break // Just take the first one for now
			}
		}

		// Clean up names (remove leading slash)
		names := make([]string, len(c.Names))
		for i, name := range c.Names {
			names[i] = strings.TrimPrefix(name, "/")
		}

		// Map ports
		var ports []DockerPort
		for _, p := range c.Ports {
			ports = append(ports, DockerPort{
				PrivatePort: p.PrivatePort,
				PublicPort:  p.PublicPort,
				Type:        p.Type,
			})
		}

		result = append(result, DockerContainer{
			ID:      c.ID[:12], // Short ID
			Names:   names,
			Image:   c.Image,
			State:   c.State,
			Status:  c.Status,
			Network: networkName,
			IP:      ipAddress,
			Ports:   ports,
		})
	}

	return result, nil
}

func isDockerConnectivityError(err error) bool {
	if err == nil {
		return false
	}

	// Common high-signal strings from docker client/daemon failures.
	msg := strings.ToLower(err.Error())
	if strings.Contains(msg, "cannot connect to the docker daemon") ||
		strings.Contains(msg, "is the docker daemon running") ||
		strings.Contains(msg, "error during connect") {
		return true
	}

	// Context timeouts typically indicate the daemon/socket is unreachable.
	if errors.Is(err, context.DeadlineExceeded) {
		return true
	}

	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		err = urlErr.Unwrap()
	}

	var netErr net.Error
	if errors.As(err, &netErr) {
		if netErr.Timeout() {
			return true
		}
	}

	// Walk common syscall error wrappers.
	var syscallErr *os.SyscallError
	if errors.As(err, &syscallErr) {
		err = syscallErr.Unwrap()
	}

	var opErr *net.OpError
	if errors.As(err, &opErr) {
		err = opErr.Unwrap()
	}

	var errno syscall.Errno
	if errors.As(err, &errno) {
		switch errno {
		case syscall.ENOENT, syscall.EACCES, syscall.EPERM, syscall.ECONNREFUSED:
			return true
		}
	}

	// os.ErrNotExist covers missing unix socket paths.
	if errors.Is(err, os.ErrNotExist) {
		return true
	}

	return false
}

func resolveLocalDockerHost() string {
	envHost := strings.TrimSpace(os.Getenv("DOCKER_HOST"))
	if strings.HasPrefix(envHost, "unix://") {
		socketPath := socketPathFromDockerHost(envHost)
		if socketPath != "" {
			if _, err := os.Stat(socketPath); err == nil {
				return envHost
			}
		}
	}

	defaultSocketPath := "/var/run/docker.sock"
	if _, err := os.Stat(defaultSocketPath); err == nil {
		return "unix:///var/run/docker.sock"
	}

	rootlessSocketPath := fmt.Sprintf("/run/user/%d/docker.sock", os.Getuid())
	if _, err := os.Stat(rootlessSocketPath); err == nil {
		return "unix://" + rootlessSocketPath
	}

	return "unix:///var/run/docker.sock"
}

func socketPathFromDockerHost(host string) string {
	trimmedHost := strings.TrimSpace(host)
	if !strings.HasPrefix(trimmedHost, "unix://") {
		return ""
	}
	return strings.TrimPrefix(trimmedHost, "unix://")
}

func buildLocalDockerUnavailableDetails(err error, localHost string) string {
	socketPath := socketPathFromDockerHost(localHost)
	if socketPath == "" {
		socketPath = "/var/run/docker.sock"
	}

	uid := os.Getuid()
	gid := os.Getgid()
	groups, _ := os.Getgroups()
	groupsStr := ""
	if len(groups) > 0 {
		groupValues := make([]string, 0, len(groups))
		for _, groupID := range groups {
			groupValues = append(groupValues, strconv.Itoa(groupID))
		}
		groupsStr = strings.Join(groupValues, ",")
	}

	if errno, ok := extractErrno(err); ok {
		switch errno {
		case syscall.ENOENT:
			return fmt.Sprintf("Local Docker socket not found at %s (local host selector uses %s). Mount %s as read-only or read-write.", socketPath, localHost, socketPath)
		case syscall.ECONNREFUSED:
			return fmt.Sprintf("Docker daemon is not accepting connections at %s.", socketPath)
		case syscall.EACCES, syscall.EPERM:
			infoMsg, socketGID := localSocketStatSummary(socketPath)
			permissionHint := ""
			if socketGID >= 0 && !slices.Contains(groups, socketGID) {
				permissionHint = fmt.Sprintf(" Process groups (%s) do not include socket gid %d; run container with matching supplemental group (e.g., --group-add %d or compose group_add: [\"%d\"]).", groupsStr, socketGID, socketGID, socketGID)
			}
			return fmt.Sprintf("Local Docker socket is mounted but not accessible by current process (uid=%d gid=%d). %s%s", uid, gid, infoMsg, permissionHint)
		}
	}

	if errors.Is(err, os.ErrNotExist) {
		return fmt.Sprintf("Local Docker socket not found at %s (local host selector uses %s).", socketPath, localHost)
	}

	return fmt.Sprintf("Cannot connect to local Docker via %s. Ensure Docker is running and the mounted socket permissions allow uid=%d gid=%d access.", localHost, uid, gid)
}

func extractErrno(err error) (syscall.Errno, bool) {
	if err == nil {
		return 0, false
	}

	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		err = urlErr.Unwrap()
	}

	var syscallErr *os.SyscallError
	if errors.As(err, &syscallErr) {
		err = syscallErr.Unwrap()
	}

	var opErr *net.OpError
	if errors.As(err, &opErr) {
		err = opErr.Unwrap()
	}

	var errno syscall.Errno
	if errors.As(err, &errno) {
		return errno, true
	}

	return 0, false
}

func localSocketStatSummary(socketPath string) (string, int) {
	info, statErr := os.Stat(socketPath)
	if statErr != nil {
		return fmt.Sprintf("Socket path %s could not be stat'ed: %v.", socketPath, statErr), -1
	}

	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok || stat == nil {
		return fmt.Sprintf("Socket path %s has mode %s.", socketPath, info.Mode().String()), -1
	}

	return fmt.Sprintf("Socket path %s has mode %s owner uid=%d gid=%d.", socketPath, info.Mode().String(), stat.Uid, stat.Gid), int(stat.Gid)
}
