//go:build windows
// +build windows

/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package windows

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"

	"github.com/Microsoft/hcsshim"
	"github.com/containerd/containerd/v2/core/snapshots"
	"github.com/containerd/containerd/v2/core/snapshots/storage"
	"github.com/containerd/continuity/fs"
	"github.com/containerd/log"
)

// windowsBaseSnapshotter is a type that implements common functionality required by both windows & cimfs
// snapshotters (sort of a base type that windows & cimfs snapshotter types derive from - however, windowsBaseSnapshotter does NOT impelement the full Snapshotter interface).  Some functions
// (like Stat, Update) that are identical for both snapshotters are directly implemented in this base
// snapshotter and such functions handle database transaction creation etc. However, the functions that are
// not common don't create a transaction to allow the caller the flexibility of deciding whether to commit or
// abort the transaction.
type windowsBaseSnapshotter struct {
	root string
	ms   *storage.MetaStore
	info hcsshim.DriverInfo
}

func newBaseSnapshotter(root string) (*windowsBaseSnapshotter, error) {
	if err := os.MkdirAll(root, 0700); err != nil {
		return nil, err
	}

	ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db"))
	if err != nil {
		return nil, err
	}

	if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) {
		return nil, err
	}

	return &windowsBaseSnapshotter{
		root: root,
		ms:   ms,
		info: hcsshim.DriverInfo{HomeDir: filepath.Join(root, "snapshots")},
	}, nil
}

func (w *windowsBaseSnapshotter) getSnapshotDir(id string) string {
	return filepath.Join(w.root, "snapshots", id)
}

func (w *windowsBaseSnapshotter) parentIDsToParentPaths(parentIDs []string) []string {
	parentLayerPaths := make([]string, 0, len(parentIDs))
	for _, ID := range parentIDs {
		parentLayerPaths = append(parentLayerPaths, w.getSnapshotDir(ID))
	}
	return parentLayerPaths
}

func (w *windowsBaseSnapshotter) Stat(ctx context.Context, key string) (info snapshots.Info, err error) {
	err = w.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
		_, info, _, err = storage.GetInfo(ctx, key)
		return err
	})
	if err != nil {
		return snapshots.Info{}, err
	}

	return info, nil
}

func (w *windowsBaseSnapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (_ snapshots.Info, err error) {
	err = w.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
		info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
		return err
	})
	if err != nil {
		return snapshots.Info{}, err
	}

	return info, nil
}

func (w *windowsBaseSnapshotter) Usage(ctx context.Context, key string) (usage snapshots.Usage, err error) {
	var (
		id   string
		info snapshots.Info
	)

	err = w.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
		id, info, usage, err = storage.GetInfo(ctx, key)
		return err
	})
	if err != nil {
		return snapshots.Usage{}, err
	}

	if info.Kind == snapshots.KindActive {
		path := w.getSnapshotDir(id)
		du, err := fs.DiskUsage(ctx, path)
		if err != nil {
			return snapshots.Usage{}, err
		}

		usage = snapshots.Usage(du)
	}

	return usage, nil
}

// Walk the committed snapshots.
func (w *windowsBaseSnapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
	return w.ms.WithTransaction(ctx, false, func(ctx context.Context) error {
		return storage.WalkInfo(ctx, fn, fs...)
	})
}

// preRemove prepares for removal of a snapshot by first renaming the snapshot directory and if that succeeds
// removing the snapshot info from the database. Then the caller can decide how to remove the actual renamed
// snapshot directory. Returns the new 'ID' (i.e the directory name after rename).
func (w *windowsBaseSnapshotter) preRemove(ctx context.Context, key string) (string, error) {
	var (
		renamed, path, renamedID string
		restore                  bool
	)

	err := w.ms.WithTransaction(ctx, true, func(ctx context.Context) error {
		id, _, err := storage.Remove(ctx, key)
		if err != nil {
			return fmt.Errorf("failed to remove: %w", err)
		}

		path = w.getSnapshotDir(id)
		renamedID = "rm-" + id
		renamed = w.getSnapshotDir(renamedID)
		if err = os.Rename(path, renamed); err != nil && !os.IsNotExist(err) {
			if !os.IsPermission(err) {
				return err
			}
			// If permission denied, it's possible that the scratch is still mounted, an
			// artifact after a hard daemon crash for example. Worth a shot to try deactivating it
			// before retrying the rename.
			var (
				home, layerID = filepath.Split(path)
				di            = hcsshim.DriverInfo{
					HomeDir: home,
				}
			)

			if deactivateErr := hcsshim.DeactivateLayer(di, layerID); deactivateErr != nil {
				return fmt.Errorf("failed to deactivate layer following failed rename: %s: %w", deactivateErr, err)
			}

			if renameErr := os.Rename(path, renamed); renameErr != nil && !os.IsNotExist(renameErr) {
				return fmt.Errorf("second rename attempt following detach failed: %s: %w", renameErr, err)
			}
		}

		restore = true
		return nil
	})
	if err != nil {
		if restore { // failed to commit
			if err1 := os.Rename(renamed, path); err1 != nil {
				// May cause inconsistent data on disk
				log.G(ctx).WithError(err1).WithField("path", renamed).Error("Failed to rename after failed commit")
			}
		}
		return "", err
	}
	return renamedID, nil
}

