//go:build windows

package uvm

import (
	"context"
	"fmt"
	"os"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"

	"github.com/Microsoft/hcsshim/ext4/dmverity"
	"github.com/Microsoft/hcsshim/ext4/tar2ext4"
	"github.com/Microsoft/hcsshim/internal/hcs/resourcepaths"
	hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
	"github.com/Microsoft/hcsshim/internal/log"
	"github.com/Microsoft/hcsshim/internal/protocol/guestrequest"
	"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
)

const (
	lcowDefaultVPMemLayerFmt = "/run/layers/p%d"
)

var (
	// ErrMaxVPMemLayerSize is the error returned when the size of `hostPath` is
	// greater than the max vPMem layer size set at create time.
	ErrMaxVPMemLayerSize = errors.New("layer size is to large for VPMEM max size")
)

// var _ resources.ResourceCloser = &VPMEMMount{} -- Causes an import cycle.

type VPMEMMount struct {
	GuestPath string
	uvm       *UtilityVM
	hostPath  string
}

func (vc *VPMEMMount) Release(ctx context.Context) error {
	return vc.uvm.RemoveVPMem(ctx, vc.hostPath)
}

type vPMemInfoDefault struct {
	hostPath string
	uvmPath  string
	refCount uint32
}

func newDefaultVPMemInfo(hostPath, uvmPath string) *vPMemInfoDefault {
	return &vPMemInfoDefault{
		hostPath: hostPath,
		uvmPath:  uvmPath,
		refCount: 1,
	}
}

// fileSystemSize retrieves ext4 fs SuperBlock and returns the file system size and block size
func fileSystemSize(vhdPath string) (int64, int, error) {
	sb, err := tar2ext4.ReadExt4SuperBlock(vhdPath)
	if err != nil {
		return 0, 0, errors.Wrap(err, "failed to read ext4 super block")
	}
	blockSize := 1024 * (1 << sb.LogBlockSize)
	fsSize := int64(blockSize) * int64(sb.BlocksCountLow)
	return fsSize, blockSize, nil
}

// readVeritySuperBlock reads ext4 super block for a given VHD to then further read the dm-verity super block
// and root hash
func readVeritySuperBlock(ctx context.Context, layerPath string) (*guestresource.DeviceVerityInfo, error) {
	// dm-verity information is expected to be appended, the size of ext4 data will be the offset
	// of the dm-verity super block, followed by merkle hash tree
	ext4SizeInBytes, ext4BlockSize, err := fileSystemSize(layerPath)
	if err != nil {
		return nil, err
	}

	dmvsb, err := dmverity.ReadDMVerityInfo(layerPath, ext4SizeInBytes)
	if err != nil {
		return nil, errors.Wrap(err, "failed to read dm-verity super block")
	}
	log.G(ctx).WithFields(logrus.Fields{
		"layerPath":     layerPath,
		"rootHash":      dmvsb.RootDigest,
		"algorithm":     dmvsb.Algorithm,
		"salt":          dmvsb.Salt,
		"dataBlocks":    dmvsb.DataBlocks,
		"dataBlockSize": dmvsb.DataBlockSize,
	}).Debug("dm-verity information")

	return &guestresource.DeviceVerityInfo{
		Ext4SizeInBytes: ext4SizeInBytes,
		BlockSize:       ext4BlockSize,
		RootDigest:      dmvsb.RootDigest,
		Algorithm:       dmvsb.Algorithm,
		Salt:            dmvsb.Salt,
		Version:         int(dmvsb.Version),
		SuperBlock:      true,
	}, nil
}

// findNextVPMemSlot finds next available VPMem slot.
//
// Lock MUST be held when calling this function.
func (uvm *UtilityVM) findNextVPMemSlot(ctx context.Context, hostPath string) (uint32, error) {
	for i := uint32(0); i < uvm.vpmemMaxCount; i++ {
		if uvm.vpmemDevicesDefault[i] == nil {
			log.G(ctx).WithFields(logrus.Fields{
				"hostPath":     hostPath,
				"deviceNumber": i,
			}).Debug("allocated VPMem location")
			return i, nil
		}
	}
	return 0, ErrNoAvailableLocation
}

// findVPMemSlot looks up `findThisHostPath` in already mounted VPMem devices
//
// Lock MUST be held when calling this function
func (uvm *UtilityVM) findVPMemSlot(ctx context.Context, findThisHostPath string) (uint32, error) {
	for i := uint32(0); i < uvm.vpmemMaxCount; i++ {
		if vi := uvm.vpmemDevicesDefault[i]; vi != nil && vi.hostPath == findThisHostPath {
			log.G(ctx).WithFields(logrus.Fields{
				"hostPath":     vi.hostPath,
				"uvmPath":      vi.uvmPath,
				"refCount":     vi.refCount,
				"deviceNumber": i,
			}).Debug("found VPMem location")
			return i, nil
		}
	}
	return 0, ErrNotAttached
}

