//go:build windows

package cim

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"syscall"
	"time"

	"github.com/Microsoft/go-winio"
	"github.com/Microsoft/go-winio/vhd"
	"github.com/Microsoft/hcsshim/computestorage"
	"github.com/Microsoft/hcsshim/internal/memory"
	"github.com/Microsoft/hcsshim/internal/security"
	"github.com/Microsoft/hcsshim/internal/vhdx"
	"github.com/Microsoft/hcsshim/internal/wclayer"
	"golang.org/x/sys/windows"
)

const defaultVHDXBlockSizeInMB = 1

// processUtilityVMLayer is similar to createContainerBaseLayerVHDs but along with the scratch creation it
// also does some BCD modifications to allow the UVM to boot from the CIM. It expects that the UVM BCD file is
// present at layerPath/`wclayer.BcdFilePath` and a UVM SYSTEM hive is present at
// layerPath/UtilityVM/`wclayer.RegFilesPath`/SYSTEM. The scratch VHDs are created under the `layerPath`
// directory.
func processUtilityVMLayer(ctx context.Context, layerPath string) error {
	// func createUtilityVMLayerVHDs(ctx context.Context, layerPath string) error {
	baseVhdPath := filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.UtilityVMBaseVhd)
	diffVhdPath := filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.UtilityVMScratchVhd)
	defaultVhdSize := uint64(10)

	// Just create the vhdx for utilityVM layer, no need to format it.
	createParams := &vhd.CreateVirtualDiskParameters{
		Version: 2,
		Version2: vhd.CreateVersion2{
			MaximumSize:      defaultVhdSize * memory.GiB,
			BlockSizeInBytes: defaultVHDXBlockSizeInMB * memory.MiB,
		},
	}

	handle, err := vhd.CreateVirtualDisk(baseVhdPath, vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, createParams)
	if err != nil {
		return fmt.Errorf("failed to create vhdx: %w", err)
	}

	defer func() {
		if err != nil {
			os.RemoveAll(baseVhdPath)
			os.RemoveAll(diffVhdPath)
		}
	}()

	err = computestorage.FormatWritableLayerVhd(ctx, windows.Handle(handle))
	closeErr := syscall.CloseHandle(handle)
	if err != nil {
		return err
	} else if closeErr != nil {
		return fmt.Errorf("failed to close vhdx handle: %w", closeErr)
	}

	partitionInfo, err := vhdx.GetScratchVhdPartitionInfo(ctx, baseVhdPath)
	if err != nil {
		return fmt.Errorf("failed to get base vhd layout info: %w", err)
	}
	// relativeCimPath needs to be the cim path relative to the snapshots directory. The snapshots
	// directory is shared inside the UVM over VSMB, so during the UVM boot this relative path will be
	// used to find the cim file under that VSMB share.
	relativeCimPath := filepath.Join(filepath.Base(GetCimDirFromLayer(layerPath)), GetCimNameFromLayer(layerPath))
	bcdPath := filepath.Join(layerPath, bcdFilePath)
	if err = updateBcdStoreForBoot(bcdPath, relativeCimPath, partitionInfo.DiskID, partitionInfo.PartitionID); err != nil {
		return fmt.Errorf("failed to update BCD: %w", err)
	}

	if err := enableCimBoot(filepath.Join(layerPath, wclayer.UtilityVMPath, wclayer.RegFilesPath, "SYSTEM")); err != nil {
		return fmt.Errorf("failed to setup cim image for uvm boot: %w", err)
	}

	// Note: diff vhd creation and granting of vm group access must be done AFTER
	// getting the partition info of the base VHD. Otherwise it causes the vhd parent
	// chain to get corrupted.
	// TODO(ambarve): figure out why this happens so that bcd update can be moved to a separate function

	// Create the differencing disk that will be what's copied for the final rw layer
	// for a container.
	if err = vhd.CreateDiffVhd(diffVhdPath, baseVhdPath, defaultVHDXBlockSizeInMB); err != nil {
		return fmt.Errorf("failed to create differencing disk: %w", err)
	}

	if err := security.GrantVmGroupAccess(baseVhdPath); err != nil {
		return fmt.Errorf("failed to grant vm group access to %s: %w", baseVhdPath, err)
	}
	if err := security.GrantVmGroupAccess(diffVhdPath); err != nil {
		return fmt.Errorf("failed to grant vm group access to %s: %w", diffVhdPath, err)
	}
	return nil
}