// Close closes the snapshotter
func (w *windowsBaseSnapshotter) Close() error {
	return w.ms.Close()
}

// This handles creating the UVMs scratch layer.
func (w *windowsBaseSnapshotter) createUVMScratchLayer(ctx context.Context, snDir string, parentLayers []string) error {
	parentLen := len(parentLayers)
	if parentLen == 0 {
		return errors.New("no parent layers present")
	}
	baseLayer := parentLayers[parentLen-1]

	// Make sure base layer has a UtilityVM folder.
	uvmPath := filepath.Join(baseLayer, "UtilityVM")
	if _, err := os.Stat(uvmPath); os.IsNotExist(err) {
		return fmt.Errorf("failed to find UtilityVM directory in base layer %q: %w", baseLayer, err)
	}

	templateDiffDisk := filepath.Join(uvmPath, "SystemTemplate.vhdx")

	// Check if SystemTemplate disk doesn't exist for some reason (this should be made during the unpacking
	// of the base layer).
	if _, err := os.Stat(templateDiffDisk); os.IsNotExist(err) {
		return fmt.Errorf("%q does not exist in Utility VM image", templateDiffDisk)
	}

	// Move the sandbox.vhdx into a nested vm folder to avoid clashing with a containers sandbox.vhdx.
	vmScratchDir := filepath.Join(snDir, "vm")
	if err := os.MkdirAll(vmScratchDir, 0777); err != nil {
		return fmt.Errorf("failed to make `vm` directory for vm's scratch space: %w", err)
	}

	return copyScratchDisk(templateDiffDisk, filepath.Join(vmScratchDir, "sandbox.vhdx"))
}

func copyScratchDisk(source, dest string) error {
	scratchSource, err := os.OpenFile(source, os.O_RDWR, 0700)
	if err != nil {
		return fmt.Errorf("failed to open %s: %w", source, err)
	}
	defer scratchSource.Close()

	f, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0700)
	if err != nil {
		return fmt.Errorf("failed to create sandbox.vhdx in snapshot: %w", err)
	}
	defer f.Close()

	if _, err := io.Copy(f, scratchSource); err != nil {
		os.Remove(dest)
		return fmt.Errorf("failed to copy cached %q to %q in snapshot: %w", source, dest, err)
	}
	return nil
}

func getRequestedScratchSize(ctx context.Context, snapshotInfo snapshots.Info) (uint64, error) {
	var sizeInBytes uint64
	var err error
	if sizeGBstr, ok := snapshotInfo.Labels[rootfsSizeInGBLabel]; ok {
		log.G(ctx).Warnf("%q label is deprecated, please use %q instead.", rootfsSizeInGBLabel, rootfsSizeInBytesLabel)

		sizeInGB, err := strconv.ParseUint(sizeGBstr, 10, 32)
		if err != nil {
			return 0, fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInGBLabel, sizeGBstr, err)
		}
		sizeInBytes = sizeInGB * 1024 * 1024 * 1024
	}

	// Prefer the newer label in bytes over the deprecated Windows specific GB variant.
	if sizeBytesStr, ok := snapshotInfo.Labels[rootfsSizeInBytesLabel]; ok {
		sizeInBytes, err = strconv.ParseUint(sizeBytesStr, 10, 64)
		if err != nil {
			return 0, fmt.Errorf("failed to parse label %q=%q: %w", rootfsSizeInBytesLabel, sizeBytesStr, err)
		}
	}
	return sizeInBytes, nil
}
