package pam

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
)

const (
	GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
	GreeterPamManagedBlockEnd   = "# END DMS GREETER AUTH"

	LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
	LockscreenPamManagedBlockEnd   = "# END DMS LOCKSCREEN AUTH"

	LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
	LockscreenU2FPamManagedBlockEnd   = "# END DMS LOCKSCREEN U2F AUTH"

	legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
	legacyGreeterPamU2FComment    = "# DMS greeter U2F"

	GreetdPamPath       = "/etc/pam.d/greetd"
	DankshellPamPath    = "/etc/pam.d/dankshell"
	DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
)

var includedPamAuthFiles = []string{
	"system-auth",
	"common-auth",
	"password-auth",
	"system-login",
	"system-local-login",
	"common-auth-pc",
	"login",
}

type AuthSettings struct {
	EnableFprint        bool `json:"enableFprint"`
	EnableU2f           bool `json:"enableU2f"`
	GreeterEnableFprint bool `json:"greeterEnableFprint"`
	GreeterEnableU2f    bool `json:"greeterEnableU2f"`
}

type SyncAuthOptions struct {
	HomeDir          string
	ForceGreeterAuth bool
}

type syncDeps struct {
	pamDir                             string
	greetdPath                         string
	dankshellPath                      string
	dankshellU2fPath                   string
	isNixOS                            func() bool
	readFile                           func(string) ([]byte, error)
	stat                               func(string) (os.FileInfo, error)
	createTemp                         func(string, string) (*os.File, error)
	removeFile                         func(string) error
	runSudoCmd                         func(string, string, ...string) error
	pamModuleExists                    func(string) bool
	fingerprintAvailableForCurrentUser func() bool
}

type lockscreenPamIncludeDirective struct {
	target     string
	filterType string
}

type lockscreenPamResolver struct {
	pamDir   string
	readFile func(string) ([]byte, error)
}

func defaultSyncDeps() syncDeps {
	return syncDeps{
		pamDir:                             "/etc/pam.d",
		greetdPath:                         GreetdPamPath,
		dankshellPath:                      DankshellPamPath,
		dankshellU2fPath:                   DankshellU2FPamPath,
		isNixOS:                            IsNixOS,
		readFile:                           os.ReadFile,
		stat:                               os.Stat,
		createTemp:                         os.CreateTemp,
		removeFile:                         os.Remove,
		runSudoCmd:                         runSudoCmd,
		pamModuleExists:                    pamModuleExists,
		fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
	}
}

func IsNixOS() bool {
	_, err := os.Stat("/etc/NIXOS")
	return err == nil
}

func ReadAuthSettings(homeDir string) (AuthSettings, error) {
	settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
	data, err := os.ReadFile(settingsPath)
	if err != nil {
		if os.IsNotExist(err) {
			return AuthSettings{}, nil
		}
		return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
	}
	if strings.TrimSpace(string(data)) == "" {
		return AuthSettings{}, nil
	}

	var settings AuthSettings
	if err := json.Unmarshal(data, &settings); err != nil {
		return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
	}
	return settings, nil
}

func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
	settings, err := ReadAuthSettings(homeDir)
	if err != nil {
		return false, false, err
	}
	return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}

func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
	return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
}

func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
	return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
}

func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
	homeDir := strings.TrimSpace(options.HomeDir)
	if homeDir == "" {
		var err error
		homeDir, err = os.UserHomeDir()
		if err != nil {
			return fmt.Errorf("failed to get user home directory: %w", err)
		}
	}

	settings, err := ReadAuthSettings(homeDir)
	if err != nil {
		return err
	}

	if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil {
		return err
	}
	if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil {
		return err
	}

	if _, err := deps.stat(deps.greetdPath); err != nil {
		if os.IsNotExist(err) {
			logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
			return nil
		}
		return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
	}

	if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
		return err
	}

	return nil
}

