// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2014-2016 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package backend

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/snapcore/snapd/boot"
	"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/kernel"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/progress"
	"github.com/snapcore/snapd/release"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/systemd"
)

var kernelEnsureKernelDriversTree = kernel.EnsureKernelDriversTree

// InstallRecord keeps a record of what installation effectively did as hints
// about what needs to be undone in case of failure.
type InstallRecord struct {
	// TargetSnapExisted indicates that the target .snap file under /var/lib/snapd/snap already existed when the
	// backend attempted SetupSnap() through squashfs Install() and should be kept.
	TargetSnapExisted bool `json:"target-snap-existed,omitempty"`
}

type SetupSnapOptions struct {
	SkipKernelExtraction bool
}

// SetupSnap does prepare and mount the snap for further processing.
func (b Backend) SetupSnap(snapFilePath, instanceName string, sideInfo *snap.SideInfo, dev snap.Device, setupOpts *SetupSnapOptions, meter progress.Meter) (snapType snap.Type, installRecord *InstallRecord, retErr error) {
	if setupOpts == nil {
		setupOpts = &SetupSnapOptions{}
	}

	// This assumes that the snap was already verified or --dangerous was used.

	s, snapf, oErr := OpenSnapFile(snapFilePath, sideInfo)
	if oErr != nil {
		return snapType, nil, oErr
	}

	// update instance key to what was requested
	_, s.InstanceKey = snap.SplitInstanceName(instanceName)

	instdir := s.MountDir()

	defer func() {
		if retErr == nil {
			return
		}

		// this may remove the snap from /var/lib/snapd/snaps depending on installRecord
		if e := b.RemoveSnapFiles(s, s.Type(), installRecord, dev, meter); e != nil {
			meter.Notify(fmt.Sprintf("while trying to clean up due to previous failure: %v", e))
		}
	}()

	if err := os.MkdirAll(instdir, 0755); err != nil {
		return snapType, nil, err
	}

	// for consistency, since we created an instance directory, let's create
	// the shared snap prefix directory as well; even if the directory is
	// removed during removal of snap sharing the same name, linking the
	// current instance will ensure it exists
	if err := createSharedSnapDirForParallelInstance(s); err != nil {
		return snapType, nil, err
	}

	// in uc20+ and classic with modes run mode, all snaps must be on the
	// same device
	opts := &snap.InstallOptions{}
	if dev.HasModeenv() && dev.RunMode() {
		opts.MustNotCrossDevices = true
	}

	didNothing, err := snapf.Install(s.MountFile(), instdir, opts)
	if err != nil {
		return snapType, nil, err
	}

	// generate the mount unit for the squashfs
	t := s.Type()
	mountFlags := systemd.EnsureMountUnitFlags{
		PreventRestartIfModified: false,
		// We need early mounts only for UC20+/hybrid, also 16.04
		// systemd seems to be buggy if we enable this.
		StartBeforeDriversLoad: t == snap.TypeKernel && dev.HasModeenv(),
	}
	if err := addMountUnit(s, mountFlags, newSystemd(b.preseed, meter)); err != nil {
		return snapType, nil, err
	}

	if !setupOpts.SkipKernelExtraction {
		if err := boot.Kernel(s, t, dev).ExtractKernelAssets(snapf); err != nil {
			return snapType, nil, fmt.Errorf("cannot install kernel: %s", err)
		}
	}

	installRecord = &InstallRecord{TargetSnapExisted: didNothing}
	return t, installRecord, nil
}

// SetupKernelSnap does extra configuration for kernel snaps.
func (b Backend) SetupKernelSnap(instanceName string, rev snap.Revision, meter progress.Meter) (err error) {
	// Build kernel tree that will be mounted from initramfs
	cpi := snap.MinimalSnapContainerPlaceInfo(instanceName, rev)
	destDir := kernel.DriversTreeDir(dirs.GlobalRootDir, instanceName, rev)

	// TODO:COMPS: consider components when installed jointly
	return kernelEnsureKernelDriversTree(
		kernel.MountPoints{
			Current: cpi.MountDir(),
			Target:  cpi.MountDir()},
		nil, destDir,
		&kernel.KernelDriversTreeOptions{KernelInstall: true})
}