// processBaseLayerHives make the base layer specific modifications on the hives and emits equivalent the
// pendingCimOps that should be applied on the CIM.  In base layer we need to create hard links from registry
// hives under Files/Windows/Sysetm32/config into Hives/*_BASE. This function creates these links outside so
// that the registry hives under Hives/ are available during children layers import.  Then we write these hive
// files inside the cim and create links inside the cim.
func processBaseLayerHives(layerPath string) ([]pendingCimOp, error) {
	pendingOps := []pendingCimOp{}

	// make hives directory both outside and in the cim
	if err := os.Mkdir(filepath.Join(layerPath, wclayer.HivesPath), 0755); err != nil {
		return pendingOps, fmt.Errorf("hives directory creation: %w", err)
	}

	hivesDirInfo := &winio.FileBasicInfo{
		CreationTime:   windows.NsecToFiletime(time.Now().UnixNano()),
		LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
		LastWriteTime:  windows.NsecToFiletime(time.Now().UnixNano()),
		ChangeTime:     windows.NsecToFiletime(time.Now().UnixNano()),
		FileAttributes: windows.FILE_ATTRIBUTE_DIRECTORY,
	}
	pendingOps = append(pendingOps, &addOp{
		pathInCim: wclayer.HivesPath,
		hostPath:  filepath.Join(layerPath, wclayer.HivesPath),
		fileInfo:  hivesDirInfo,
	})

	// add hard links from base hive files.
	for _, hv := range hives {
		oldHivePathRelative := filepath.Join(wclayer.RegFilesPath, hv.name)
		newHivePathRelative := filepath.Join(wclayer.HivesPath, hv.base)
		if err := os.Link(filepath.Join(layerPath, oldHivePathRelative), filepath.Join(layerPath, newHivePathRelative)); err != nil {
			return pendingOps, fmt.Errorf("hive link creation: %w", err)
		}

		pendingOps = append(pendingOps, &linkOp{
			oldPath: oldHivePathRelative,
			newPath: newHivePathRelative,
		})
	}
	return pendingOps, nil
}

// processLayoutFile creates a file named "layout" in the root of the base layer. This allows certain
// container startup related functions to understand that the hives are a part of the container rootfs.
func processLayoutFile(layerPath string) ([]pendingCimOp, error) {
	fileContents := "vhd-with-hives\n"
	if err := os.WriteFile(filepath.Join(layerPath, "layout"), []byte(fileContents), 0755); err != nil {
		return []pendingCimOp{}, fmt.Errorf("write layout file: %w", err)
	}

	layoutFileInfo := &winio.FileBasicInfo{
		CreationTime:   windows.NsecToFiletime(time.Now().UnixNano()),
		LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
		LastWriteTime:  windows.NsecToFiletime(time.Now().UnixNano()),
		ChangeTime:     windows.NsecToFiletime(time.Now().UnixNano()),
		FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
	}

	op := &addOp{
		pathInCim: "layout",
		hostPath:  filepath.Join(layerPath, "layout"),
		fileInfo:  layoutFileInfo,
	}
	return []pendingCimOp{op}, nil
}

// Some of the layer files that are generated during the processBaseLayer call must be added back
// inside the cim, some registry file links must be updated. This function takes care of all those
// steps. This function opens the cim file for writing and updates it.
func (cw *CimLayerWriter) processBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
	if processUtilityVM {
		if err = processUtilityVMLayer(ctx, cw.path); err != nil {
			return fmt.Errorf("process utilityVM layer: %w", err)
		}
	}

	ops, err := processBaseLayerHives(cw.path)
	if err != nil {
		return err
	}
	cw.pendingOps = append(cw.pendingOps, ops...)

	ops, err = processLayoutFile(cw.path)
	if err != nil {
		return err
	}
	cw.pendingOps = append(cw.pendingOps, ops...)
	return nil
}

// processNonBaseLayer takes care of the processing required for a non base layer. As of now
// the only processing required for non base layer is to merge the delta registry hives of the
// non-base layer with it's parent layer.
func (cw *CimLayerWriter) processNonBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
	for _, hv := range hives {
		baseHive := filepath.Join(wclayer.HivesPath, hv.base)
		deltaHive := filepath.Join(wclayer.HivesPath, hv.delta)
		_, err := os.Stat(filepath.Join(cw.path, deltaHive))
		// merge with parent layer if delta exists.
		if err != nil && !os.IsNotExist(err) {
			return fmt.Errorf("stat delta hive %s: %w", filepath.Join(cw.path, deltaHive), err)
		} else if err == nil {
			// merge base hive of parent layer with the delta hive of this layer and write it as
			// the base hive of this layer.
			err = mergeHive(filepath.Join(cw.parentLayerPaths[0], baseHive), filepath.Join(cw.path, deltaHive), filepath.Join(cw.path, baseHive))
			if err != nil {
				return err
			}

			// the newly created merged file must be added to the cim
			cw.pendingOps = append(cw.pendingOps, &addOp{
				pathInCim: baseHive,
				hostPath:  filepath.Join(cw.path, baseHive),
				fileInfo: &winio.FileBasicInfo{
					CreationTime:   windows.NsecToFiletime(time.Now().UnixNano()),
					LastAccessTime: windows.NsecToFiletime(time.Now().UnixNano()),
					LastWriteTime:  windows.NsecToFiletime(time.Now().UnixNano()),
					ChangeTime:     windows.NsecToFiletime(time.Now().UnixNano()),
					FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
				},
			})
		}
	}

	if processUtilityVM {
		return processUtilityVMLayer(ctx, cw.path)
	}
	return nil
}