func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
	if deps.isNixOS() {
		return nil
	}

	data, err := deps.readFile(deps.greetdPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
	}

	originalContent := string(data)
	stripped, removed := stripManagedGreeterPamBlock(originalContent)
	strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
	if !removed && !removedLegacy {
		return nil
	}

	if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
		return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
	}

	logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
	return nil
}

func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
	if pamText == "" {
		return false, false, false, false
	}

	lines := strings.Split(pamText, "\n")
	inManaged := false
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		switch trimmed {
		case GreeterPamManagedBlockStart:
			managed = true
			inManaged = true
			continue
		case GreeterPamManagedBlockEnd:
			inManaged = false
			continue
		}

		if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
			legacy = true
		}
		if !inManaged {
			continue
		}
		if strings.Contains(trimmed, "pam_fprintd") {
			fingerprint = true
		}
		if strings.Contains(trimmed, "pam_u2f") {
			u2f = true
		}
	}

	return managed, fingerprint, u2f, legacy
}

func StripManagedGreeterPamContent(pamText string) (string, bool) {
	stripped, removed := stripManagedGreeterPamBlock(pamText)
	stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
	return stripped, removed || removedLegacy
}

func PamTextIncludesFile(pamText, filename string) bool {
	lines := strings.Split(pamText, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if strings.Contains(trimmed, filename) &&
			(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
			return true
		}
	}
	return false
}

func PamFileHasModule(pamFilePath, module string) bool {
	data, err := os.ReadFile(pamFilePath)
	if err != nil {
		return false
	}
	return pamContentHasModule(string(data), module)
}

func DetectIncludedPamModule(pamText, module string) string {
	return detectIncludedPamModule(pamText, module, defaultSyncDeps())
}

func detectIncludedPamModule(pamText, module string, deps syncDeps) string {
	for _, includedFile := range includedPamAuthFiles {
		if !PamTextIncludesFile(pamText, includedFile) {
			continue
		}
		path := filepath.Join(deps.pamDir, includedFile)
		data, err := deps.readFile(path)
		if err != nil {
			continue
		}
		if pamContentHasModule(string(data), module) {
			return includedFile
		}
	}
	return ""
}

func pamContentHasModule(content, module string) bool {
	lines := strings.Split(content, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if strings.Contains(trimmed, module) {
			return true
		}
	}
	return false
}

func hasManagedLockscreenPamFile(content string) bool {
	return strings.Contains(content, LockscreenPamManagedBlockStart) &&
		strings.Contains(content, LockscreenPamManagedBlockEnd)
}

func hasManagedLockscreenU2FPamFile(content string) bool {
	return strings.Contains(content, LockscreenU2FPamManagedBlockStart) &&
		strings.Contains(content, LockscreenU2FPamManagedBlockEnd)
}

func pamDirectiveType(line string) string {
	fields := strings.Fields(line)
	if len(fields) == 0 {
		return ""
	}

	directiveType := strings.TrimPrefix(fields[0], "-")
	switch directiveType {
	case "auth", "account", "password", "session":
		return directiveType
	default:
		return ""
	}
}

func isExcludedLockscreenPamLine(line string) bool {
	for _, field := range strings.Fields(line) {
		if strings.HasPrefix(field, "#") {
			break
		}
		if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") {
			return true
		}
	}
	return false
}

func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) {
	fields := strings.Fields(trimmed)
	if len(fields) >= 2 && fields[0] == "@include" {
		return lockscreenPamIncludeDirective{
			target:     fields[1],
			filterType: inheritedFilter,
		}, true
	}

	if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") {
		lineType := pamDirectiveType(trimmed)
		if lineType == "" {
			return lockscreenPamIncludeDirective{}, false
		}
		return lockscreenPamIncludeDirective{
			target:     fields[2],
			filterType: lineType,
		}, true
	}

	if len(fields) >= 3 && fields[1] == "@include" {
		lineType := pamDirectiveType(trimmed)
		if lineType == "" {
			return lockscreenPamIncludeDirective{}, false
		}
		return lockscreenPamIncludeDirective{
			target:     fields[2],
			filterType: lineType,
		}, true
	}

	return lockscreenPamIncludeDirective{}, false
}

