//go:build amd64 || arm64
// +build amd64 arm64

package machine

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/containers/podman/v4/pkg/machine/compression"
	"github.com/containers/podman/v4/pkg/machine/define"
	"github.com/containers/storage/pkg/homedir"
	"github.com/containers/storage/pkg/lockfile"
	"github.com/sirupsen/logrus"
)

type InitOptions struct {
	CPUS               uint64
	DiskSize           uint64
	IgnitionPath       string
	ImagePath          string
	Volumes            []string
	VolumeDriver       string
	IsDefault          bool
	Memory             uint64
	Name               string
	TimeZone           string
	URI                url.URL
	Username           string
	ReExec             bool
	Rootful            bool
	UID                string // uid of the user that called machine
	UserModeNetworking *bool  // nil = use backend/system default, false = disable, true = enable
	USBs               []string
}

type Status = string

const (
	// Running indicates the qemu vm is running.
	Running Status = "running"
	// Stopped indicates the vm has stopped.
	Stopped Status = "stopped"
	// Starting indicated the vm is in the process of starting
	Starting Status = "starting"
	// Unknown means the state is not known
	Unknown            Status = "unknown"
	DefaultMachineName string = "podman-machine-default"
	apiUpTimeout              = 20 * time.Second
)

type RemoteConnectionType string

var (
	SSHRemoteConnection     RemoteConnectionType = "ssh"
	DefaultIgnitionUserName                      = "core"
	ForwarderBinaryName                          = "gvproxy"
)

type Download struct {
	Arch                  string
	Artifact              define.Artifact
	CacheDir              string
	CompressionType       compression.ImageCompression
	DataDir               string
	Format                define.ImageFormat
	ImageName             string
	LocalPath             string
	LocalUncompressedFile string
	Sha256sum             string
	Size                  int64
	URL                   *url.URL
	VMKind                VMType
	VMName                string
}

type ListOptions struct{}

type ListResponse struct {
	Name               string
	CreatedAt          time.Time
	LastUp             time.Time
	Running            bool
	Starting           bool
	Stream             string
	VMType             string
	CPUs               uint64
	Memory             uint64
	DiskSize           uint64
	Port               int
	RemoteUsername     string
	IdentityPath       string
	UserModeNetworking bool
}

type SetOptions struct {
	CPUs               *uint64
	DiskSize           *uint64
	Memory             *uint64
	Rootful            *bool
	UserModeNetworking *bool
	USBs               *[]string
}

type SSHOptions struct {
	Username string
	Args     []string
}

type StartOptions struct {
	NoInfo bool
	Quiet  bool
}

type StopOptions struct{}

type RemoveOptions struct {
	Force        bool
	SaveKeys     bool
	SaveImage    bool
	SaveIgnition bool
}

type InspectOptions struct{}

type VM interface {
	Init(opts InitOptions) (bool, error)
	Inspect() (*InspectInfo, error)
	Remove(name string, opts RemoveOptions) (string, func() error, error)
	Set(name string, opts SetOptions) ([]error, error)
	SSH(name string, opts SSHOptions) error
	Start(name string, opts StartOptions) error
	State(bypass bool) (Status, error)
	Stop(name string, opts StopOptions) error
}

func GetLock(name string, vmtype VMType) (*lockfile.LockFile, error) {
	// FIXME: there's a painful amount of `GetConfDir` calls scattered
	// across the code base.  This should be done once and stored
	// somewhere instead.
	vmConfigDir, err := GetConfDir(vmtype)
	if err != nil {
		return nil, err
	}

	lockPath := filepath.Join(vmConfigDir, name+".lock")
	lock, err := lockfile.GetLockFile(lockPath)
	if err != nil {
		return nil, fmt.Errorf("creating lockfile for VM: %w", err)
	}

	return lock, nil
}

type DistributionDownload interface {
	HasUsableCache() (bool, error)
	Get() *Download
	CleanCache() error
}
type InspectInfo struct {
	ConfigPath         define.VMFile
	ConnectionInfo     ConnectionConfig
	Created            time.Time
	Image              ImageConfig
	LastUp             time.Time
	Name               string
	Resources          ResourceConfig
	SSHConfig          SSHConfig
	State              Status
	UserModeNetworking bool
	Rootful            bool
}

func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL {
	// TODO Should this function have input verification?
	userInfo := url.User(userName)
	uri := url.URL{
		Scheme:     "ssh",
		Opaque:     "",
		User:       userInfo,
		Host:       host,
		Path:       path,
		RawPath:    "",
		ForceQuery: false,
		RawQuery:   "",
		Fragment:   "",
	}
	if len(port) > 0 {
		uri.Host = net.JoinHostPort(uri.Hostname(), port)
	}
	return uri
}

