package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/lxc/incus/v7/internal/rsync"
	"github.com/lxc/incus/v7/internal/server/db"
	"github.com/lxc/incus/v7/internal/server/instance"
	"github.com/lxc/incus/v7/internal/server/instance/instancetype"
	"github.com/lxc/incus/v7/internal/server/node"
	"github.com/lxc/incus/v7/internal/server/project"
	"github.com/lxc/incus/v7/internal/server/state"
	storagePools "github.com/lxc/incus/v7/internal/server/storage"
	storageDrivers "github.com/lxc/incus/v7/internal/server/storage/drivers"
	internalUtil "github.com/lxc/incus/v7/internal/util"
	"github.com/lxc/incus/v7/shared/api"
	"github.com/lxc/incus/v7/shared/logger"
	"github.com/lxc/incus/v7/shared/util"
)

func daemonStorageVolumesUnmount(s *state.State) error {
	var storageBackups string
	var storageImages string

	err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error {
		nodeConfig, err := node.ConfigLoad(ctx, tx)
		if err != nil {
			return err
		}

		storageBackups = nodeConfig.StorageBackupsVolume()
		storageImages = nodeConfig.StorageImagesVolume()

		return nil
	})
	if err != nil {
		return err
	}

	unmount := func(storageType string, source string) error {
		// Parse the source.
		poolName, volumeName, err := daemonStorageSplitVolume(source)
		if err != nil {
			return err
		}

		pool, err := storagePools.LoadByName(s, poolName)
		if err != nil {
			return err
		}

		// Mount volume.
		_, err = pool.UnmountCustomVolume(api.ProjectDefaultName, volumeName, nil)
		if err != nil {
			return fmt.Errorf("Failed to unmount storage volume %q: %w", source, err)
		}

		return nil
	}

	if storageBackups != "" {
		err := unmount("backups", storageBackups)
		if err != nil {
			return fmt.Errorf("Failed to unmount backups storage: %w", err)
		}
	}

	if storageImages != "" {
		err := unmount("images", storageImages)
		if err != nil {
			return fmt.Errorf("Failed to unmount images storage: %w", err)
		}
	}

	return nil
}

func daemonStorageMount(s *state.State) error {
	var storageBackups string
	var storageImages string
	var storageLogs string

	err := s.DB.Node.Transaction(context.Background(), func(ctx context.Context, tx *db.NodeTx) error {
		nodeConfig, err := node.ConfigLoad(ctx, tx)
		if err != nil {
			return err
		}

		storageBackups = nodeConfig.StorageBackupsVolume()
		storageImages = nodeConfig.StorageImagesVolume()
		storageLogs = nodeConfig.StorageLogsVolume()

		return nil
	})
	if err != nil {
		return err
	}

	mount := func(storageType string, source string) error {
		// Parse the source.
		poolName, volumeName, err := daemonStorageSplitVolume(source)
		if err != nil {
			return err
		}

		pool, err := storagePools.LoadByName(s, poolName)
		if err != nil {
			return err
		}

		// Mount volume.
		_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
		if err != nil {
			return fmt.Errorf("Failed to mount storage volume %q: %w", source, err)
		}

		// Ensure we have the correct symlink in place.
		volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)

		_ = os.RemoveAll(internalUtil.VarPath(storageType))
		err = os.Symlink(internalUtil.VarPath("storage-pools", poolName, "custom", volStorageName), internalUtil.VarPath(storageType))
		if err != nil {
			return fmt.Errorf("Failed to set up symlink for %q: %w", storageType, err)
		}

		return nil
	}

	if storageBackups != "" {
		err := mount("backups", storageBackups)
		if err != nil {
			return fmt.Errorf("Failed to mount backups storage: %w", err)
		}
	}

	if storageImages != "" {
		err := mount("images", storageImages)
		if err != nil {
			return fmt.Errorf("Failed to mount images storage: %w", err)
		}
	}

	if storageLogs != "" {
		err := mount("logs", storageLogs)
		if err != nil {
			return fmt.Errorf("Failed to mount logs storage: %w", err)
		}
	}

	return nil
}

func daemonStorageSplitVolume(volume string) (string, string, error) {
	fields := strings.Split(volume, "/")
	if len(fields) != 2 {
		return "", "", errors.New("Invalid syntax for volume, must be <pool>/<volume>")
	}

	poolName := fields[0]
	volumeName := fields[1]

	return poolName, volumeName, nil
}