func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
	if strings.TrimSpace(target) == "" {
		return "", fmt.Errorf("empty PAM include target")
	}

	cleanPamDir := filepath.Clean(pamDir)
	if filepath.IsAbs(target) {
		cleanTarget := filepath.Clean(target)
		if filepath.Dir(cleanTarget) != cleanPamDir {
			return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
		}
		return cleanTarget, nil
	}

	cleanTarget := filepath.Clean(target)
	if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
		return "", fmt.Errorf("invalid PAM include target: %s", target)
	}

	return filepath.Join(cleanPamDir, cleanTarget), nil
}

func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
	path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
	if err != nil {
		return nil, err
	}

	for _, seen := range stack {
		if seen == path {
			chain := append(append([]string{}, stack...), path)
			display := make([]string, 0, len(chain))
			for _, item := range chain {
				display = append(display, filepath.Base(item))
			}
			return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
		}
	}

	data, err := r.readFile(path)
	if err != nil {
		return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err)
	}

	var resolved []string
	for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
		rawLine = strings.TrimRight(rawLine, "\r")
		trimmed := strings.TrimSpace(rawLine)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" {
			continue
		}

		if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
			lineType := pamDirectiveType(trimmed)
			if filterType != "" && lineType != "" && lineType != filterType {
				continue
			}

			nested, err := r.resolveService(include.target, include.filterType, append(stack, path))
			if err != nil {
				return nil, err
			}
			resolved = append(resolved, nested...)
			continue
		}

		lineType := pamDirectiveType(trimmed)
		if lineType == "" {
			return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed)
		}
		if filterType != "" && lineType != filterType {
			continue
		}
		if isExcludedLockscreenPamLine(trimmed) {
			continue
		}

		resolved = append(resolved, rawLine)
	}

	return resolved, nil
}

func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
	resolver := lockscreenPamResolver{
		pamDir:   pamDir,
		readFile: readFile,
	}

	resolvedLines, err := resolver.resolveService("login", "", nil)
	if err != nil {
		return "", err
	}
	if len(resolvedLines) == 0 {
		return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
	}

	hasAuth := false
	for _, line := range resolvedLines {
		if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
			hasAuth = true
			break
		}
	}
	if !hasAuth {
		return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
	}

	var b strings.Builder
	b.WriteString("#%PAM-1.0\n")
	b.WriteString(LockscreenPamManagedBlockStart + "\n")
	for _, line := range resolvedLines {
		b.WriteString(line)
		b.WriteByte('\n')
	}
	b.WriteString(LockscreenPamManagedBlockEnd + "\n")
	return b.String(), nil
}

func buildManagedLockscreenU2FPamContent() string {
	var b strings.Builder
	b.WriteString("#%PAM-1.0\n")
	b.WriteString(LockscreenU2FPamManagedBlockStart + "\n")
	b.WriteString("auth    required    pam_u2f.so  cue nouserok timeout=10\n")
	b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n")
	return b.String()
}

func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
	if deps.isNixOS() {
		logFunc("ℹ NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
		return nil
	}

	existingData, err := deps.readFile(deps.dankshellPath)
	if err == nil {
		if !hasManagedLockscreenPamFile(string(existingData)) {
			logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.")
			return nil
		}
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
	}

	content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
	if err != nil {
		return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
	}

	if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil {
		return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err)
	}

	logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication")
	return nil
}

func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error {
	if deps.isNixOS() {
		logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.")
		return nil
	}

	existingData, err := deps.readFile(deps.dankshellU2fPath)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err)
	}

	if enabled {
		if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
			logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.")
			return nil
		}
		if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil {
			return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err)
		}
		logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication")
		return nil
	}

	if os.IsNotExist(err) {
		return nil
	}
	if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
		logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.")
		return nil
	}

	if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil {
		return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err)
	}
	logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f")
	return nil
}