// addVPMemDefault adds a VPMem disk to a utility VM at the next available location and
// returns the UVM path where the layer was mounted.
func (uvm *UtilityVM) addVPMemDefault(ctx context.Context, hostPath string) (_ string, err error) {
	if devNumber, err := uvm.findVPMemSlot(ctx, hostPath); err == nil {
		device := uvm.vpmemDevicesDefault[devNumber]
		device.refCount++
		return device.uvmPath, nil
	}

	fi, err := os.Stat(hostPath)
	if err != nil {
		return "", err
	}
	if uint64(fi.Size()) > uvm.vpmemMaxSizeBytes {
		return "", ErrMaxVPMemLayerSize
	}

	deviceNumber, err := uvm.findNextVPMemSlot(ctx, hostPath)
	if err != nil {
		return "", err
	}

	modification := &hcsschema.ModifySettingRequest{
		RequestType: guestrequest.RequestTypeAdd,
		Settings: hcsschema.VirtualPMemDevice{
			HostPath:    hostPath,
			ReadOnly:    true,
			ImageFormat: "Vhd1",
		},
		ResourcePath: fmt.Sprintf(resourcepaths.VPMemControllerResourceFormat, deviceNumber),
	}

	uvmPath := fmt.Sprintf(lcowDefaultVPMemLayerFmt, deviceNumber)
	guestSettings := guestresource.LCOWMappedVPMemDevice{
		DeviceNumber: deviceNumber,
		MountPath:    uvmPath,
	}
	if v, iErr := readVeritySuperBlock(ctx, hostPath); iErr != nil {
		log.G(ctx).WithError(iErr).WithField("hostPath", hostPath).Debug("unable to read dm-verity information from VHD")
	} else {
		if v != nil {
			log.G(ctx).WithFields(logrus.Fields{
				"hostPath":   hostPath,
				"rootDigest": v.RootDigest,
			}).Debug("adding VPMem with dm-verity")
		}
		guestSettings.VerityInfo = v
	}

	modification.GuestRequest = guestrequest.ModificationRequest{
		ResourceType: guestresource.ResourceTypeVPMemDevice,
		RequestType:  guestrequest.RequestTypeAdd,
		Settings:     guestSettings,
	}

	if err := uvm.modify(ctx, modification); err != nil {
		return "", errors.Errorf("uvm::addVPMemDefault: failed to modify utility VM configuration: %s", err)
	}

	uvm.vpmemDevicesDefault[deviceNumber] = newDefaultVPMemInfo(hostPath, uvmPath)
	return uvmPath, nil
}

// removeVPMemDefault removes a VPMem disk from a Utility VM. If the `hostPath` is not
// attached returns `ErrNotAttached`.
func (uvm *UtilityVM) removeVPMemDefault(ctx context.Context, hostPath string) error {
	deviceNumber, err := uvm.findVPMemSlot(ctx, hostPath)
	if err != nil {
		return err
	}

	device := uvm.vpmemDevicesDefault[deviceNumber]
	if device.refCount > 1 {
		device.refCount--
		return nil
	}

	var verity *guestresource.DeviceVerityInfo
	if v, _ := readVeritySuperBlock(ctx, hostPath); v != nil {
		log.G(ctx).WithFields(logrus.Fields{
			"hostPath":   hostPath,
			"rootDigest": v.RootDigest,
		}).Debug("removing VPMem with dm-verity")
		verity = v
	}
	modification := &hcsschema.ModifySettingRequest{
		RequestType:  guestrequest.RequestTypeRemove,
		ResourcePath: fmt.Sprintf(resourcepaths.VPMemControllerResourceFormat, deviceNumber),
		GuestRequest: guestrequest.ModificationRequest{
			ResourceType: guestresource.ResourceTypeVPMemDevice,
			RequestType:  guestrequest.RequestTypeRemove,
			Settings: guestresource.LCOWMappedVPMemDevice{
				DeviceNumber: deviceNumber,
				MountPath:    device.uvmPath,
				VerityInfo:   verity,
			},
		},
	}
	if err := uvm.modify(ctx, modification); err != nil {
		return errors.Errorf("failed to remove VPMEM %s from utility VM %s: %s", hostPath, uvm.id, err)
	}
	log.G(ctx).WithFields(logrus.Fields{
		"hostPath":     device.hostPath,
		"uvmPath":      device.uvmPath,
		"refCount":     device.refCount,
		"deviceNumber": deviceNumber,
	}).Debug("removed VPMEM location")

	uvm.vpmemDevicesDefault[deviceNumber] = nil

	return nil
}

func (uvm *UtilityVM) AddVPMem(ctx context.Context, hostPath string) (*VPMEMMount, error) {
	if uvm.operatingSystem != "linux" {
		return nil, errNotSupported
	}

	uvm.m.Lock()
	defer uvm.m.Unlock()

	var (
		guestPath string
		err       error
	)
	if uvm.vpmemMultiMapping {
		guestPath, err = uvm.addVPMemMappedDevice(ctx, hostPath)
	} else {
		guestPath, err = uvm.addVPMemDefault(ctx, hostPath)
	}
	if err != nil {
		return nil, err
	}
	return &VPMEMMount{GuestPath: guestPath, uvm: uvm, hostPath: hostPath}, nil
}

func (uvm *UtilityVM) RemoveVPMem(ctx context.Context, hostPath string) error {
	if uvm.operatingSystem != "linux" {
		return errNotSupported
	}

	uvm.m.Lock()
	defer uvm.m.Unlock()

	if uvm.vpmemMultiMapping {
		return uvm.removeVPMemMappedDevice(ctx, hostPath)
	}
	return uvm.removeVPMemDefault(ctx, hostPath)
}