// GetCacheDir returns the dir where VM images are downloaded into when pulled
func GetCacheDir(vmType VMType) (string, error) {
	dataDir, err := GetDataDir(vmType)
	if err != nil {
		return "", err
	}
	cacheDir := filepath.Join(dataDir, "cache")
	if _, err := os.Stat(cacheDir); !errors.Is(err, os.ErrNotExist) {
		return cacheDir, nil
	}
	return cacheDir, os.MkdirAll(cacheDir, 0755)
}

// GetDataDir returns the filepath where vm images should
// live for podman-machine.
func GetDataDir(vmType VMType) (string, error) {
	dataDirPrefix, err := DataDirPrefix()
	if err != nil {
		return "", err
	}
	dataDir := filepath.Join(dataDirPrefix, vmType.String())
	if _, err := os.Stat(dataDir); !errors.Is(err, os.ErrNotExist) {
		return dataDir, nil
	}
	mkdirErr := os.MkdirAll(dataDir, 0755)
	return dataDir, mkdirErr
}

// GetGLobalDataDir returns the root of all backends
// for shared machine data.
func GetGlobalDataDir() (string, error) {
	dataDir, err := DataDirPrefix()
	if err != nil {
		return "", err
	}

	return dataDir, os.MkdirAll(dataDir, 0755)
}

// DataDirPrefix returns the path prefix for all machine data files
func DataDirPrefix() (string, error) {
	data, err := homedir.GetDataHome()
	if err != nil {
		return "", err
	}
	dataDir := filepath.Join(data, "containers", "podman", "machine")
	return dataDir, nil
}

// GetConfigDir returns the filepath to where configuration
// files for podman-machine should live
func GetConfDir(vmType VMType) (string, error) {
	confDirPrefix, err := ConfDirPrefix()
	if err != nil {
		return "", err
	}
	confDir := filepath.Join(confDirPrefix, vmType.String())
	if _, err := os.Stat(confDir); !errors.Is(err, os.ErrNotExist) {
		return confDir, nil
	}
	mkdirErr := os.MkdirAll(confDir, 0755)
	return confDir, mkdirErr
}

// ConfDirPrefix returns the path prefix for all machine config files
func ConfDirPrefix() (string, error) {
	conf, err := homedir.GetConfigHome()
	if err != nil {
		return "", err
	}
	confDir := filepath.Join(conf, "containers", "podman", "machine")
	return confDir, nil
}

type USBConfig struct {
	Bus       string
	DevNumber string
	Vendor    int
	Product   int
}

// ResourceConfig describes physical attributes of the machine
type ResourceConfig struct {
	// CPUs to be assigned to the VM
	CPUs uint64
	// Disk size in gigabytes assigned to the vm
	DiskSize uint64
	// Memory in megabytes assigned to the vm
	Memory uint64
	// Usbs
	USBs []USBConfig
}

type Mount struct {
	ReadOnly bool
	Source   string
	Tag      string
	Target   string
	Type     string
}

// ImageConfig describes the bootable image for the VM
type ImageConfig struct {
	// IgnitionFile is the path to the filesystem where the
	// ignition file was written (if needs one)
	IgnitionFile define.VMFile `json:"IgnitionFilePath"`
	// ImageStream is the update stream for the image
	ImageStream string
	// ImageFile is the fq path to
	ImagePath define.VMFile `json:"ImagePath"`
}

// HostUser describes the host user
type HostUser struct {
	// Whether this machine should run in a rootful or rootless manner
	Rootful bool
	// UID is the numerical id of the user that called machine
	UID int
	// Whether one of these fields has changed and actions should be taken
	Modified bool `json:"HostUserModified"`
}

// SSHConfig contains remote access information for SSH
type SSHConfig struct {
	// IdentityPath is the fq path to the ssh priv key
	IdentityPath string
	// SSH port for user networking
	Port int
	// RemoteUsername of the vm user
	RemoteUsername string
}

// ConnectionConfig contains connections like sockets, etc.
type ConnectionConfig struct {
	// PodmanSocket is the exported podman service socket
	PodmanSocket *define.VMFile `json:"PodmanSocket"`
	// PodmanPipe is the exported podman service named pipe (Windows hosts only)
	PodmanPipe *define.VMFile `json:"PodmanPipe"`
}

type VMType int64

const (
	QemuVirt VMType = iota
	WSLVirt
	AppleHvVirt
	HyperVVirt
	UnknownVirt
)

func (v VMType) String() string {
	switch v {
	case WSLVirt:
		return "wsl"
	case AppleHvVirt:
		return "applehv"
	case HyperVVirt:
		return "hyperv"
	}
	return "qemu"
}

type APIForwardingState int

const (
	NoForwarding APIForwardingState = iota
	ClaimUnsupported
	NotInstalled
	MachineLocal
	DockerGlobal
)

func ParseVMType(input string, emptyFallback VMType) (VMType, error) {
	switch strings.TrimSpace(strings.ToLower(input)) {
	case "qemu":
		return QemuVirt, nil
	case "wsl":
		return WSLVirt, nil
	case "applehv":
		return AppleHvVirt, nil
	case "hyperv":
		return HyperVVirt, nil
	case "":
		return emptyFallback, nil
	default:
		return UnknownVirt, fmt.Errorf("unknown VMType `%s`", input)
	}
}