func (b Backend) RemoveKernelSnapSetup(instanceName string, rev snap.Revision, meter progress.Meter) error {
	kernelTree := kernel.DriversTreeDir(dirs.GlobalRootDir, instanceName, rev)
	return kernel.RemoveKernelDriversTree(kernelTree)
}

// SetupComponent prepares and mounts a component for further processing.
func (b Backend) SetupComponent(compFilePath string, compPi snap.ContainerPlaceInfo, dev snap.Device, meter progress.Meter) (installRecord *InstallRecord, retErr error) {
	// This assumes that the component was already verified or --dangerous was used.

	compInfo, snapf, oErr := OpenComponentFile(compFilePath, nil, nil)
	if oErr != nil {
		return nil, oErr
	}

	defer func() {
		if retErr == nil {
			return
		}

		// this may remove the component from /var/lib/snapd/snaps
		// depending on installRecord
		if e := b.RemoveComponentFiles(compPi, installRecord, dev,
			RemoveComponentOpts{MaybeInitramfsMounted: false}, meter); e != nil {
			meter.Notify(fmt.Sprintf(
				"while trying to clean up due to previous failure: %v", e))
		}
	}()

	// Create mount dir for the component
	mntDir := compPi.MountDir()
	if err := os.MkdirAll(mntDir, 0755); err != nil {
		return nil, err
	}

	// in uc20+ and classic with modes run mode, all snaps must be on the
	// same device
	opts := &snap.InstallOptions{}
	if dev.HasModeenv() && dev.RunMode() {
		opts.MustNotCrossDevices = true
	}

	// Copy file to snaps folder
	didNothing, err := snapf.Install(compPi.MountFile(), mntDir, opts)
	if err != nil {
		return nil, err
	}

	// generate the mount unit for the squashfs
	mountFlags := systemd.EnsureMountUnitFlags{
		PreventRestartIfModified: false,
		// We need early mounts only for UC20+/hybrid, also 16.04
		// systemd seems to be buggy if we enable this.
		StartBeforeDriversLoad: compInfo.Type == snap.KernelModulesComponent && dev.HasModeenv(),
	}
	if err := addMountUnit(compPi, mountFlags, newSystemd(b.preseed, meter)); err != nil {
		return nil, err
	}

	installRecord = &InstallRecord{TargetSnapExisted: didNothing}
	return installRecord, nil
}

// RemoveSnapFiles removes the snap files from the disk after unmounting the snap.
func (b Backend) RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, installRecord *InstallRecord, dev snap.Device, meter progress.Meter) error {
	mountDir := s.MountDir()

	// this also ensures that the mount unit stops
	if err := removeMountUnit(mountDir, meter); err != nil {
		return err
	}

	if err := os.RemoveAll(mountDir); err != nil {
		return err
	}

	// snapPath may either be a file or a (broken) symlink to a dir
	snapPath := s.MountFile()
	if _, err := os.Lstat(snapPath); err == nil {
		// remove the kernel assets (if any)
		if err := boot.Kernel(s, typ, dev).RemoveKernelAssets(); err != nil {
			return err
		}

		// don't remove snap path if it existed before snap installation was attempted
		// and is a symlink, which is the case with kernel/core snaps during seeding.
		keepSeededSnap := installRecord != nil && installRecord.TargetSnapExisted && osutil.IsSymlink(snapPath)
		if !keepSeededSnap {
			// remove the snap
			if err := os.RemoveAll(snapPath); err != nil {
				return err
			}
		}
	}

	return nil
}

// RemoveComponentOpts are options considered when removing a component.
type RemoveComponentOpts struct {
	// MaybeInitramfsMounted is set if the component was mounted also from
	// the initramfs, which can be the case for kernel-modules components.
	MaybeInitramfsMounted bool
}

