package main

import (
	"bufio"
	"errors"
	"fmt"
	"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
	users      UserManager
	runner     CommandRunner
	lookPath   LookPathFunc
	reader     *bufio.Reader
	writer     *bufio.Writer
}

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
}

func NewApp(configPath string, users UserManager, runner CommandRunner, lookPath LookPathFunc) *App {
	return &App{
		configPath: configPath,
		users:      users,
		runner:     runner,
		lookPath:   lookPath,
		reader:     bufio.NewReader(os.Stdin),
		writer:     bufio.NewWriter(os.Stdout),
	}
}

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) {
	a.println("")
	a.println("Samba setup assistant")
	a.println("=====================")
	a.println("[s] set up or edit shares on this computer")
	a.println("[c] connect this computer to a share on another server")
	a.println("[q] quit")
	a.flush()

	choice, err := a.prompt("What would you like to do")
	if err != nil {
		return "", err
	}

	switch strings.ToLower(choice) {
	case "s", "server":
		return "server", nil
	case "c", "client":
		return "client", nil
	case "q", "quit":
		return "quit", nil
	default:
		a.println("Unknown choice.")
		return a.chooseStartupWorkflow()
	}
}

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()
		a.println("")
		a.println("Samba share editor")
		a.println("==================")
		a.println("Current shares:")
		if len(shareSections) == 0 {
			a.println("  (none)")
		} else {
			for i, section := range shareSections {
				cfg := ShareFromSection(section)
				a.printf("  %d. %s -> %s\n", i+1, cfg.Name, cfg.Path)
			}
		}
		a.println("")
		a.println("[a] add share")
		a.println("[e] edit share")
		a.println("[d] delete share")
		a.println("[m] set up client mount")
		a.println("[u] manage users")
		a.println("[w] write config and exit")
		a.println("[q] quit without saving")
		a.flush()

		choice, err := a.prompt("Select an action")
		if err != nil {
			return err
		}

		switch strings.ToLower(choice) {
		case "a", "add":
			if err := a.addShare(doc); err != nil {
				return err
			}
		case "e", "edit":
			if err := a.editShare(doc); err != nil {
				return err
			}
		case "d", "delete":
			if err := a.deleteShare(doc); err != nil {
				return err
			}
		case "m", "mount":
			if err := a.setupClientMount(); err != nil {
				return err
			}
		case "u", "users":
			if err := a.manageUsers(doc); err != nil {
				return err
			}
		case "w", "write":
			return a.writeConfig(doc)
		case "q", "quit":
			return nil
		default:
			a.println("Unknown choice.")
		}
	}
}

func (a *App) setupClientMount() error {
	a.println("")
	a.println("Client mount setup")
	a.println("==================")
	a.println("This will help connect this computer to a shared folder on another server.")
	a.flush()

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

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

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

	line := buildFstabLine(cfg)
	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()

	addLine, err := a.confirm("Add this line to /etc/fstab now", true)
	if err != nil {
		return err
	}
	if !addLine {
		return nil
	}

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

	a.println("")
	a.println("The mount entry was added to /etc/fstab.")
	mountNow, err := a.confirm("Try mounting it now", true)
	if err != nil {
		return err
	}
	if mountNow {
		if err := a.reloadSystemdIfNeeded(); err != nil {
			return err
		}
		if err := a.mountCIFS(cfg.MountPoint); err != nil {
			return err
		}
		a.println("The network share was mounted.")
	}

	return nil
}