type VirtProvider interface { //nolint:interfacebloat
	Artifact() define.Artifact
	CheckExclusiveActiveVM() (bool, string, error)
	Compression() compression.ImageCompression
	Format() define.ImageFormat
	IsValidVMName(name string) (bool, error)
	List(opts ListOptions) ([]*ListResponse, error)
	LoadVMByName(name string) (VM, error)
	NewMachine(opts InitOptions) (VM, error)
	NewDownload(vmName string) (Download, error)
	RemoveAndCleanMachines() error
	VMType() VMType
}

type Virtualization struct {
	artifact    define.Artifact
	compression compression.ImageCompression
	format      define.ImageFormat
	vmKind      VMType
}

func (p *Virtualization) Artifact() define.Artifact {
	return p.artifact
}

func (p *Virtualization) Compression() compression.ImageCompression {
	return p.compression
}

func (p *Virtualization) Format() define.ImageFormat {
	return p.format
}

func (p *Virtualization) VMType() VMType {
	return p.vmKind
}

func (p *Virtualization) NewDownload(vmName string) (Download, error) {
	cacheDir, err := GetCacheDir(p.VMType())
	if err != nil {
		return Download{}, err
	}

	dataDir, err := GetDataDir(p.VMType())
	if err != nil {
		return Download{}, err
	}

	return Download{
		Artifact:        p.Artifact(),
		CacheDir:        cacheDir,
		CompressionType: p.Compression(),
		DataDir:         dataDir,
		Format:          p.Format(),
		VMKind:          p.VMType(),
		VMName:          vmName,
	}, nil
}

func NewVirtualization(artifact define.Artifact, compression compression.ImageCompression, format define.ImageFormat, vmKind VMType) Virtualization {
	return Virtualization{
		artifact,
		compression,
		format,
		vmKind,
	}
}

func WaitAndPingAPI(sock string) {
	client := http.Client{
		Transport: &http.Transport{
			DialContext: func(context.Context, string, string) (net.Conn, error) {
				con, err := net.DialTimeout("unix", sock, apiUpTimeout)
				if err != nil {
					return nil, err
				}
				if err := con.SetDeadline(time.Now().Add(apiUpTimeout)); err != nil {
					return nil, err
				}
				return con, nil
			},
		},
	}

	resp, err := client.Get("http://host/_ping")
	if err == nil {
		defer resp.Body.Close()
	}
	if err != nil || resp.StatusCode != 200 {
		logrus.Warn("API socket failed ping test")
	}
}

func (dl Download) NewFcosDownloader(imageStream FCOSStream) (DistributionDownload, error) {
	info, err := dl.GetFCOSDownload(imageStream)
	if err != nil {
		return nil, err
	}
	urlSplit := strings.Split(info.Location, "/")
	dl.ImageName = urlSplit[len(urlSplit)-1]
	downloadURL, err := url.Parse(info.Location)
	if err != nil {
		return nil, err
	}

	// Complete the download struct
	dl.Arch = GetFcosArch()
	// This could be eliminated as a struct and be a generated()
	dl.LocalPath = filepath.Join(dl.CacheDir, dl.ImageName)
	dl.Sha256sum = info.Sha256Sum
	dl.URL = downloadURL
	fcd := FcosDownload{
		Download: dl,
	}
	dataDir, err := GetDataDir(dl.VMKind)
	if err != nil {
		return nil, err
	}
	fcd.Download.LocalUncompressedFile = fcd.GetLocalUncompressedFile(dataDir)
	return fcd, nil
}

// AcquireVMImage determines if the image is already in a FCOS stream. If so,
// retrieves the image path of the uncompressed file. Otherwise, the user has
// provided an alternative image, so we set the image path and download the image.
func (dl Download) AcquireVMImage(imagePath string) (*define.VMFile, FCOSStream, error) {
	var (
		err           error
		imageLocation *define.VMFile
		fcosStream    FCOSStream
	)

	switch imagePath {
	// TODO these need to be re-typed as FCOSStreams
	case Testing.String(), Next.String(), Stable.String(), "":
		// Get image as usual
		fcosStream, err = FCOSStreamFromString(imagePath)
		if err != nil {
			return nil, 0, err
		}

		dd, err := dl.NewFcosDownloader(fcosStream)
		if err != nil {
			return nil, 0, err
		}

		imageLocation, err = define.NewMachineFile(dd.Get().LocalUncompressedFile, nil)
		if err != nil {
			return nil, 0, err
		}

		if err := DownloadImage(dd); err != nil {
			return nil, 0, err
		}
	default:
		// The user has provided an alternate image which can be a file path
		// or URL.
		fcosStream = CustomStream
		imgPath, err := dl.AcquireAlternateImage(imagePath)
		if err != nil {
			return nil, 0, err
		}
		imageLocation = imgPath
	}
	return imageLocation, fcosStream, nil
}
