package main

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"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
}

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 {
	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("[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 "w", "write":
			return a.writeConfig(doc)
		case "q", "quit":
			return nil
		default:
			a.println("Unknown choice.")
		}
	}
}

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