func (a *App) manageUsers(doc *Document) error {
	for {
		a.println("")
		a.println("User management")
		a.println("===============")
		a.println("[c] check share accounts")
		a.println("[p] change a Samba password")
		a.println("[x] delete an unused account")
		a.println("[b] back")
		a.flush()

		choice, err := a.prompt("Select an action")
		if err != nil {
			return err
		}

		switch strings.ToLower(choice) {
		case "c", "check":
			if err := a.checkShareAccounts(doc); err != nil {
				return err
			}
		case "p", "password":
			if err := a.changeSambaPassword(doc); err != nil {
				return err
			}
		case "x", "delete":
			if err := a.deleteUnusedAccount(doc); err != nil {
				return err
			}
		case "b", "back":
			return nil
		default:
			a.println("Unknown choice.")
		}
	}
}

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

	a.println("")
	a.println("Accounts used by the current shares:")
	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.println("")
		a.println("All listed share accounts already exist.")
		return nil
	}

	a.println("")
	a.println("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.println("I couldn't find any obvious unused share accounts to delete.")
		a.println("For safety, system accounts are excluded from this list.")
		return nil
	}

	a.println("")
	a.println("Accounts that look unused by the current shares:")
	for i, entry := range candidates {
		a.printf("  %d. %s (shell: %s)\n", i+1, entry.Name, entry.Shell)
	}
	a.println("Only accounts that are not listed in any share are shown here.")
	a.flush()

	raw, err := a.prompt("Account number to delete")
	if err != nil {
		return err
	}

	index, err := strconv.Atoi(raw)
	if err != nil || index < 1 || index > len(candidates) {
		a.println("Invalid selection.")
		return nil
	}

	entry := candidates[index-1]
	a.println("")
	a.printf("This will remove the local account %q.\n", entry.Name)
	a.println("This is only safe if nobody needs this account for Samba or anything else.")
	a.flush()

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

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

	a.printf("Deleted account %s\n", entry.Name)
	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.println("No accounts are listed in any share.")
		return nil
	}

	a.println("")
	a.println("Accounts used by the current shares:")
	for i, ref := range references {
		status := "missing"
		if a.users.UserExists(ref.User) {
			status = "present"
		}
		a.printf("  %d. %s [%s] used by: %s\n", i+1, ref.User, status, strings.Join(ref.Shares, ", "))
	}
	a.flush()

	raw, err := a.prompt("Account number to update")
	if err != nil {
		return err
	}

	index, err := strconv.Atoi(raw)
	if err != nil || index < 1 || index > len(references) {
		a.println("Invalid selection.")
		return nil
	}

	ref := references[index-1]
	if !a.users.UserExists(ref.User) {
		a.println("")
		a.printf("The local account %q does not exist yet.\n", ref.User)
		a.println("I can create it first so a Samba password can be set.")
		a.flush()

		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.println("")
	a.printf("I’ll open the password setup for %q now.\n", ref.User)
	a.println("You’ll be asked to type the new Samba password.")
	a.flush()
	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() (CIFSMountConfig, error) {
	server, err := a.prompt("Server name or IP")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	share, err := a.prompt("Share name on that server")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	mountPointDefault := filepath.Join("/mnt", strings.TrimSpace(share))
	mountPoint, err := a.promptDefault("Local mount folder", mountPointDefault)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	username, err := a.prompt("Username for the remote share")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	password, err := a.prompt("Password for the remote share")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	domain, err := a.promptDefault("Domain or workgroup (optional)", "")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	uid, err := a.promptDefault("Local owner username or uid (optional)", "")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	gid, err := a.promptDefault("Local group name or gid (optional)", "")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	fileMode, err := a.promptDefault("File permissions", "0664")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	dirMode, err := a.promptDefault("Folder permissions", "0775")
	if err != nil {
		return CIFSMountConfig{}, err
	}
	autoMount, err := a.confirm("Mount automatically at startup", true)
	if err != nil {
		return CIFSMountConfig{}, err
	}
	readOnly, err := a.confirm("Make this mount read-only", false)
	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 (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) appendFstabLine(line string) error {
	content := line + "\n"
	file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0)
	if err == nil {
		defer file.Close()
		if _, err := file.WriteString(content); err == nil {
			return nil
		} else if !shouldOfferPrivilegeRetry(err) {
			return fmt.Errorf("append mount entry to /etc/fstab: %w", err)
		}
	} else if !shouldOfferPrivilegeRetry(err) {
		return fmt.Errorf("open /etc/fstab for update: %w", err)
	}

	a.println("")
	a.println("Updating /etc/fstab needs administrator permission.")
	a.println("I can try again using sudo so you can enter your admin password.")
	a.flush()

	retry, err := a.confirm("Retry updating /etc/fstab with sudo", true)
	if err != nil {
		return err
	}
	if !retry {
		return errors.New("updating /etc/fstab was cancelled")
	}

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

	if err := a.runner.Run("sudo", "sh", "-c", fmt.Sprintf("cat %s >> /etc/fstab", shellQuote(tempPath))); err != nil {
		return fmt.Errorf("append mount entry to /etc/fstab with sudo: %w", 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.println("There are no shares to select.")
		return nil, nil
	}

	raw, err := a.prompt("Share number")
	if err != nil {
		return nil, err
	}

	index, err := strconv.Atoi(raw)
	if err != nil || index < 1 || index > len(shares) {
		a.println("Invalid selection.")
		return nil, nil
	}

	return shares[index-1], 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.printf("%s: ", label)
	a.flush()
	text, err := a.reader.ReadString('\n')
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(text), nil
}

func (a *App) promptDefault(label, defaultValue string) (string, error) {
	if defaultValue == "" {
		return a.prompt(label)
	}

	a.printf("%s [%s]: ", label, defaultValue)
	a.flush()
	text, err := a.reader.ReadString('\n')
	if err != nil {
		return "", err
	}
	text = strings.TrimSpace(text)
	if text == "" {
		return defaultValue, nil
	}
	return text, nil
}

func (a *App) confirm(label string, defaultYes bool) (bool, error) {
	suffix := "y/N"
	if defaultYes {
		suffix = "Y/n"
	}

	answer, err := a.prompt(fmt.Sprintf("%s [%s]", label, suffix))
	if err != nil {
		return false, err
	}
	if answer == "" {
		return defaultYes, nil
	}

	switch strings.ToLower(answer) {
	case "y", "yes":
		return true, nil
	case "n", "no":
		return false, nil
	default:
		return false, errors.New("expected yes or no")
	}
}

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 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, "'", `'\''`) + "'"
}

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")
}