// RemoveComponentFiles unmounts and removes component files from the disk.
func (b Backend) RemoveComponentFiles(cpi snap.ContainerPlaceInfo, installRecord *InstallRecord, dev snap.Device, opts RemoveComponentOpts, meter progress.Meter) error {
	if opts.MaybeInitramfsMounted {
		// Stop duplicated mounts created from initramfs for kernel-modules components, if
		// existing (if we have not rebooted after installation these will not be exist):
		//
		// - On UC there is a mount under /writable/system-data, as the "snap" directory
		//   there is bind mounted later to /. There is a unit file created by the
		//   initramfs, but it has a "sysroot-" prefix to the real mount path, so systemd
		//   does not consider it associated with the mount. This unit file is inactive
		//   therefore. We leave this file as it is, it will disappear in next reboot and it
		//   would be a waste to remove it and do a daemon-reload.
		//
		// - On hybrid the initramfs will create the mount already in /snap, however, there
		//   will be a mount in /run/mnt/data as / is bind-mounted there and that mount is
		//   not marked private, so mount events in / will leak.
		extraMountRoot := dirs.WritableUbuntuCoreSystemDataDir
		if release.OnClassic {
			extraMountRoot = boot.InitramfsDataDir
		}
		mntPoint := filepath.Join(extraMountRoot, dirs.StripRootDir(cpi.MountDir()))
		isMounted, err := osutil.IsMounted(mntPoint)
		if err != nil {
			return err
		}
		if isMounted {
			// TODO we handle (un)mounts in different ways in different places
			// (direct syscalls or (u)mount commands). We need to unify this
			// eventually.
			if output, err := exec.Command("umount", "--lazy", mntPoint).
				CombinedOutput(); err != nil {
				return osutil.OutputErr(output, err)
			}
		}
	}

	// this also ensures that the mount unit stops
	if err := removeMountUnit(cpi.MountDir(), meter); err != nil {
		return err
	}

	// Remove /snap/<snap_instance>/components/mnt/<comp_name>/<comp_rev>
	if err := os.RemoveAll(cpi.MountDir()); err != nil {
		return err
	}

	compFilePath := cpi.MountFile()
	if _, err := os.Lstat(compFilePath); err == nil {
		// remove the component
		if err := os.RemoveAll(compFilePath); err != nil {
			return err
		}
	}

	return nil
}

func (b Backend) RemoveSnapDir(s snap.PlaceInfo, hasOtherInstances bool) error {
	mountDir := s.MountDir()

	_, instanceKey := snap.SplitInstanceName(s.InstanceName())
	if instanceKey != "" {
		// always ok to remove instance specific one, failure to remove
		// is ok, there may be other revisions
		os.Remove(filepath.Dir(mountDir))
	}
	if !hasOtherInstances {
		// remove only if not used by other instances of the same snap,
		// failure to remove is ok, there may be other revisions
		os.Remove(snap.BaseDir(s.SnapName()))
	}
	return nil
}

func (b Backend) RemoveComponentDir(cpi snap.ContainerPlaceInfo) error {
	compMountDir := cpi.MountDir()
	// Remove last 3 directories of
	// /snap/<snap_instance>/components/mnt/<comp_name>/ if they
	// are empty (last one should be). Note that subdirectories with snap
	// revisions are handled by UnlinkComponent.
	for i := 0; i < 3; i++ {
		compMountDir = filepath.Dir(compMountDir)
		if err := os.Remove(compMountDir); err != nil {
			break
		}
	}

	return nil
}

// UndoSetupSnap undoes the work of SetupSnap using RemoveSnapFiles.
func (b Backend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *InstallRecord, dev snap.Device, meter progress.Meter) error {
	return b.RemoveSnapFiles(s, typ, installRecord, dev, meter)
}

// UndoSetupComponent undoes the work of SetupComponent using RemoveComponentFiles.
func (b Backend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, installRecord *InstallRecord, dev snap.Device, removeOpts RemoveComponentOpts, meter progress.Meter) error {
	return b.RemoveComponentFiles(cpi, installRecord, dev, removeOpts, meter)
}

// RemoveSnapInhibitLock removes the file controlling inhibition of "snap run".
func (b Backend) RemoveSnapInhibitLock(instanceName string, stateUnlocker runinhibit.Unlocker) error {
	if stateUnlocker == nil {
		return errors.New("internal error: stateUnlocker cannot be nil")
	}
	return runinhibit.RemoveLockFile(instanceName, stateUnlocker)
}

