// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package mount

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"sort"
	"strings"
	"time"

	"golang.org/x/sys/unix"
)

func unmountWithTimeout(target string, flags int, timeout time.Duration) error {
	errCh := make(chan error, 1)

	go func() {
		errCh <- unix.Unmount(target, flags)
	}()

	timer := time.NewTimer(timeout)
	defer timer.Stop()

	select {
	case <-timer.C:
		return fmt.Errorf("unmounting %s timed out after %s", target, timeout)
	case err := <-errCh:
		return err
	}
}

// UnmountAll attempts to unmount all the mounted filesystems via "self" mountinfo.
//
//nolint:gocyclo
func UnmountAll() error {
	// timeout in seconds
	const timeout = 10

	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	for iteration := range timeout {
		mounts, err := readMountInfo()
		if err != nil {
			return err
		}

		failedUnmounts := 0

		var failedMountPoints []string

		for _, mountInfo := range mounts {
			if mountInfo.MountPoint == "" {
				continue
			}

			if strings.HasPrefix(mountInfo.MountSource, "/dev/") {
				err = unmountWithTimeout(mountInfo.MountPoint, 0, time.Second)
				if err == nil {
					log.Printf("unmounted %s (%s)", mountInfo.MountPoint, mountInfo.MountSource)
				} else {
					log.Printf("failed unmounting %s: %s", mountInfo.MountPoint, err)

					failedUnmounts++

					failedMountPoints = append(failedMountPoints, mountInfo.MountPoint)
				}
			}
		}

		if failedUnmounts == 0 {
			break
		}

		// Log mount users on first and last failure to help diagnose busy mounts.
		if iteration == 0 || iteration == timeout-1 {
			for _, mp := range failedMountPoints {
				logMountUsers(log.Printf, mp)
			}
		}

		log.Printf("retrying %d unmount operations...", failedUnmounts)

		<-ticker.C
	}

	return nil
}

type mountInfo struct {
	MountPoint  string
	MountSource string
}

func readMountInfo() ([]mountInfo, error) {
	f, err := os.Open("/proc/self/mountinfo")
	if err != nil {
		return nil, err
	}

	defer f.Close() //nolint:errcheck

	var mounts []mountInfo

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()

		parts := strings.SplitN(line, " - ", 2)

		if len(parts) < 2 {
			continue
		}

		var mntInfo mountInfo

		pre := strings.Fields(parts[0])
		post := strings.Fields(parts[1])

		if len(pre) >= 5 {
			mntInfo.MountPoint = pre[4]
		}

		if len(post) >= 2 {
			mntInfo.MountSource = post[1]
		}

		mounts = append(mounts, mntInfo)
	}

	return mounts, scanner.Err()
}

func getSubmounts(target string) ([]string, error) {
	mounts, err := readMountInfo()
	if err != nil {
		return nil, err
	}

	var submounts []string

	for _, mnt := range mounts {
		if mnt.MountPoint == target {
			continue
		}

		if strings.HasPrefix(mnt.MountPoint, target+"/") {
			submounts = append(submounts, mnt.MountPoint)
		}
	}

	sort.Slice(submounts, func(i, j int) bool {
		return len(submounts[i]) > len(submounts[j])
	})

	return submounts, nil
}
