// criu.go provides shared CRIU utilities used by both checkpoint and restore.
package common

import (
	"bufio"
	"fmt"
	"os"
	"strings"

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

// OpenPathForCRIU opens a path (directory or file) and clears the CLOEXEC flag
// so the FD can be inherited by CRIU child processes.
// Returns the opened file and its FD. Caller must close the file when done.
func OpenPathForCRIU(path string) (*os.File, int32, error) {
	dir, err := os.Open(path)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to open %s: %w", path, err)
	}

	// Clear CLOEXEC so the FD is inherited by CRIU child process.
	// Go's os.Open() sets O_CLOEXEC by default, but go-criu's swrk mode
	// requires the FD to be inherited.
	if _, err := unix.FcntlInt(dir.Fd(), unix.F_SETFD, 0); err != nil {
		dir.Close()
		return nil, 0, fmt.Errorf("failed to clear CLOEXEC on %s: %w", path, err)
	}

	return dir, int32(dir.Fd()), nil
}

// CRIUMountPoint represents a parsed mount point from /proc/pid/mountinfo.
type CRIUMountPoint struct {
	MountID   string // Mount ID
	ParentID  string // Parent mount ID
	Path      string // Mount point path (container-side)
	Root      string // Root within filesystem (host-side for bind mounts)
	FSType    string // Filesystem type
	Source    string // Mount source
	Options   string // Mount options
	SuperOpts string // Super block options
}

// ParseMountInfoFile parses a mountinfo file and returns all mount points.
// This is used by both checkpoint (to mark mounts as external) and
// restore (to generate external mount mappings).
func ParseMountInfoFile(path string) ([]CRIUMountPoint, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("failed to open mountinfo: %w", err)
	}
	defer file.Close()

	var mounts []CRIUMountPoint
	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		mount, err := parseCRIUMountInfoLine(scanner.Text())
		if err != nil {
			continue // Skip malformed lines
		}
		mounts = append(mounts, mount)
	}

	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("error reading mountinfo: %w", err)
	}

	return mounts, nil
}

// GetMountPointPaths returns just the mount point paths from a mountinfo file.
// This is a convenience function when you only need the paths.
func GetMountPointPaths(path string) ([]string, error) {
	mounts, err := ParseMountInfoFile(path)
	if err != nil {
		return nil, err
	}

	paths := make([]string, 0, len(mounts))
	for _, m := range mounts {
		paths = append(paths, m.Path)
	}
	return paths, nil
}

// parseCRIUMountInfoLine parses a single line from mountinfo.
// mountinfo format:
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// (1)(2)(3)   (4)   (5)      (6)     (7)   (8) (9)   (10)         (11)
func parseCRIUMountInfoLine(line string) (CRIUMountPoint, error) {
	fields := strings.Fields(line)
	if len(fields) < 10 {
		return CRIUMountPoint{}, fmt.Errorf("malformed mountinfo line")
	}

	// Find separator (-) to get fstype and source
	sepIdx := -1
	for i, f := range fields {
		if f == "-" {
			sepIdx = i
			break
		}
	}

	if sepIdx == -1 || sepIdx+2 >= len(fields) {
		return CRIUMountPoint{}, fmt.Errorf("malformed mountinfo line (no separator)")
	}

	superOpts := ""
	if sepIdx+3 < len(fields) {
		superOpts = fields[sepIdx+3]
	}

	return CRIUMountPoint{
		MountID:   fields[0],
		ParentID:  fields[1],
		Root:      fields[3],
		Path:      fields[4],
		Options:   fields[5],
		FSType:    fields[sepIdx+1],
		Source:    fields[sepIdx+2],
		SuperOpts: superOpts,
	}, nil
}