// SetupKernelModulesComponents changes kernel-modules configuration by
// installing currentComps. The components currently active are currentComps,
// while ksnapName and ksnapRev identify the currently active kernel.
func (b Backend) SetupKernelModulesComponents(currentComps, finalComps []*snap.ComponentSideInfo, ksnapName string, ksnapRev snap.Revision, meter progress.Meter) (err error) {
	return moveKModsComponentsState(
		currentComps, finalComps, ksnapName, ksnapRev,
		"after failure to set-up kernel modules components")
}

// moveKModsComponentsState changes kernel-modules set-up from currentComps to
// finalComps, for the kernel/revision specified by ksnapName/ksnapRev.
func moveKModsComponentsState(currentComps, finalComps []*snap.ComponentSideInfo, ksnapName string, ksnapRev snap.Revision, cleanErrMsg string) (err error) {
	cpi := snap.MinimalSnapContainerPlaceInfo(ksnapName, ksnapRev)
	kMntPts := kernel.MountPoints{
		Current: cpi.MountDir(),
		Target:  cpi.MountDir(),
	}
	destDir := kernel.DriversTreeDir(dirs.GlobalRootDir, ksnapName, ksnapRev)
	kinfo, err := kernel.ReadInfo(kMntPts.Current)
	if err != nil {
		return err
	}
	finalCompsMntPts := compsMountPoints(finalComps, ksnapName, ksnapRev, kinfo)

	if err := kernelEnsureKernelDriversTree(kMntPts, finalCompsMntPts, destDir,
		&kernel.KernelDriversTreeOptions{KernelInstall: false}); err != nil {

		// Revert change on error
		currentCompsMntPts := compsMountPoints(currentComps, ksnapName, ksnapRev, kinfo)
		if e := kernelEnsureKernelDriversTree(kMntPts, currentCompsMntPts, destDir,
			&kernel.KernelDriversTreeOptions{KernelInstall: false}); e != nil {
			logger.Noticef("while restoring kernel tree %s: %v", cleanErrMsg, e)
		}

		return err
	}

	return nil
}

func compsMountPoints(comps []*snap.ComponentSideInfo, kSnapName string, ksnapRev snap.Revision, ki *kernel.Info) []kernel.ModulesCompMountPoints {
	compsMntPts := make([]kernel.ModulesCompMountPoints, 0, len(comps)+1)
	for _, csi := range comps {
		compPlaceInfo := snap.MinimalComponentContainerPlaceInfo(csi.Component.ComponentName,
			csi.Revision, kSnapName)
		if dirHasDrivers(compPlaceInfo.MountDir()) {
			compsMntPts = append(compsMntPts, kernel.ModulesCompMountPoints{
				LinkName: csi.Component.ComponentName,
				MountPoints: kernel.MountPoints{
					Current: compPlaceInfo.MountDir(),
					Target:  compPlaceInfo.MountDir(),
				}})
		}
	}

	// The kernel might generate dynamically modules, check
	if dynDir := ki.DynamicModulesDir(kSnapName, ksnapRev); dynDir != "" {
		if dirHasDrivers(dynDir) {
			logger.Noticef("setup: modules for %s", dynDir)
			compsMntPts = append(compsMntPts, kernel.ModulesCompMountPoints{
				LinkName: kSnapName + "_dyn",
				MountPoints: kernel.MountPoints{
					Current: dynDir,
					Target:  dynDir,
				}})
		}
	}

	return compsMntPts
}

func dirHasDrivers(dir string) bool {
	modExists, modIsDir, _ := osutil.DirExists(filepath.Join(dir, "modules"))
	fwExists, fwIsDir, _ := osutil.DirExists(filepath.Join(dir, "firmware"))
	return (modExists && modIsDir) || (fwExists && fwIsDir)
}

func newSystemd(preseed bool, meter progress.Meter) systemd.Systemd {
	if preseed {
		return systemd.NewEmulationMode(dirs.GlobalRootDir)
	}
	return systemd.New(systemd.SystemMode, meter)
}