func stripManagedGreeterPamBlock(content string) (string, bool) {
	lines := strings.Split(content, "\n")
	filtered := make([]string, 0, len(lines))
	inManagedBlock := false
	removed := false

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == GreeterPamManagedBlockStart {
			inManagedBlock = true
			removed = true
			continue
		}
		if trimmed == GreeterPamManagedBlockEnd {
			inManagedBlock = false
			removed = true
			continue
		}
		if inManagedBlock {
			removed = true
			continue
		}
		filtered = append(filtered, line)
	}

	return strings.Join(filtered, "\n"), removed
}

func stripLegacyGreeterPamLines(content string) (string, bool) {
	lines := strings.Split(content, "\n")
	filtered := make([]string, 0, len(lines))
	removed := false

	for i := 0; i < len(lines); i++ {
		trimmed := strings.TrimSpace(lines[i])
		if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
			removed = true
			if i+1 < len(lines) {
				nextLine := strings.TrimSpace(lines[i+1])
				if strings.HasPrefix(nextLine, "auth") &&
					(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
					i++
				}
			}
			continue
		}
		filtered = append(filtered, lines[i])
	}

	return strings.Join(filtered, "\n"), removed
}

func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
	lines := strings.Split(content, "\n")
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
			block := strings.Join(blockLines, "\n")
			prefix := strings.Join(lines[:i], "\n")
			suffix := strings.Join(lines[i:], "\n")
			switch {
			case prefix == "":
				return block + "\n" + suffix, nil
			case suffix == "":
				return prefix + "\n" + block, nil
			default:
				return prefix + "\n" + block + "\n" + suffix, nil
			}
		}
	}
	return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}

func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
	var wantFprint, wantU2f bool
	fprintToggleEnabled := forceAuth
	u2fToggleEnabled := forceAuth
	if forceAuth {
		wantFprint = deps.pamModuleExists("pam_fprintd.so")
		wantU2f = deps.pamModuleExists("pam_u2f.so")
	} else {
		fprintToggleEnabled = settings.GreeterEnableFprint
		u2fToggleEnabled = settings.GreeterEnableU2f
		fprintModule := deps.pamModuleExists("pam_fprintd.so")
		u2fModule := deps.pamModuleExists("pam_u2f.so")
		wantFprint = settings.GreeterEnableFprint && fprintModule
		wantU2f = settings.GreeterEnableU2f && u2fModule
		if settings.GreeterEnableFprint && !fprintModule {
			logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
		}
		if settings.GreeterEnableU2f && !u2fModule {
			logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
		}
	}

	if deps.isNixOS() {
		logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
		logFunc("  Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
		return nil
	}

	pamData, err := deps.readFile(deps.greetdPath)
	if err != nil {
		return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
	}
	originalContent := string(pamData)
	content, _ := stripManagedGreeterPamBlock(originalContent)
	content, _ = stripLegacyGreeterPamLines(content)

	includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
	includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
	fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
	if wantFprint && includedFprintFile != "" {
		logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
		wantFprint = false
	}
	if wantU2f && includedU2fFile != "" {
		logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
		wantU2f = false
	}
	if !wantFprint && includedFprintFile != "" {
		if fprintToggleEnabled {
			logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
			if fprintAvailableForCurrentUser {
				logFunc("  DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
			} else {
				logFunc("  No enrolled fingerprints detected for the current user; password auth remains the effective path.")
			}
		} else {
			if fprintAvailableForCurrentUser {
				logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
				logFunc("  Password login will work but may be delayed while the fingerprint module runs first.")
				logFunc("  To eliminate the delay, " + pamManagerHintForCurrentDistro())
			} else {
				logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
				logFunc("  Password auth remains the effective login path.")
			}
		}
	}
	if !wantU2f && includedU2fFile != "" {
		if u2fToggleEnabled {
			logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
			logFunc("  DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
		} else {
			logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
			logFunc("  " + pamManagerHintForCurrentDistro())
		}
	}

	if wantFprint || wantU2f {
		blockLines := []string{GreeterPamManagedBlockStart}
		if wantFprint {
			blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
		}
		if wantU2f {
			blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
		}
		blockLines = append(blockLines, GreeterPamManagedBlockEnd)

		content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
		if err != nil {
			return err
		}
	}

	if content == originalContent {
		return nil
	}

	if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
		return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
	}
	if wantFprint || wantU2f {
		logFunc("✓ Configured greetd PAM for fingerprint/U2F")
	} else {
		logFunc("✓ Cleared DMS-managed greeter PAM auth block")
	}
	return nil
}

func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
	tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
	if err != nil {
		return err
	}
	tmpPath := tmpFile.Name()
	defer func() {
		_ = deps.removeFile(tmpPath)
	}()

	if _, err := tmpFile.WriteString(content); err != nil {
		tmpFile.Close()
		return err
	}
	if err := tmpFile.Close(); err != nil {
		return err
	}
	if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil {
		return err
	}
	if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
		return fmt.Errorf("failed to set permissions on %s: %w", destPath, err)
	}
	return nil
}

