package sockets

import (
	"bufio"
	"bytes"
	"fmt"
	"net"
	"path/filepath"
	"time"

	"github.com/containers/podman/v5/pkg/machine/define"
	"github.com/containers/storage/pkg/fileutils"
	"github.com/sirupsen/logrus"
)

// SetSocket creates a new machine file for the socket and assigns it to
// `socketLoc`
func SetSocket(socketLoc *define.VMFile, path string, symlink *string) error {
	socket, err := define.NewMachineFile(path, symlink)
	if err != nil {
		return err
	}
	*socketLoc = *socket
	return nil
}

// ReadySocketPath returns the filepath for the ready socket
func ReadySocketPath(runtimeDir, machineName string) string {
	return filepath.Join(runtimeDir, fmt.Sprintf("%s_ready.sock", machineName))
}

// ListenAndWaitOnSocket waits for a new connection to the listener and sends
// any error back through the channel. ListenAndWaitOnSocket is intended to be
// used as a goroutine
func ListenAndWaitOnSocket(errChan chan<- error, listener net.Listener) {
	conn, err := listener.Accept()
	if err != nil {
		logrus.Debug("failed to connect to ready socket")
		errChan <- err
		return
	}
	_, err = bufio.NewReader(conn).ReadString('\n')
	logrus.Debug("ready ack received")

	if closeErr := conn.Close(); closeErr != nil {
		errChan <- closeErr
		return
	}

	errChan <- err
}

// DialSocketWithBackoffs attempts to connect to the socket in maxBackoffs attempts
func DialSocketWithBackoffs(maxBackoffs int, backoff time.Duration, socketPath string) (conn net.Conn, err error) {
	for i := 0; i < maxBackoffs; i++ {
		if i > 0 {
			time.Sleep(backoff)
			backoff *= 2
		}
		conn, err = net.Dial("unix", socketPath)
		if err == nil {
			return conn, nil
		}
	}
	return nil, err
}

// DialSocketWithBackoffsAndProcCheck attempts to connect to the socket in
// maxBackoffs attempts. After every failure to connect, it makes sure the
// specified process is alive
func DialSocketWithBackoffsAndProcCheck(
	maxBackoffs int,
	backoff time.Duration,
	socketPath string,
	checkProccessStatus func(string, int, *bytes.Buffer) error,
	procHint string,
	procPid int,
	errBuf *bytes.Buffer,
) (conn net.Conn, err error) {
	for i := 0; i < maxBackoffs; i++ {
		if i > 0 {
			time.Sleep(backoff)
			backoff *= 2
		}
		conn, err = net.Dial("unix", socketPath)
		if err == nil {
			return conn, nil
		}

		// check to make sure process denoted by procHint is alive
		err = checkProccessStatus(procHint, procPid, errBuf)
		if err != nil {
			return nil, err
		}
	}
	return nil, err
}

// WaitForSocketWithBackoffs attempts to discover listening socket in maxBackoffs attempts
func WaitForSocketWithBackoffs(maxBackoffs int, backoff time.Duration, socketPath string, name string) error {
	backoffWait := backoff
	logrus.Debugf("checking that %q socket is ready", name)
	for i := 0; i < maxBackoffs; i++ {
		err := fileutils.Exists(socketPath)
		if err == nil {
			return nil
		}
		time.Sleep(backoffWait)
		backoffWait *= 2
	}
	return fmt.Errorf("unable to connect to %q socket at %q", name, socketPath)
}
