package main

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"
)

var ErrCancelled = errors.New("cancelled")

type UserManager interface {
	UserExists(name string) bool
}

type CommandRunner interface {
	Run(name string, args ...string) error
}

type LookPathFunc func(file string) (string, error)

type RealUserManager struct{}

func (RealUserManager) UserExists(name string) bool {
	_, err := user.Lookup(name)
	return err == nil
}

type OSCommandRunner struct{}

func (OSCommandRunner) Run(name string, args ...string) error {
	cmd := exec.Command(name, args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

type App struct {
	configPath string
	fstabPath  string
	users      UserManager
	runner     CommandRunner
	lookPath   LookPathFunc
	input      io.Reader
	output     io.Writer
	writer     *bufio.Writer
	theme      tuiTheme
}

type PasswdEntry struct {
	Name  string
	UID   int
	GID   int
	Home  string
	Shell string
}

type CIFSMountConfig struct {
	Server     string
	Share      string
	MountPoint string
	Username   string
	Password   string
	Domain     string
	UID        string
	GID        string
	FileMode   string
	DirMode    string
	AutoMount  bool
	ReadOnly   bool
}

type FstabMountEntry struct {
	LineIndex   int
	Source      string
	MountPoint  string
	FSType      string
	Options     []string
	Dump        string
	Pass        string
	RawLine     string
	DisplayName string
}

func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App {
	return &App{
		configPath: configPath,
		fstabPath:  "/etc/fstab",
		users:      users,
		runner:     runner,
		lookPath:   lookPath,
		input:      os.Stdin,
		output:     os.Stdout,
		writer:     bufio.NewWriter(os.Stdout),
		theme:      newTUITheme(),
	}
}

func (a *App) Run() error {
	for {
		choice, err := a.chooseStartupWorkflow()
		if err != nil {
			return err
		}

		switch choice {
		case "server":
			return a.runServerWorkflow()
		case "client":
			return a.setupClientMount()
		case "quit":
			return nil
		}
	}
}

func (a *App) chooseStartupWorkflow() (string, error) {
	return a.chooseMenu(
		"Choose A Workflow",
		"Server shares and client mounts in one place.",
		[]string{
			"Use the server tools to edit smb.conf and manage share accounts.",
			"Use the client tools to add or maintain CIFS mounts in /etc/fstab.",
		},
		[]menuOption{
			{Key: "s", Value: "server", Label: "Server shares"},
			{Key: "c", Value: "client", Label: "Client mounts"},
			{Key: "q", Value: "quit", Label: "Quit"},
		},
	)
}

func (a *App) runServerWorkflow() error {
	doc, err := ParseConfigFile(a.configPath)
	if err != nil {
		if os.IsNotExist(err) {
			doc, err = a.handleMissingConfig()
			if err == nil {
				goto loaded
			}
		}

		return fmt.Errorf("read config: %w", err)
	}

loaded:
	for {
		shareSections := doc.ShareSections()
		intro := []string{"Current shares:"}
		if len(shareSections) == 0 {
			intro = append(intro, "  none yet")
		} else {
			for _, section := range shareSections {
				cfg := ShareFromSection(section)
				intro = append(intro, fmt.Sprintf("  %s -> %s", cfg.Name, cfg.Path))
			}
		}

		choice, err := a.chooseMenu(
			"Server Share Editor",
			fmt.Sprintf("Working against %s", a.configPath),
			intro,
			[]menuOption{
				{Key: "a", Value: "add", Label: "Add share"},
				{Key: "e", Value: "edit", Label: "Edit share"},
				{Key: "d", Value: "delete", Label: "Delete share"},
				{Key: "m", Value: "mount", Label: "Client mounts"},
				{Key: "u", Value: "users", Label: "Users"},
				{Key: "w", Value: "write", Label: "Write and exit"},
				{Key: "q", Value: "quit", Label: "Quit"},
			},
		)
		if err != nil {
			if errors.Is(err, ErrCancelled) {
				return nil
			}
			return err
		}

		switch choice {
		case "add":
			if err := a.addShare(doc); err != nil {
				return err
			}
		case "edit":
			if err := a.editShare(doc); err != nil {
				return err
			}
		case "delete":
			if err := a.deleteShare(doc); err != nil {
				return err
			}
		case "mount":
			if err := a.setupClientMount(); err != nil {
				return err
			}
		case "users":
			if err := a.manageUsers(doc); err != nil {
				return err
			}
		case "write":
			return a.writeConfig(doc)
		case "quit":
			return nil
		}
	}
}

func (a *App) setupClientMount() error {
	for {
		entries, err := a.loadCIFSMountEntries()
		if err != nil {
			return err
		}

		intro := []string{
			"Remote shares are persisted in /etc/fstab so they can mount cleanly on boot.",
			"Current client mounts:",
		}
		if len(entries) == 0 {
			intro = append(intro, "  none yet")
		} else {
			for _, entry := range entries {
				intro = append(intro, fmt.Sprintf("  %s -> %s", entry.DisplayName, entry.MountPoint))
			}
		}

		choice, err := a.chooseMenu(
			"Client Mount Setup",
			fmt.Sprintf("Editing %s", a.fstabPath),
			intro,
			[]menuOption{
				{Key: "a", Value: "add", Label: "Add mount"},
				{Key: "e", Value: "edit", Label: "Edit mount"},
				{Key: "d", Value: "delete", Label: "Delete mount"},
				{Key: "b", Value: "back", Label: "Back"},
			},
		)
		if err != nil {
			if errors.Is(err, ErrCancelled) {
				return nil
			}
			return err
		}

		switch choice {
		case "add":
			if err := a.addClientMount(); err != nil {
				return err
			}
		case "edit":
			if err := a.editClientMount(entries); err != nil {
				return err
			}
		case "delete":
			if err := a.deleteClientMount(entries); err != nil {
				return err
			}
		case "back":
			return nil
		}
	}
}

func (a *App) manageUsers(doc *Document) error {
	for {
		choice, err := a.chooseMenu(
			"User Management",
			"Keep local Linux accounts and Samba credentials aligned with your shares.",
			[]string{
				fmt.Sprintf("%d share accounts referenced in the current config.", len(shareUserReferences(doc))),
			},
			[]menuOption{
				{Key: "c", Value: "check", Label: "Check accounts"},
				{Key: "p", Value: "password", Label: "Change password"},
				{Key: "x", Value: "delete", Label: "Delete account"},
				{Key: "b", Value: "back", Label: "Back"},
			},
		)
		if err != nil {
			if errors.Is(err, ErrCancelled) {
				return nil
			}
			return err
		}

		switch choice {
		case "check":
			if err := a.checkShareAccounts(doc); err != nil {
				return err
			}
		case "password":
			if err := a.changeSambaPassword(doc); err != nil {
				return err
			}
		case "delete":
			if err := a.deleteUnusedAccount(doc); err != nil {
				return err
			}
		case "back":
			return nil
		}
	}
}

func (a *App) checkShareAccounts(doc *Document) error {
	references := shareUserReferences(doc)
	if len(references) == 0 {
		a.showMessage("info", "No accounts are listed in any share.")
		return nil
	}

	a.showPanel("Accounts Used By Current Shares", "", nil)
	for _, ref := range references {
		status := "missing"
		if a.users.UserExists(ref.User) {
			status = "present"
		}
		a.printf("  %s [%s] used by: %s\n", ref.User, status, strings.Join(ref.Shares, ", "))
	}

	var missing []string
	for _, ref := range references {
		if !a.users.UserExists(ref.User) {
			missing = append(missing, ref.User)
		}
	}

	if len(missing) == 0 {
		a.showMessage("success", "All listed share accounts already exist.")
		return nil
	}

	a.showMessage("warn", "Some accounts are missing and those users may not be able to sign in.")
	if err := a.ensureUsers(missing); err != nil {
		return err
	}
	return nil
}

func (a *App) deleteUnusedAccount(doc *Document) error {
	entries, err := readPasswdEntries()
	if err != nil {
		return fmt.Errorf("read local users: %w", err)
	}

	candidates := unusedAccountCandidates(entries, shareUsers(doc))
	if len(candidates) == 0 {
		a.showPanel("Unused Account Cleanup", "No obvious removable share accounts were found.", []string{
			"For safety, system accounts are excluded from this list.",
		})
		return nil
	}

	options := make([]menuOption, 0, len(candidates)+1)
	for i, entry := range candidates {
		options = append(options, menuOption{
			Key:         fmt.Sprintf("%d", i+1),
			Value:       entry.Name,
			Label:       entry.Name,
			Description: "shell: " + strings.TrimSpace(entry.Shell),
		})
	}
	options = append(options, menuOption{Key: "b", Value: "", Label: "Back"})

	choice, err := a.chooseMenu(
		"Delete An Unused Account",
		"Only accounts that are not listed in any share are shown here.",
		nil,
		options,
	)
	if err != nil {
		return err
	}
	if choice == "" {
		return nil
	}

	a.showPanel("Delete Account", "", []string{
		fmt.Sprintf("This will remove the local account %q.", choice),
		"This is only safe if nobody needs this account for Samba or anything else.",
	})

	confirm, err := a.confirm("Delete this account now", false)
	if err != nil {
		return err
	}
	if !confirm {
		a.showMessage("info", "Delete cancelled.")
		return nil
	}

	if err := a.deleteUser(choice); err != nil {
		return err
	}

	a.showMessage("success", "Deleted account "+choice)
	return nil
}

func (a *App) changeSambaPassword(doc *Document) error {
	if a.lookPath != nil {
		if _, err := a.lookPath("smbpasswd"); err != nil {
			a.println("I couldn't find the smbpasswd tool yet.")
			a.println("Install Samba first, then try this again.")
			return nil
		}
	}

	references := shareUserReferences(doc)
	if len(references) == 0 {
		a.showMessage("info", "No accounts are listed in any share.")
		return nil
	}

	options := make([]menuOption, 0, len(references)+1)
	for i, ref := range references {
		status := "missing"
		if a.users.UserExists(ref.User) {
			status = "present"
		}
		options = append(options, menuOption{
			Key:         fmt.Sprintf("%d", i+1),
			Value:       ref.User,
			Label:       ref.User + " [" + status + "]",
			Description: "used by: " + strings.Join(ref.Shares, ", "),
		})
	}
	options = append(options, menuOption{Key: "b", Value: "", Label: "Back"})

	choice, err := a.chooseMenu(
		"Choose An Account",
		"Select the local user whose Samba password you want to change.",
		nil,
		options,
	)
	if err != nil {
		return err
	}
	if choice == "" {
		return nil
	}

	var ref ShareUserReference
	for _, candidate := range references {
		if candidate.User == choice {
			ref = candidate
			break
		}
	}
	if !a.users.UserExists(ref.User) {
		a.showPanel("Local User Missing", "", []string{
			fmt.Sprintf("The local account %q does not exist yet.", ref.User),
			"I can create it first so a Samba password can be set.",
		})

		create, promptErr := a.confirm("Create this account now", true)
		if promptErr != nil {
			return promptErr
		}
		if !create {
			return nil
		}

		if err := a.createUser(ref.User); err != nil {
			return err
		}
	}

	a.showPanel("Password Update", "", []string{
		fmt.Sprintf("I will open the password setup for %q now.", ref.User),
		"You will be asked to type the new Samba password.",
	})
	return a.setSambaPassword(ref.User)
}

func (a *App) handleMissingConfig() (*Document, error) {
	a.println("")
	a.println("Samba is not set up yet on this computer.")
	a.printf("I couldn't find the Samba config file at %s.\n", a.configPath)

	plan, ok := DetectSambaInstallPlan(a.lookPath, os.Geteuid() == 0)
	if !ok {
		a.println("I also couldn't find a supported package manager automatically.")
		a.println("Install the Samba server package for your Linux distribution, then run this tool again.")
		a.flush()
		return nil, os.ErrNotExist
	}

	a.println("")
	a.printf("I found %s and can try to install the Samba server for you.\n", plan.ManagerName)
	a.printf("This would run: %s\n", plan.DisplayCommand())
	a.println("You may be asked for your administrator password.")
	a.flush()

	install, err := a.confirm("Install Samba server now", true)
	if err != nil {
		return nil, err
	}
	if !install {
		return nil, os.ErrNotExist
	}

	if err := a.runner.Run(plan.Command[0], plan.Command[1:]...); err != nil {
		return nil, fmt.Errorf("install Samba with %s: %w", plan.ManagerName, err)
	}

	doc, err := ParseConfigFile(a.configPath)
	if err != nil {
		return nil, fmt.Errorf("Samba installation finished, but the config file is still missing at %s: %w", a.configPath, err)
	}

	a.println("Samba was installed and the config file is now available.")
	a.flush()
	return doc, nil
}

func (a *App) addShare(doc *Document) error {
	cfg, err := a.collectShareConfig(ShareConfig{})
	if err != nil {
		return err
	}

	if doc.Section(cfg.Name) != nil {
		a.println("A share with that name already exists.")
		return nil
	}

	if err := a.ensureUsers(cfg.ValidUsers); err != nil {
		return err
	}

	doc.UpsertSection(BuildShareSection(nil, cfg))
	a.println("Share added.")
	return nil
}

func (a *App) editShare(doc *Document) error {
	section, err := a.selectShare(doc)
	if err != nil || section == nil {
		return err
	}

	originalName := section.Name
	cfg, err := a.collectShareConfig(ShareFromSection(section))
	if err != nil {
		return err
	}

	if !strings.EqualFold(originalName, cfg.Name) && doc.Section(cfg.Name) != nil {
		a.println("A share with that new name already exists.")
		return nil
	}

	if err := a.ensureUsers(cfg.ValidUsers); err != nil {
		return err
	}

	if !strings.EqualFold(originalName, cfg.Name) {
		doc.DeleteSection(originalName)
	}
	doc.UpsertSection(BuildShareSection(section, cfg))
	a.println("Share updated.")
	return nil
}

func (a *App) deleteShare(doc *Document) error {
	section, err := a.selectShare(doc)
	if err != nil || section == nil {
		return err
	}

	confirm, err := a.confirm(fmt.Sprintf("Delete share %q", section.Name), false)
	if err != nil {
		return err
	}
	if !confirm {
		a.println("Delete cancelled.")
		return nil
	}

	doc.DeleteSection(section.Name)
	a.println("Share deleted.")
	return nil
}

func (a *App) writeConfig(doc *Document) error {
	if err := a.ensureShareDirectories(doc); err != nil {
		return err
	}

	backup := fmt.Sprintf("%s.bak.%s", a.configPath, time.Now().UTC().Format("20060102T150405Z"))
	serialized := doc.Serialize()

	if err := copyFile(a.configPath, backup); err != nil {
		if shouldOfferPrivilegeRetry(err) {
			return a.writeConfigWithPrivilegeRetry(serialized, backup)
		}
		return friendlyWriteError("create backup", backup, err)
	}

	if err := os.WriteFile(a.configPath, []byte(serialized), 0o644); err != nil {
		if shouldOfferPrivilegeRetry(err) {
			return a.writeConfigWithPrivilegeRetry(serialized, backup)
		}
		return friendlyWriteError("write config", a.configPath, err)
	}

	a.printf("Config written to %s\n", a.configPath)
	a.printf("Backup saved to %s\n", backup)
	return nil
}

func (a *App) ensureCIFSUtilsInstalled() error {
	if a.lookPath != nil {
		if _, err := a.lookPath("mount.cifs"); err == nil {
			return nil
		}
	}

	a.println("")
	a.println("This computer does not seem to have the CIFS mount tools installed yet.")

	plan, ok := DetectCIFSUtilsInstallPlan(a.lookPath, os.Geteuid() == 0)
	if !ok {
		a.println("I couldn't find a supported package manager automatically.")
		a.println("Install cifs-utils for your Linux distribution, then try again.")
		return errors.New("cifs-utils is not installed")
	}

	a.printf("I found %s and can try to install the CIFS client tools for you.\n", plan.ManagerName)
	a.printf("This would run: %s\n", plan.DisplayCommand())
	a.flush()

	install, err := a.confirm("Install the CIFS client tools now", true)
	if err != nil {
		return err
	}
	if !install {
		return errors.New("cifs-utils is required for client mounts")
	}

	if err := a.runner.Run(plan.Command[0], plan.Command[1:]...); err != nil {
		return fmt.Errorf("install CIFS client tools with %s: %w", plan.ManagerName, err)
	}

	if a.lookPath != nil {
		if _, err := a.lookPath("mount.cifs"); err != nil {
			return fmt.Errorf("cifs-utils installation finished, but mount.cifs is still not available: %w", err)
		}
	}

	return nil
}

func (a *App) collectCIFSMountConfig(defaults CIFSMountConfig) (CIFSMountConfig, error) {
	server, err := a.promptDefault("Server name or IP", defaults.Server)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	share, err := a.promptDefault("Share name", defaults.Share)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
	if defaults.MountPoint != "" {
		mountPointDefault = defaults.MountPoint
	}
	mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	username, err := a.promptDefault("Remote username", defaults.Username)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	password, err := a.promptDefault("Remote password", defaults.Password)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	domain, err := a.promptDefault("Domain or workgroup", defaults.Domain)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	uid, err := a.promptDefault("Local owner username or uid", defaults.UID)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	gid, err := a.promptDefault("Local group name or gid", defaults.GID)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	fileModeDefault := defaults.FileMode
	if fileModeDefault == "" {
		fileModeDefault = "0664"
	}
	fileMode, err := a.promptDefault("File permissions", fileModeDefault)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	dirModeDefault := defaults.DirMode
	if dirModeDefault == "" {
		dirModeDefault = "0775"
	}
	dirMode, err := a.promptDefault("Folder permissions", dirModeDefault)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	autoMountDefault := true
	if defaults.Server != "" || defaults.Share != "" || defaults.MountPoint != "" {
		autoMountDefault = defaults.AutoMount
	}
	autoMount, err := a.confirm("Mount automatically at startup", autoMountDefault)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	readOnly, err := a.confirm("Make this mount read-only", defaults.ReadOnly)
	if err != nil {
		return CIFSMountConfig{}, err
	}

	return CIFSMountConfig{
		Server:     strings.TrimSpace(server),
		Share:      strings.TrimSpace(share),
		MountPoint: filepath.Clean(strings.TrimSpace(mountPoint)),
		Username:   strings.TrimSpace(username),
		Password:   strings.TrimSpace(password),
		Domain:     strings.TrimSpace(domain),
		UID:        strings.TrimSpace(uid),
		GID:        strings.TrimSpace(gid),
		FileMode:   strings.TrimSpace(fileMode),
		DirMode:    strings.TrimSpace(dirMode),
		AutoMount:  autoMount,
		ReadOnly:   readOnly,
	}, nil
}

func defaultCIFSMountConfig() CIFSMountConfig {
	uid, gid := defaultLocalMountOwnerGroup(os.Getenv, user.Current, user.Lookup, user.LookupGroupId)
	return CIFSMountConfig{
		UID: uid,
		GID: gid,
	}
}

func defaultLocalMountOwnerGroup(
	getenv func(string) string,
	currentUser func() (*user.User, error),
	lookupUser func(string) (*user.User, error),
	lookupGroupID func(string) (*user.Group, error),
) (uid string, gid string) {
	resolveGroup := func(groupID string) string {
		if strings.TrimSpace(groupID) == "" {
			return ""
		}
		group, err := lookupGroupID(groupID)
		if err != nil {
			return strings.TrimSpace(groupID)
		}
		return strings.TrimSpace(group.Name)
	}

	if sudoUser := strings.TrimSpace(getenv("SUDO_USER")); sudoUser != "" {
		uid = sudoUser
		if sudoGID := strings.TrimSpace(getenv("SUDO_GID")); sudoGID != "" {
			return uid, resolveGroup(sudoGID)
		}
		if u, err := lookupUser(sudoUser); err == nil {
			return uid, resolveGroup(u.Gid)
		}
		return uid, ""
	}

	u, err := currentUser()
	if err != nil {
		return "", ""
	}

	uid = strings.TrimSpace(u.Username)
	gid = resolveGroup(u.Gid)
	return uid, gid
}

func (a *App) ensureMountPoint(path string) error {
	info, err := os.Stat(path)
	if err == nil {
		if !info.IsDir() {
			return fmt.Errorf("mount folder exists but is not a directory: %s", path)
		}
		return nil
	}
	if !os.IsNotExist(err) {
		return fmt.Errorf("check mount folder %s: %w", path, err)
	}

	a.println("")
	a.printf("The local mount folder %s does not exist yet.\n", path)
	a.println("I can create it now.")
	a.flush()

	create, err := a.confirm("Create this folder now", true)
	if err != nil {
		return err
	}
	if !create {
		return errors.New("a mount folder is needed before continuing")
	}

	if err := a.createDirectory(path); err != nil {
		return err
	}
	return nil
}

func (a *App) mountCIFS(path string) error {
	if err := a.runPrivilegedOrLocal("mount", []string{path}, "Mounting the network share needs administrator permission."); err != nil {
		return fmt.Errorf("mount %s: %w", path, err)
	}
	return nil
}

func (a *App) reloadSystemdIfNeeded() error {
	if _, err := os.Stat("/run/systemd/system"); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("check for systemd: %w", err)
	}

	if a.lookPath != nil {
		if _, err := a.lookPath("systemctl"); err != nil {
			return nil
		}
	}

	if err := a.runPrivilegedOrLocal("systemctl", []string{"daemon-reload"}, "Refreshing system mount settings needs administrator permission."); err != nil {
		return fmt.Errorf("reload systemd after updating /etc/fstab: %w", err)
	}
	return nil
}

func (a *App) ensureShareDirectories(doc *Document) error {
	for _, section := range doc.ShareSections() {
		cfg := ShareFromSection(section)
		if strings.TrimSpace(cfg.Path) == "" {
			continue
		}

		info, err := os.Stat(cfg.Path)
		if err == nil {
			if info.IsDir() {
				if err := a.offerAdjustDirectoryPermissions(cfg); err != nil {
					return err
				}
				continue
			}
			return fmt.Errorf("share %q path exists but is not a directory: %s", cfg.Name, cfg.Path)
		}

		if !os.IsNotExist(err) {
			return fmt.Errorf("check share %q path %s: %w", cfg.Name, cfg.Path, err)
		}

		a.println("")
		a.printf("The folder for share %q does not exist yet.\n", cfg.Name)
		a.printf("Missing folder: %s\n", cfg.Path)
		a.println("I can create it now so the share is ready to use.")
		a.flush()

		create, promptErr := a.confirm("Create this folder now", true)
		if promptErr != nil {
			return promptErr
		}
		if !create {
			return fmt.Errorf("share %q needs an existing folder before saving", cfg.Name)
		}

		if err := a.createDirectory(cfg.Path); err != nil {
			return fmt.Errorf("create folder for share %q: %w", cfg.Name, err)
		}

		a.printf("Created folder %s\n", cfg.Path)
		if err := a.offerAdjustDirectoryPermissions(cfg); err != nil {
			return err
		}
	}

	return nil
}

func (a *App) selectShare(doc *Document) (*Section, error) {
	shares := doc.ShareSections()
	if len(shares) == 0 {
		a.showMessage("info", "There are no shares to select.")
		return nil, nil
	}

	options := make([]menuOption, 0, len(shares)+1)
	for i, share := range shares {
		cfg := ShareFromSection(share)
		options = append(options, menuOption{
			Key:         fmt.Sprintf("%d", i+1),
			Value:       share.Name,
			Label:       cfg.Name,
			Description: cfg.Path,
		})
	}
	options = append(options, menuOption{Key: "b", Value: "", Label: "Back", Description: "Return to the previous menu."})

	raw, err := a.chooseMenu("Choose A Share", "Select the share you want to work with.", nil, options)
	if err != nil {
		return nil, err
	}
	if raw == "" {
		return nil, nil
	}
	return doc.Section(raw), nil
}

func (a *App) collectShareConfig(existing ShareConfig) (ShareConfig, error) {
	name, err := a.promptDefault("Share name", existing.Name)
	if err != nil {
		return ShareConfig{}, err
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return ShareConfig{}, errors.New("share name cannot be empty")
	}

	path, err := a.promptDefault("Path", existing.Path)
	if err != nil {
		return ShareConfig{}, err
	}
	path = strings.TrimSpace(path)
	if path == "" {
		return ShareConfig{}, errors.New("path cannot be empty")
	}

	comment, err := a.promptDefault("Comment", existing.Comment)
	if err != nil {
		return ShareConfig{}, err
	}

	browseable, err := a.promptDefault("Browseable (yes/no)", defaultString(existing.Browseable, "yes"))
	if err != nil {
		return ShareConfig{}, err
	}

	readOnly, err := a.promptDefault("Read only (yes/no)", defaultString(existing.ReadOnly, "no"))
	if err != nil {
		return ShareConfig{}, err
	}

	guestOK, err := a.promptDefault("Guest ok (yes/no)", defaultString(existing.GuestOK, "no"))
	if err != nil {
		return ShareConfig{}, err
	}

	usersDefault := strings.Join(existing.ValidUsers, ", ")
	usersRaw, err := a.promptDefault("Valid users (comma or space separated, blank for none)", usersDefault)
	if err != nil {
		return ShareConfig{}, err
	}

	return ShareConfig{
		Name:       name,
		Path:       filepath.Clean(path),
		Comment:    strings.TrimSpace(comment),
		Browseable: browseable,
		ReadOnly:   readOnly,
		GuestOK:    guestOK,
		ValidUsers: splitUsers(usersRaw),
	}, nil
}

func (a *App) ensureUsers(users []string) error {
	for _, name := range users {
		if a.users.UserExists(name) {
			continue
		}

		create, err := a.confirm(fmt.Sprintf("User %q does not exist. Create it with useradd", name), true)
		if err != nil {
			return err
		}
		if !create {
			continue
		}

		if err := a.createUser(name); err != nil {
			return err
		}

		a.printf("Created local user %s\n", name)
		if err := a.offerSetSambaPassword(name); err != nil {
			return err
		}
	}

	return nil
}

func (a *App) createUser(name string) error {
	args := []string{"-M", "-s", "/usr/sbin/nologin", name}
	if err := a.runner.Run("useradd", args...); err == nil {
		return nil
	} else if shouldOfferPrivilegeRetry(err) {
		a.println("")
		a.println("Creating a Linux user usually needs administrator permission.")
		a.println("I can try again using sudo so you can enter your admin password.")
		a.flush()

		canUseSudo := false
		if a.lookPath != nil {
			_, sudoErr := a.lookPath("sudo")
			canUseSudo = sudoErr == nil
		}
		if !canUseSudo {
			return fmt.Errorf("create user %s: %w", name, err)
		}

		retry, promptErr := a.confirm("Retry user creation with sudo", true)
		if promptErr != nil {
			return promptErr
		}
		if !retry {
			return fmt.Errorf("create user %s: %w", name, err)
		}

		if sudoErr := a.runner.Run("sudo", append([]string{"useradd"}, args...)...); sudoErr != nil {
			return fmt.Errorf("create user %s with sudo: %w", name, sudoErr)
		}
		return nil
	} else {
		return fmt.Errorf("create user %s: %w", name, err)
	}
}

func (a *App) offerSetSambaPassword(name string) error {
	a.println("")
	a.println("This share can use a Samba password for sign-in.")
	a.println("I can set that up now and ask you to type the password you want to use.")
	a.flush()

	setPassword, err := a.confirm(fmt.Sprintf("Set a Samba password for %q now", name), true)
	if err != nil {
		return err
	}
	if !setPassword {
		a.println("You can do this later with: smbpasswd -a " + name)
		return nil
	}

	return a.setSambaPassword(name)
}

func (a *App) setSambaPassword(name string) error {
	if a.lookPath != nil {
		if _, err := a.lookPath("smbpasswd"); err != nil {
			a.println("I couldn't find the smbpasswd tool yet.")
			a.println("If Samba was just installed, try opening the app again after the installation finishes.")
			return fmt.Errorf("set Samba password for %s: smbpasswd command not found", name)
		}
	}

	if err := a.runner.Run("smbpasswd", "-a", name); err == nil {
		a.printf("Samba password set for %s\n", name)
		return nil
	} else if shouldOfferPrivilegeRetry(err) {
		a.println("")
		a.println("Setting a Samba password usually needs administrator permission.")
		a.println("I can try again using sudo so you can enter your admin password.")
		a.flush()

		canUseSudo := false
		if a.lookPath != nil {
			_, sudoErr := a.lookPath("sudo")
			canUseSudo = sudoErr == nil
		}
		if !canUseSudo {
			return fmt.Errorf("set Samba password for %s: %w", name, err)
		}

		retry, promptErr := a.confirm("Retry Samba password setup with sudo", true)
		if promptErr != nil {
			return promptErr
		}
		if !retry {
			return fmt.Errorf("set Samba password for %s: %w", name, err)
		}

		if sudoErr := a.runner.Run("sudo", "smbpasswd", "-a", name); sudoErr != nil {
			return fmt.Errorf("set Samba password for %s with sudo: %w", name, sudoErr)
		}

		a.printf("Samba password set for %s\n", name)
		return nil
	} else {
		return fmt.Errorf("set Samba password for %s: %w", name, err)
	}
}

func (a *App) deleteUser(name string) error {
	if err := a.runPrivilegedOrLocal("userdel", []string{name}, "Deleting a Linux user needs administrator permission."); err != nil {
		return fmt.Errorf("delete user %s: %w", name, err)
	}

	if a.lookPath != nil {
		if _, err := a.lookPath("smbpasswd"); err == nil {
			removeSamba, promptErr := a.confirm("Also remove the Samba password for this account", true)
			if promptErr != nil {
				return promptErr
			}
			if removeSamba {
				if err := a.runPrivilegedOrLocal("smbpasswd", []string{"-x", name}, "Removing the Samba password needs administrator permission."); err != nil {
					return fmt.Errorf("remove Samba password for %s: %w", name, err)
				}
			}
		}
	}

	return nil
}

func (a *App) prompt(label string) (string, error) {
	a.flush()
	return runTextPrompt(a.input, a.output, a.theme, "Enter A Value", "Field-by-field configuration", label, "")
}

func (a *App) promptDefault(label, defaultValue string) (string, error) {
	a.flush()
	return runTextPrompt(a.input, a.output, a.theme, "Enter A Value", "Field-by-field configuration", label, defaultValue)
}

func (a *App) confirm(label string, defaultYes bool) (bool, error) {
	options := []menuOption{
		{Key: "y", Value: "yes", Label: "Yes"},
		{Key: "n", Value: "no", Label: "No"},
	}
	if !defaultYes {
		options[0], options[1] = options[1], options[0]
	}

	answer, err := a.chooseMenu("Confirm Action", label, nil, options)
	if err != nil {
		return false, err
	}
	switch answer {
	case "yes":
		return true, nil
	case "no":
		return false, nil
	default:
		return defaultYes, nil
	}
}

func (a *App) println(line string) {
	fmt.Fprintln(a.writer, line)
}

func (a *App) printf(format string, args ...any) {
	fmt.Fprintf(a.writer, format, args...)
}

func (a *App) flush() {
	_ = a.writer.Flush()
}

func (a *App) chooseMenu(title, subtitle string, intro []string, options []menuOption) (string, error) {
	a.flush()
	choice, err := runMenuPrompt(a.input, a.output, a.theme, title, subtitle, intro, options)
	a.flush()
	return choice, err
}

func (a *App) showPanel(title, subtitle string, lines []string) {
	body := a.theme.renderSection(84, "", lines)
	fmt.Fprintln(a.writer, a.theme.renderFrame(84, title, subtitle, body, ""))
	a.flush()
}

func (a *App) showMessage(kind, message string) {
	fmt.Fprintln(a.writer, a.theme.renderFrame(84, "Status", "", a.theme.renderMessage(84, kind, message), ""))
	a.flush()
}

func defaultString(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func copyFile(src, dst string) error {
	data, err := os.ReadFile(src)
	if err != nil {
		return err
	}
	return os.WriteFile(dst, data, 0o644)
}

func shouldOfferPrivilegeRetry(err error) bool {
	if os.Geteuid() == 0 {
		return false
	}

	text := strings.ToLower(err.Error())
	return strings.Contains(text, "permission denied") ||
		strings.Contains(text, "operation not permitted") ||
		strings.Contains(text, "exit status")
}

func (a *App) writeConfigWithPrivilegeRetry(serialized, backup string) error {
	a.println("")
	a.println("Saving Samba settings usually needs administrator permission.")
	a.println("I can try again using sudo so you can enter your admin password.")
	a.flush()

	canUseSudo := false
	if a.lookPath != nil {
		_, sudoErr := a.lookPath("sudo")
		canUseSudo = sudoErr == nil
	}
	if !canUseSudo {
		return friendlyWriteError("save Samba settings", a.configPath, os.ErrPermission)
	}

	retry, err := a.confirm("Retry saving with sudo", true)
	if err != nil {
		return err
	}
	if !retry {
		return friendlyWriteError("save Samba settings", a.configPath, os.ErrPermission)
	}

	tempPath, err := a.writeTempConfig(serialized)
	if err != nil {
		return fmt.Errorf("prepare config for sudo save: %w", err)
	}
	defer os.Remove(tempPath)

	if err := a.runner.Run("sudo", "cp", a.configPath, backup); err != nil {
		return friendlyWriteError("create backup", backup, err)
	}

	if err := a.runner.Run("sudo", "install", "-m", "644", tempPath, a.configPath); err != nil {
		return friendlyWriteError("write config", a.configPath, err)
	}

	a.printf("Config written to %s\n", a.configPath)
	a.printf("Backup saved to %s\n", backup)
	return nil
}

func (a *App) writeTempConfig(serialized string) (string, error) {
	file, err := os.CreateTemp("", "samba-configer-*.conf")
	if err != nil {
		return "", err
	}
	defer file.Close()

	if _, err := file.WriteString(serialized); err != nil {
		return "", err
	}
	return file.Name(), nil
}

func (a *App) createDirectory(path string) error {
	if err := os.MkdirAll(path, 0o755); err == nil {
		return nil
	} else if shouldOfferPrivilegeRetry(err) {
		a.println("")
		a.println("Creating that folder needs administrator permission.")
		a.println("I can try again using sudo so you can enter your admin password.")
		a.flush()

		canUseSudo := false
		if a.lookPath != nil {
			_, sudoErr := a.lookPath("sudo")
			canUseSudo = sudoErr == nil
		}
		if !canUseSudo {
			return fmt.Errorf("create folder %s: %w", path, err)
		}

		retry, promptErr := a.confirm("Retry folder creation with sudo", true)
		if promptErr != nil {
			return promptErr
		}
		if !retry {
			return fmt.Errorf("create folder %s: %w", path, err)
		}

		if sudoErr := a.runner.Run("sudo", "mkdir", "-p", path); sudoErr != nil {
			return fmt.Errorf("create folder %s with sudo: %w", path, sudoErr)
		}
		if sudoErr := a.runner.Run("sudo", "chmod", "755", path); sudoErr != nil {
			return fmt.Errorf("set permissions on folder %s with sudo: %w", path, sudoErr)
		}
		return nil
	} else {
		return fmt.Errorf("create folder %s: %w", path, err)
	}
}

func (a *App) offerAdjustDirectoryPermissions(cfg ShareConfig) error {
	mode, owner, group, explanation := recommendSharePermissions(cfg)

	a.println("")
	a.printf("The share folder %s may need permissions adjusted for file operations.\n", cfg.Path)
	a.println(explanation)
	if owner != "" || group != "" {
		a.printf("Suggested owner/group: %s:%s\n", defaultString(owner, "unchanged"), defaultString(group, "unchanged"))
	}
	a.printf("Suggested permissions: %s\n", mode)
	a.println("I can apply these recommended permissions now.")
	a.flush()

	adjust, err := a.confirm("Adjust this folder now", true)
	if err != nil {
		return err
	}
	if !adjust {
		return nil
	}

	if err := a.applyDirectoryPermissions(cfg.Path, owner, group, mode); err != nil {
		return fmt.Errorf("adjust permissions for share %q: %w", cfg.Name, err)
	}

	a.printf("Updated permissions for %s\n", cfg.Path)
	return nil
}

func recommendSharePermissions(cfg ShareConfig) (mode, owner, group, explanation string) {
	mode = "0770"
	group = "users"
	explanation = "Members of the allowed account list should usually be able to read and write here."

	if strings.EqualFold(normalizeBoolish(cfg.ReadOnly, "no"), "yes") {
		mode = "0755"
		explanation = "This share is marked read-only, so a safer folder mode is recommended."
	}

	if strings.EqualFold(normalizeBoolish(cfg.GuestOK, "no"), "yes") {
		mode = "0777"
		group = ""
		explanation = "This share allows guests, so wider permissions are usually needed for uploads and edits."
		return mode, "", group, explanation
	}

	if len(cfg.ValidUsers) == 1 {
		owner = cfg.ValidUsers[0]
		group = cfg.ValidUsers[0]
		explanation = "This share is limited to one account, so making that account the owner is the simplest setup."
		return mode, owner, group, explanation
	}

	if len(cfg.ValidUsers) > 1 {
		explanation = "This share has multiple allowed accounts, so a shared writable group-style setup is recommended."
	}

	return mode, owner, group, explanation
}

func (a *App) applyDirectoryPermissions(path, owner, group, mode string) error {
	chownTarget := owner
	if group != "" {
		if chownTarget == "" {
			chownTarget = ":" + group
		} else {
			chownTarget = chownTarget + ":" + group
		}
	}

	if err := a.runPrivilegedOrLocal("chmod", []string{mode, path}, "Changing folder permissions needs administrator permission."); err != nil {
		return err
	}

	if chownTarget != "" {
		if err := a.runPrivilegedOrLocal("chown", []string{chownTarget, path}, "Changing folder ownership needs administrator permission."); err != nil {
			return err
		}
	}

	return nil
}

func (a *App) runPrivilegedOrLocal(command string, args []string, privilegeMessage string) error {
	if err := a.runner.Run(command, args...); err == nil {
		return nil
	} else if shouldOfferPrivilegeRetry(err) {
		a.println("")
		a.println(privilegeMessage)
		a.println("I can try again using sudo so you can enter your admin password.")
		a.flush()

		canUseSudo := false
		if a.lookPath != nil {
			_, sudoErr := a.lookPath("sudo")
			canUseSudo = sudoErr == nil
		}
		if !canUseSudo {
			return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err)
		}

		retry, promptErr := a.confirm("Retry with sudo", true)
		if promptErr != nil {
			return promptErr
		}
		if !retry {
			return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err)
		}

		if sudoErr := a.runner.Run("sudo", append([]string{command}, args...)...); sudoErr != nil {
			return fmt.Errorf("%s %s with sudo: %w", command, strings.Join(args, " "), sudoErr)
		}
		return nil
	} else {
		return fmt.Errorf("%s %s: %w", command, strings.Join(args, " "), err)
	}
}