func daemonStorageValidate(s *state.State, storageType string, target string) error {
	// Check syntax.
	if target == "" {
		return nil
	}

	poolName, volumeName, err := daemonStorageSplitVolume(target)
	if err != nil {
		return err
	}

	var poolID int64
	var snapshots []db.StorageVolumeArgs

	err = s.DB.Cluster.Transaction(context.Background(), func(ctx context.Context, tx *db.ClusterTx) error {
		// Validate pool exists.
		var pool *api.StoragePool
		poolID, pool, _, err = tx.GetStoragePool(ctx, poolName)
		if err != nil {
			return fmt.Errorf("Unable to load storage pool %q: %w", poolName, err)
		}

		if slices.Contains(db.StorageRemoteDriverNames(), pool.Driver) {
			return errors.New("Daemon storage volumes cannot be located on clustered storage pools")
		}

		// Confirm volume exists.
		dbVol, err := tx.GetStoragePoolVolume(ctx, poolID, api.ProjectDefaultName, db.StoragePoolVolumeTypeCustom, volumeName, true)
		if err != nil {
			return fmt.Errorf("Failed loading storage volume %q in %q project: %w", target, api.ProjectDefaultName, err)
		}

		if dbVol.ContentType != db.StoragePoolVolumeContentTypeNameFS {
			return fmt.Errorf("Storage volume %q in %q project is not filesystem content type", target, api.ProjectDefaultName)
		}

		snapshots, err = tx.GetLocalStoragePoolVolumeSnapshotsWithType(ctx, api.ProjectDefaultName, volumeName, db.StoragePoolVolumeTypeCustom, poolID)
		if err != nil {
			return fmt.Errorf("Unable to load storage volume snapshots %q in %q project: %w", target, api.ProjectDefaultName, err)
		}

		return nil
	})
	if err != nil {
		return err
	}

	if len(snapshots) != 0 {
		return errors.New("Storage volumes for use by Incus itself cannot have snapshots")
	}

	// If logs, ensure no running instances.
	if storageType == "logs" {
		localInstances, err := instance.LoadNodeAll(s, instancetype.Any)
		if err != nil {
			return fmt.Errorf("Failed loading local instance list: %w", err)
		}

		for _, inst := range localInstances {
			if inst.IsRunning() {
				return fmt.Errorf("`storage.logs_volume` cannot be changed if there are running instances")
			}
		}
	}

	pool, err := storagePools.LoadByName(s, poolName)
	if err != nil {
		return err
	}

	// Mount volume.
	_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
	if err != nil {
		return fmt.Errorf("Failed to mount storage volume %q: %w", target, err)
	}

	defer func() { _, _ = pool.UnmountCustomVolume(api.ProjectDefaultName, volumeName, nil) }()

	// Validate volume is empty (ignore lost+found).
	volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)
	mountpoint := storageDrivers.GetVolumeMountPath(poolName, storageDrivers.VolumeTypeCustom, volStorageName)

	entries, err := os.ReadDir(mountpoint)
	if err != nil {
		return fmt.Errorf("Failed to list %q: %w", mountpoint, err)
	}

	for _, entry := range entries {
		entryName := entry.Name()

		// Don't fail on clean ext4 volumes.
		if entryName == "lost+found" {
			continue
		}

		// Don't fail on systems with snapdir=visible.
		if entryName == ".zfs" {
			continue
		}

		return fmt.Errorf("Storage volume %q isn't empty", target)
	}

	return nil
}