func pamManagerHintForCurrentDistro() string {
	osInfo, err := distros.GetOSInfo()
	if err != nil {
		return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
	}
	config, exists := distros.Registry[osInfo.Distribution.ID]
	if !exists {
		return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
	}

	switch config.Family {
	case distros.FamilyFedora:
		return "Disable it in authselect to force password-only greeter login."
	case distros.FamilyDebian, distros.FamilyUbuntu:
		return "Disable it in pam-auth-update to force password-only greeter login."
	default:
		return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
	}
}

func pamModuleExists(module string) bool {
	for _, libDir := range []string{
		"/usr/lib64/security",
		"/usr/lib/security",
		"/lib64/security",
		"/lib/security",
		"/lib/x86_64-linux-gnu/security",
		"/usr/lib/x86_64-linux-gnu/security",
		"/lib/aarch64-linux-gnu/security",
		"/usr/lib/aarch64-linux-gnu/security",
		"/run/current-system/sw/lib64/security",
		"/run/current-system/sw/lib/security",
	} {
		if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
			return true
		}
	}
	return false
}

func hasEnrolledFingerprintOutput(output string) bool {
	lower := strings.ToLower(output)
	if strings.Contains(lower, "no fingers enrolled") ||
		strings.Contains(lower, "no fingerprints enrolled") ||
		strings.Contains(lower, "no prints enrolled") {
		return false
	}
	if strings.Contains(lower, "has fingers enrolled") ||
		strings.Contains(lower, "has fingerprints enrolled") {
		return true
	}
	for _, line := range strings.Split(lower, "\n") {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "finger:") {
			return true
		}
		if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
			return true
		}
	}
	return false
}

func FingerprintAuthAvailableForCurrentUser() bool {
	username := strings.TrimSpace(os.Getenv("SUDO_USER"))
	if username == "" {
		username = strings.TrimSpace(os.Getenv("USER"))
	}
	if username == "" {
		out, err := exec.Command("id", "-un").Output()
		if err == nil {
			username = strings.TrimSpace(string(out))
		}
	}
	return fingerprintAuthAvailableForUser(username)
}

func fingerprintAuthAvailableForUser(username string) bool {
	username = strings.TrimSpace(username)
	if username == "" {
		return false
	}
	if !pamModuleExists("pam_fprintd.so") {
		return false
	}
	if _, err := exec.LookPath("fprintd-list"); err != nil {
		return false
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
	if err != nil {
		return false
	}
	return hasEnrolledFingerprintOutput(string(out))
}

func runSudoCmd(sudoPassword string, command string, args ...string) error {
	var cmd *exec.Cmd

	if sudoPassword != "" {
		fullArgs := append([]string{command}, args...)
		quotedArgs := make([]string, len(fullArgs))
		for i, arg := range fullArgs {
			quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
		}
		cmdStr := strings.Join(quotedArgs, " ")

		cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
	} else {
		cmd = exec.Command("sudo", append([]string{command}, args...)...)
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}