func friendlyWriteError(action, path string, err error) error {
	if errors.Is(err, os.ErrPermission) || strings.Contains(strings.ToLower(err.Error()), "permission denied") {
		return fmt.Errorf("%s %s: administrator permission is needed", action, path)
	}
	return fmt.Errorf("%s %s: %w", action, path, err)
}

func buildFstabLine(cfg CIFSMountConfig) string {
	source := fmt.Sprintf("//%s/%s", cfg.Server, cfg.Share)

	options := []string{
		"username=" + escapeFstabValue(cfg.Username),
		"password=" + escapeFstabValue(cfg.Password),
		"iocharset=utf8",
	}

	if cfg.Domain != "" {
		options = append(options, "domain="+escapeFstabValue(cfg.Domain))
	}
	if cfg.UID != "" {
		options = append(options, "uid="+escapeFstabValue(cfg.UID))
	}
	if cfg.GID != "" {
		options = append(options, "gid="+escapeFstabValue(cfg.GID))
	}
	if cfg.FileMode != "" {
		options = append(options, "file_mode="+cfg.FileMode)
	}
	if cfg.DirMode != "" {
		options = append(options, "dir_mode="+cfg.DirMode)
	}
	if cfg.ReadOnly {
		options = append(options, "ro")
	} else {
		options = append(options, "rw")
	}
	if !cfg.AutoMount {
		options = append(options, "noauto")
	}

	return fmt.Sprintf("%s %s cifs %s 0 0", source, cfg.MountPoint, strings.Join(options, ","))
}