func daemonStorageMove(s *state.State, storageType string, target string) error {
	var destPath string

	isLogs := storageType == "logs"
	if isLogs && target == "" && os.Getenv("INCUS_DIR") == "" {
		// We keep the system-wide location when dealing with logs without a custom daemon path.
		destPath = "/var/log/incus"
	} else {
		destPath = internalUtil.VarPath(storageType)
	}

	// Track down the current storage.
	var sourcePool string
	var sourceVolume string

	sourcePath, err := os.Readlink(internalUtil.VarPath(storageType))
	if err != nil {
		sourcePath = destPath
	} else {
		fields := strings.Split(sourcePath, "/")
		sourcePool = fields[len(fields)-3]
		sourceVolume = fields[len(fields)-1]
	}

	moveContent := func(source string, target string) error {
		// Copy the content.
		_, err := rsync.LocalCopy(source, target, "", false)
		if err != nil {
			return err
		}

		// Remove the source content.
		entries, err := os.ReadDir(source)
		if err != nil {
			return err
		}

		for _, entry := range entries {
			err := os.RemoveAll(filepath.Join(source, entry.Name()))
			if err != nil {
				return err
			}
		}

		return nil
	}

	// We should not move all data away from /var/log/incus and move it into the volume as that would interfere
	// with /var/log/incus/incusd.log which is created prior to us setting up volume mounts
	moveInstanceDirs := func(source string, target string) error {
		entries, err := os.ReadDir(source)
		if err != nil {
			return err
		}

		for _, entry := range entries {
			if !entry.IsDir() {
				continue
			}

			src := filepath.Join(source, entry.Name())
			dst := filepath.Join(target, entry.Name())

			_, err := rsync.LocalCopy(src, dst, "", false)
			if err != nil {
				return err
			}

			err = os.RemoveAll(src)
			if err != nil {
				return err
			}
		}

		return nil
	}

	// Deal with unsetting.
	if target == "" {
		// Things already look correct.
		if sourcePath == destPath {
			return nil
		}

		// Remove the symlink.
		err = os.Remove(internalUtil.VarPath(storageType))
		if err != nil {
			return fmt.Errorf("Failed to delete storage symlink at %q: %w", destPath, err)
		}

		// Re-create as a directory.
		// In the context of Logs, we ensure system log dir exists and move instance dirs back there
		err = os.MkdirAll(destPath, 0o700)
		if err != nil {
			return fmt.Errorf("Failed to create directory %q: %w", destPath, err)
		}

		if isLogs {
			err = moveInstanceDirs(sourcePath, destPath)
			if err != nil {
				return fmt.Errorf("Failed to move instance logs back to %q: %w", destPath, err)
			}
		} else {
			// Move the data across.
			err = moveContent(sourcePath, destPath)
			if err != nil {
				return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
			}
		}

		pool, err := storagePools.LoadByName(s, sourcePool)
		if err != nil {
			return err
		}

		// Unmount old volume.
		projectName, sourceVolumeName := project.StorageVolumeParts(sourceVolume)
		_, err = pool.UnmountCustomVolume(projectName, sourceVolumeName, nil)
		if err != nil {
			if !isLogs {
				return fmt.Errorf(`Failed to umount storage volume "%s/%s": %w`, sourcePool, sourceVolumeName, err)
			}

			logger.Warn("Unable to unmount logs storage, daemon restart required")
		}

		return nil
	}

	// Parse the target.
	poolName, volumeName, err := daemonStorageSplitVolume(target)
	if err != nil {
		return err
	}

	pool, err := storagePools.LoadByName(s, poolName)
	if err != nil {
		return err
	}

	// Mount volume.
	_, err = pool.MountCustomVolume(api.ProjectDefaultName, volumeName, nil)
	if err != nil {
		return fmt.Errorf("Failed to mount storage volume %q: %w", target, err)
	}

	// Set ownership & mode.
	volStorageName := project.StorageVolume(api.ProjectDefaultName, volumeName)
	mountpoint := storageDrivers.GetVolumeMountPath(poolName, storageDrivers.VolumeTypeCustom, volStorageName)
	destPath = mountpoint

	err = os.Chmod(mountpoint, 0o700)
	if err != nil {
		return fmt.Errorf("Failed to set permissions on %q: %w", mountpoint, err)
	}

	err = os.Chown(mountpoint, 0, 0)
	if err != nil {
		return fmt.Errorf("Failed to set ownership on %q: %w", mountpoint, err)
	}

	// Handle changes.
	if sourcePath != internalUtil.VarPath(storageType) {
		// Remove the symlink.
		err := os.Remove(internalUtil.VarPath(storageType))
		if err != nil {
			return fmt.Errorf("Failed to remove the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
		}

		// Create the new symlink.
		err = os.Symlink(destPath, internalUtil.VarPath(storageType))
		if err != nil {
			return fmt.Errorf("Failed to create the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
		}

		// Move the data across.
		if isLogs {
			err = moveInstanceDirs(sourcePath, destPath)
			if err != nil {
				return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
			}
		} else {
			err = moveContent(sourcePath, destPath)
			if err != nil {
				return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
			}
		}

		pool, err := storagePools.LoadByName(s, sourcePool)
		if err != nil {
			return err
		}

		// Unmount old volume.
		projectName, sourceVolumeName := project.StorageVolumeParts(sourceVolume)
		_, err = pool.UnmountCustomVolume(projectName, sourceVolumeName, nil)
		if err != nil {
			if !isLogs {
				return fmt.Errorf(`Failed to umount storage volume "%s/%s": %w`, sourcePool, sourceVolumeName, err)
			}

			logger.Warn("Unable to unmount logs storage, daemon restart required")
		}

		return nil
	}

	// Rename the existing storage.
	if isLogs && os.Getenv("INCUS_DIR") == "" {
		sourcePath = "/var/log/incus"
	} else if util.PathExists(internalUtil.VarPath(storageType)) {
		sourcePath = internalUtil.VarPath(storageType) + ".temp"

		err = os.Rename(internalUtil.VarPath(storageType), sourcePath)
		if err != nil {
			return fmt.Errorf("Failed to rename existing storage %q: %w", internalUtil.VarPath(storageType), err)
		}
	}

	// Create the new symlink.
	err = os.Symlink(destPath, internalUtil.VarPath(storageType))
	if err != nil {
		return fmt.Errorf("Failed to create the new symlink at %q: %w", internalUtil.VarPath(storageType), err)
	}

	// Move the data across.
	if isLogs {
		err = moveInstanceDirs(sourcePath, destPath)
		if err != nil {
			return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
		}
	} else {
		err = moveContent(sourcePath, destPath)
		if err != nil {
			return fmt.Errorf("Failed to move data over to directory %q: %w", destPath, err)
		}
	}

	// Remove the old data.
	if sourcePath != "/var/log/incus" {
		err = os.RemoveAll(sourcePath)
		if err != nil {
			return fmt.Errorf("Failed to cleanup old directory %q: %w", sourcePath, err)
		}
	}

	return nil
}
