package config

import (
	"errors"
	"fmt"
	"net"
	"net/netip"
	"strconv"
	"strings"

	"github.com/psviderski/uncloud/internal/secret"
)

type MachineConnection struct {
	// SSH uses the system ssh CLI command to connect. This is the default SSH connection method.
	SSH SSHDestination `yaml:"ssh,omitempty"`
	// SSHCLI is a backward-compatible alias for SSH.
	SSHCLI SSHDestination `yaml:"ssh_cli,omitempty"`
	// SSHGo uses Go's built-in SSH library to connect.
	SSHGo      SSHDestination `yaml:"ssh_go,omitempty"`
	SSHKeyFile string         `yaml:"ssh_key_file,omitempty"`
	// TCP is the address and port of the machine's API server.
	// The pointer is used to omit the field when not set. Otherwise, yaml marshalling includes an empty object.
	TCP *netip.AddrPort `yaml:"tcp,omitempty"`
	// Unix is the path to the machine's API unix socket.
	Unix      string        `yaml:"unix,omitempty"`
	Host      string        `yaml:"host,omitempty"`
	PublicKey secret.Secret `yaml:"public_key,omitempty"`
	MachineID string        `yaml:"machine_id,omitempty"`
}

func (c *MachineConnection) String() string {
	if c.SSH != "" {
		return "ssh://" + string(c.SSH)
	} else if c.SSHCLI != "" {
		return "ssh://" + string(c.SSHCLI)
	} else if c.SSHGo != "" {
		return "ssh+go://" + string(c.SSHGo)
	} else if c.TCP != nil && c.TCP.IsValid() {
		return fmt.Sprintf("tcp://%s", c.TCP)
	} else if c.Unix != "" {
		return fmt.Sprintf("unix://%s", c.Unix)
	}
	return "unknown connection"
}

func (c *MachineConnection) Validate() error {
	setCount := 0
	if c.SSH != "" {
		setCount++
	}
	if c.SSHCLI != "" {
		setCount++
	}
	if c.SSHGo != "" {
		setCount++
	}
	if c.TCP != nil && c.TCP.IsValid() {
		setCount++
	}
	if c.Unix != "" {
		setCount++
	}

	if setCount == 0 {
		return errors.New("no connection method specified (ssh, ssh_go, tcp, or unix required)")
	}
	if setCount > 1 {
		return errors.New("only one connection method allowed per connection (ssh, ssh_go, tcp, or unix)")
	}

	return nil
}

// SSHDestination represents an SSH destination string in the canonical form of "user@host:port".
// Empty user or port components are omitted.
type SSHDestination string

// NewSSHDestination constructs an SSHDestination from user, host, and port components.
// If user is empty, it is omitted.
// If port is 0, it is omitted.
func NewSSHDestination(user, host string, port int) SSHDestination {
	dst := host
	if port != 0 {
		dst = net.JoinHostPort(host, strconv.Itoa(port))
	}
	if user != "" {
		dst = fmt.Sprintf("%s@%s", user, dst)
	}

	return SSHDestination(dst)
}

// Parse parses the SSH destination string into user, host, and port components.
// If user is not specified, it returns an empty string.
// If port is not specified, it returns 0.
func (d SSHDestination) Parse() (user string, host string, port int, err error) {
	host = string(d)
	if strings.Contains(host, "@") {
		user, host, _ = strings.Cut(host, "@")
	}
	h, p, sErr := net.SplitHostPort(host)
	if sErr == nil {
		host = h
		port, err = strconv.Atoi(p)
	}

	return
}