func escapeFstabValue(value string) string {
	value = strings.ReplaceAll(value, `\`, `\\`)
	value = strings.ReplaceAll(value, ",", `\,`)
	value = strings.ReplaceAll(value, " ", `\040`)
	return value
}

func shellQuote(value string) string {
	return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}

func (a *App) loadCIFSMountEntries() ([]FstabMountEntry, error) {
	data, err := os.ReadFile(a.fstabPath)
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", a.fstabPath, err)
	}
	return parseCIFSMountEntries(string(data)), nil
}

func parseCIFSMountEntries(contents string) []FstabMountEntry {
	lines := strings.Split(contents, "\n")
	entries := make([]FstabMountEntry, 0)
	for index, line := range lines {
		entry, ok := parseFstabMountLine(line, index)
		if ok {
			entries = append(entries, entry)
		}
	}
	return entries
}

func parseFstabMountLine(line string, index int) (FstabMountEntry, bool) {
	trimmed := strings.TrimSpace(line)
	if trimmed == "" || strings.HasPrefix(trimmed, "#") {
		return FstabMountEntry{}, false
	}

	fields := strings.Fields(trimmed)
	if len(fields) < 4 {
		return FstabMountEntry{}, false
	}

	fsType := strings.ToLower(fields[2])
	if fsType != "cifs" && fsType != "smbfs" {
		return FstabMountEntry{}, false
	}

	entry := FstabMountEntry{
		LineIndex:  index,
		Source:     fields[0],
		MountPoint: unescapeFstabValue(fields[1]),
		FSType:     fields[2],
		Options:    splitFstabOptions(fields[3]),
		RawLine:    line,
		DisplayName: fmt.Sprintf(
			"%s (%s)",
			unescapeFstabValue(fields[0]),
			unescapeFstabValue(fields[2]),
		),
	}
	if len(fields) > 4 {
		entry.Dump = fields[4]
	}
	if len(fields) > 5 {
		entry.Pass = fields[5]
	}
	if entry.Dump == "" {
		entry.Dump = "0"
	}
	if entry.Pass == "" {
		entry.Pass = "0"
	}
	if entry.DisplayName == " (cifs)" || entry.DisplayName == " (smbfs)" {
		entry.DisplayName = entry.MountPoint
	}
	return entry, true
}

func splitFstabOptions(value string) []string {
	if value == "" {
		return nil
	}

	var options []string
	var current strings.Builder
	escaped := false
	for _, r := range value {
		switch {
		case escaped:
			current.WriteRune(r)
			escaped = false
		case r == '\\':
			current.WriteRune(r)
			escaped = true
		case r == ',':
			options = append(options, current.String())
			current.Reset()
		default:
			current.WriteRune(r)
		}
	}
	options = append(options, current.String())
	return options
}

func unescapeFstabValue(value string) string {
	value = strings.ReplaceAll(value, `\040`, " ")
	value = strings.ReplaceAll(value, `\,`, ",")
	value = strings.ReplaceAll(value, `\\`, `\`)
	return value
}

func (a *App) addClientMount() error {
	if err := a.ensureCIFSUtilsInstalled(); err != nil {
		return err
	}

	cfg, err := a.collectCIFSMountConfig(defaultCIFSMountConfig())
	if err != nil {
		return err
	}

	if err := a.ensureMountPoint(cfg.MountPoint); err != nil {
		return err
	}

	line := buildFstabLine(cfg)
	if err := a.confirmAndWriteClientMount(line, -1, "Add this line to /etc/fstab now"); err != nil {
		return err
	}

	a.println("")
	a.println("The mount entry was added to /etc/fstab.")
	return a.offerMountNow(cfg)
}

func (a *App) editClientMount(entries []FstabMountEntry) error {
	entry, err := a.selectCIFSMount(entries)
	if err != nil || entry == nil {
		return err
	}

	cfg, ok := cifsMountConfigFromEntry(*entry)
	if !ok {
		return fmt.Errorf("could not read mount details from %s", entry.RawLine)
	}

	if err := a.ensureCIFSUtilsInstalled(); err != nil {
		return err
	}

	updated, err := a.collectCIFSMountConfig(cfg)
	if err != nil {
		return err
	}
	if err := a.ensureMountPoint(updated.MountPoint); err != nil {
		return err
	}

	line := buildFstabLine(updated)
	if err := a.confirmAndWriteClientMount(line, entry.LineIndex, "Save these changes to /etc/fstab now"); err != nil {
		return err
	}

	a.println("")
	a.println("The mount entry was updated in /etc/fstab.")
	return a.offerMountNow(updated)
}

func (a *App) deleteClientMount(entries []FstabMountEntry) error {
	entry, err := a.selectCIFSMount(entries)
	if err != nil || entry == nil {
		return err
	}

	confirm, err := a.confirm(fmt.Sprintf("Delete the mount for %s", entry.DisplayName), false)
	if err != nil {
		return err
	}
	if !confirm {
		a.println("Delete cancelled.")
		return nil
	}

	if err := a.writeFstabLineChange("", entry.LineIndex); err != nil {
		return err
	}

	a.println("The mount entry was removed from /etc/fstab.")
	return a.reloadSystemdIfNeeded()
}

func (a *App) selectCIFSMount(entries []FstabMountEntry) (*FstabMountEntry, error) {
	if len(entries) == 0 {
		a.showMessage("info", "There are no client mounts to select.")
		return nil, nil
	}

	options := make([]menuOption, 0, len(entries)+1)
	for i, entry := range entries {
		options = append(options, menuOption{
			Key:         fmt.Sprintf("%d", i+1),
			Value:       strconv.Itoa(i),
			Label:       entry.DisplayName,
			Description: entry.MountPoint,
		})
	}
	options = append(options, menuOption{Key: "b", Value: "", Label: "Back"})

	selection, err := a.chooseMenu("Choose A Client Mount", "Select the saved mount entry you want to change.", nil, options)
	if err != nil {
		return nil, err
	}
	if selection == "" {
		return nil, nil
	}

	index, err := strconv.Atoi(selection)
	if err != nil || index < 0 || index >= len(entries) {
		return nil, nil
	}
	entry := entries[index]
	return &entry, nil
}

func cifsMountConfigFromEntry(entry FstabMountEntry) (CIFSMountConfig, bool) {
	server, share, ok := splitCIFSSource(entry.Source)
	if !ok {
		return CIFSMountConfig{}, false
	}

	cfg := CIFSMountConfig{
		Server:     server,
		Share:      share,
		MountPoint: entry.MountPoint,
		AutoMount:  true,
	}

	for _, option := range entry.Options {
		key, value, hasValue := strings.Cut(option, "=")
		switch strings.ToLower(key) {
		case "username":
			if hasValue {
				cfg.Username = unescapeFstabValue(value)
			}
		case "password":
			if hasValue {
				cfg.Password = unescapeFstabValue(value)
			}
		case "domain":
			if hasValue {
				cfg.Domain = unescapeFstabValue(value)
			}
		case "uid":
			if hasValue {
				cfg.UID = unescapeFstabValue(value)
			}
		case "gid":
			if hasValue {
				cfg.GID = unescapeFstabValue(value)
			}
		case "file_mode":
			if hasValue {
				cfg.FileMode = value
			}
		case "dir_mode":
			if hasValue {
				cfg.DirMode = value
			}
		case "ro":
			cfg.ReadOnly = true
		case "rw":
			cfg.ReadOnly = false
		case "noauto":
			cfg.AutoMount = false
		}
	}

	return cfg, true
}

func splitCIFSSource(source string) (server, share string, ok bool) {
	unescaped := unescapeFstabValue(source)
	if !strings.HasPrefix(unescaped, "//") {
		return "", "", false
	}

	parts := strings.SplitN(strings.TrimPrefix(unescaped, "//"), "/", 2)
	if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
		return "", "", false
	}

	return parts[0], parts[1], true
}

func (a *App) confirmAndWriteClientMount(line string, lineIndex int, promptLabel string) error {
	a.println("")
	a.println("Suggested /etc/fstab line:")
	a.println(line)
	a.println("")
	a.println("This will store the username and password directly in /etc/fstab.")
	a.println("That is simple for home use, but less secure than a credentials file.")
	a.flush()

	confirm, err := a.confirm(promptLabel, true)
	if err != nil {
		return err
	}
	if !confirm {
		return nil
	}

	if err := a.writeFstabLineChange(line, lineIndex); err != nil {
		return err
	}
	return a.reloadSystemdIfNeeded()
}

func (a *App) offerMountNow(cfg CIFSMountConfig) error {
	mountNow, err := a.confirm("Try mounting it now", true)
	if err != nil {
		return err
	}
	if !mountNow {
		return nil
	}
	if err := a.mountCIFS(cfg.MountPoint); err != nil {
		return err
	}
	a.println("The network share was mounted.")
	return nil
}

func (a *App) writeFstabLineChange(line string, lineIndex int) error {
	data, err := os.ReadFile(a.fstabPath)
	if err != nil {
		return fmt.Errorf("read %s: %w", a.fstabPath, err)
	}

	updated, err := updateFstabContents(string(data), line, lineIndex)
	if err != nil {
		return err
	}
	return a.writeFstabContents(updated)
}

func updateFstabContents(contents, line string, lineIndex int) (string, error) {
	hasTrailingNewline := strings.HasSuffix(contents, "\n")
	lines := strings.Split(contents, "\n")
	if len(lines) > 0 && lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	}

	switch {
	case lineIndex < 0:
		lines = append(lines, line)
	case lineIndex >= len(lines):
		return "", fmt.Errorf("mount entry line %d is out of range", lineIndex)
	case line == "":
		lines = append(lines[:lineIndex], lines[lineIndex+1:]...)
	default:
		lines[lineIndex] = line
	}

	updated := strings.Join(lines, "\n")
	if hasTrailingNewline || updated != "" {
		updated += "\n"
	}
	return updated, nil
}

func (a *App) writeFstabContents(serialized string) error {
	backup := fmt.Sprintf("%s.bak.%s", a.fstabPath, time.Now().UTC().Format("20060102T150405Z"))

	if err := copyFile(a.fstabPath, backup); err != nil {
		if shouldOfferPrivilegeRetry(err) {
			return a.writeFstabWithPrivilegeRetry(serialized, backup)
		}
		return friendlyWriteError("create backup", backup, err)
	}

	if err := os.WriteFile(a.fstabPath, []byte(serialized), 0o644); err != nil {
		if shouldOfferPrivilegeRetry(err) {
			return a.writeFstabWithPrivilegeRetry(serialized, backup)
		}
		return friendlyWriteError("write config", a.fstabPath, err)
	}

	return nil
}

func (a *App) writeFstabWithPrivilegeRetry(serialized, backup string) error {
	a.println("")
	a.println("Updating /etc/fstab usually needs administrator permission.")
	a.println("I can try again using sudo so you can enter your admin password.")
	a.flush()

	canUseSudo := false
	if a.lookPath != nil {
		_, sudoErr := a.lookPath("sudo")
		canUseSudo = sudoErr == nil
	}
	if !canUseSudo {
		return friendlyWriteError("write config", a.fstabPath, os.ErrPermission)
	}

	retry, err := a.confirm("Retry updating /etc/fstab with sudo", true)
	if err != nil {
		return err
	}
	if !retry {
		return friendlyWriteError("write config", a.fstabPath, os.ErrPermission)
	}

	tempPath, err := a.writeTempConfig(serialized)
	if err != nil {
		return fmt.Errorf("prepare fstab for sudo save: %w", err)
	}
	defer os.Remove(tempPath)

	if err := a.runner.Run("sudo", "cp", a.fstabPath, backup); err != nil {
		return friendlyWriteError("create backup", backup, err)
	}

	if err := a.runner.Run("sudo", "install", "-m", "644", tempPath, a.fstabPath); err != nil {
		return friendlyWriteError("write config", a.fstabPath, err)
	}

	return nil
}

type ShareUserReference struct {
	User   string
	Shares []string
}

func shareUsers(doc *Document) map[string]struct{} {
	users := make(map[string]struct{})
	for _, section := range doc.ShareSections() {
		cfg := ShareFromSection(section)
		for _, user := range cfg.ValidUsers {
			users[user] = struct{}{}
		}
	}
	return users
}

func shareUserReferences(doc *Document) []ShareUserReference {
	refs := map[string][]string{}
	for _, section := range doc.ShareSections() {
		cfg := ShareFromSection(section)
		for _, user := range cfg.ValidUsers {
			refs[user] = append(refs[user], cfg.Name)
		}
	}

	var out []ShareUserReference
	for user, shares := range refs {
		out = append(out, ShareUserReference{User: user, Shares: shares})
	}
	slices.SortFunc(out, func(a, b ShareUserReference) int {
		return strings.Compare(a.User, b.User)
	})
	return out
}

func readPasswdEntries() ([]PasswdEntry, error) {
	data, err := os.ReadFile("/etc/passwd")
	if err != nil {
		return nil, err
	}

	var entries []PasswdEntry
	for _, line := range strings.Split(string(data), "\n") {
		if strings.TrimSpace(line) == "" {
			continue
		}

		parts := strings.Split(line, ":")
		if len(parts) < 7 {
			continue
		}

		uid, err := strconv.Atoi(parts[2])
		if err != nil {
			continue
		}
		gid, err := strconv.Atoi(parts[3])
		if err != nil {
			continue
		}

		entries = append(entries, PasswdEntry{
			Name:  parts[0],
			UID:   uid,
			GID:   gid,
			Home:  parts[5],
			Shell: parts[6],
		})
	}

	return entries, nil
}

func unusedAccountCandidates(entries []PasswdEntry, active map[string]struct{}) []PasswdEntry {
	var candidates []PasswdEntry
	for _, entry := range entries {
		if _, ok := active[entry.Name]; ok {
			continue
		}
		if !looksLikeSafeShareAccount(entry) {
			continue
		}
		candidates = append(candidates, entry)
	}

	slices.SortFunc(candidates, func(a, b PasswdEntry) int {
		return strings.Compare(a.Name, b.Name)
	})
	return candidates
}

func looksLikeSafeShareAccount(entry PasswdEntry) bool {
	if entry.UID < 1000 {
		return false
	}

	shell := strings.TrimSpace(entry.Shell)
	return strings.HasSuffix(shell, "/nologin") || strings.HasSuffix(shell, "/false")
}
