package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/signal"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
	"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)

type ipcTargets map[string]map[string][]string

// getProcessExitCode returns the exit code from a ProcessState.
// For normal exits, returns the exit code directly.
// For signal termination, returns 128 + signal number (Unix convention).
func getProcessExitCode(state *os.ProcessState) int {
	if state == nil {
		return 1
	}
	if code := state.ExitCode(); code != -1 {
		return code
	}
	// Process was killed by signal - extract signal number
	if status, ok := state.Sys().(syscall.WaitStatus); ok {
		if status.Signaled() {
			return 128 + int(status.Signal())
		}
	}
	return 1
}

var isSessionManaged bool

func execDetachedRestart(targetPID int) {
	selfPath, err := os.Executable()
	if err != nil {
		return
	}

	cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setsid: true,
	}
	cmd.Start()
}

func runDetachedRestart(targetPIDStr string) {
	targetPID, err := strconv.Atoi(targetPIDStr)
	if err != nil {
		return
	}

	time.Sleep(200 * time.Millisecond)

	proc, err := os.FindProcess(targetPID)
	if err == nil {
		proc.Signal(syscall.SIGTERM)
	}

	time.Sleep(500 * time.Millisecond)

	killShell()
	runShellDaemon(false)
}

func getRuntimeDir() string {
	if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
		return runtime
	}
	return os.TempDir()
}

func appendLogEnv(env []string) []string {
	if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
		env = append(env, "DMS_LOG_LEVEL="+v)
	}
	if v := os.Getenv("DMS_LOG_FILE"); v != "" {
		env = append(env, "DMS_LOG_FILE="+v)
	}
	return env
}

func hasSystemdRun() bool {
	_, err := exec.LookPath("systemd-run")
	return err == nil
}

func getPIDFilePath() string {
	return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
}

func writePIDFile(childPID int) error {
	pidFile := getPIDFilePath()
	return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}

func removePIDFile() {
	pidFile := getPIDFilePath()
	os.Remove(pidFile)
}

func getAllDMSPIDs() []int {
	dir := getRuntimeDir()
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil
	}

	var pids []int

	for _, entry := range entries {
		if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
			continue
		}

		pidFile := filepath.Join(dir, entry.Name())
		data, err := os.ReadFile(pidFile)
		if err != nil {
			continue
		}

		childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
		if err != nil {
			os.Remove(pidFile)
			continue
		}

		proc, err := os.FindProcess(childPID)
		if err != nil {
			os.Remove(pidFile)
			continue
		}

		if err := proc.Signal(syscall.Signal(0)); err != nil {
			os.Remove(pidFile)
			continue
		}

		pids = append(pids, childPID)

		parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
		parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
		if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
			if parentProc, err := os.FindProcess(parentPID); err == nil {
				if err := parentProc.Signal(syscall.Signal(0)); err == nil {
					pids = append(pids, parentPID)
				}
			}
		}
	}

	return pids
}

func runShellInteractive(session bool) {
	isSessionManaged = session
	go printASCII()
	fmt.Fprintf(os.Stderr, "dms %s\n", Version)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	socketPath := server.GetSocketPath()

	configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
	if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
		log.Warnf("Failed to write config state file: %v", err)
	}
	defer os.Remove(configStateFile)

	errChan := make(chan error, 2)

	go func() {
		defer func() {
			if r := recover(); r != nil {
				errChan <- fmt.Errorf("server panic: %v", r)
			}
		}()
		server.CLIVersion = Version
		if err := server.Start(false); err != nil {
			errChan <- fmt.Errorf("server error: %w", err)
		}
	}()

	ensureFontCache()
	log.Infof("Spawning quickshell with -p %s", configPath)

	cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
	cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
	if os.Getenv("QT_LOGGING_RULES") == "" {
		if qtRules := log.GetQtLoggingRules(); qtRules != "" {
			cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
		}
	}

	if isSessionManaged && hasSystemdRun() {
		cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
	}

	homeDir, err := os.UserHomeDir()
	if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
		if !strings.HasPrefix(configPath, homeDir) {
			cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
		}
	}

	if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
	}
	if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
	}
	if os.Getenv("QT_QPA_PLATFORM") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
	}

	cmd.Env = appendLogEnv(cmd.Env)

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	tracker := &stderrTracker{parent: os.Stderr}
	cmd.Stderr = tracker

	startTime := time.Now()
	if err := cmd.Start(); err != nil {
		log.Fatalf("Error starting quickshell: %v", err)
	}

	// Write PID file for the quickshell child process
	if err := writePIDFile(cmd.Process.Pid); err != nil {
		log.Warnf("Failed to write PID file: %v", err)
	}
	defer removePIDFile()

	defer func() {
		if cmd.Process != nil {
			cmd.Process.Signal(syscall.SIGTERM)
		}
	}()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)

	go func() {
		if err := cmd.Wait(); err != nil {
			errChan <- fmt.Errorf("quickshell exited: %w", err)
		} else {
			errChan <- fmt.Errorf("quickshell exited")
		}
	}()

	for {
		select {
		case sig := <-sigChan:
			if sig == syscall.SIGUSR1 {
				if isSessionManaged {
					log.Infof("Received SIGUSR1, exiting for systemd restart...")
					cancel()
					cmd.Process.Signal(syscall.SIGTERM)
					os.Remove(socketPath)
					os.Exit(1)
				}
				log.Infof("Received SIGUSR1, spawning detached restart process...")
				execDetachedRestart(os.Getpid())
				return
			}

			// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
			select {
			case <-errChan:
				cancel()
				os.Remove(socketPath)
				exitCode := getProcessExitCode(cmd.ProcessState)
				logStartupFailure(startTime, exitCode, tracker)
				os.Exit(exitCode)
			case <-time.After(500 * time.Millisecond):
			}

			log.Infof("\nReceived signal %v, shutting down...", sig)
			cancel()
			cmd.Process.Signal(syscall.SIGTERM)
			os.Remove(socketPath)
			return

		case err := <-errChan:
			log.Error(err)
			cancel()
			if cmd.Process != nil {
				cmd.Process.Signal(syscall.SIGTERM)
			}
			os.Remove(socketPath)
			exitCode := getProcessExitCode(cmd.ProcessState)
			logStartupFailure(startTime, exitCode, tracker)
			os.Exit(exitCode)
		}
	}
}

func restartShell() {
	pids := getAllDMSPIDs()

	if len(pids) == 0 {
		log.Info("No running DMS shell instances found. Starting daemon...")
		runShellDaemon(false)
		return
	}

	currentPid := os.Getpid()
	uniquePids := make(map[int]bool)

	for _, pid := range pids {
		if pid != currentPid {
			uniquePids[pid] = true
		}
	}

	for pid := range uniquePids {
		proc, err := os.FindProcess(pid)
		if err != nil {
			log.Errorf("Error finding process %d: %v", pid, err)
			continue
		}

		if err := proc.Signal(syscall.Signal(0)); err != nil {
			continue
		}

		if err := proc.Signal(syscall.SIGUSR1); err != nil {
			log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
		} else {
			log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
		}
	}
}

func killShell() {
	pids := getAllDMSPIDs()

	if len(pids) == 0 {
		log.Info("No running DMS shell instances found.")
		return
	}

	currentPid := os.Getpid()
	uniquePids := make(map[int]bool)

	for _, pid := range pids {
		if pid != currentPid {
			uniquePids[pid] = true
		}
	}

	for pid := range uniquePids {
		proc, err := os.FindProcess(pid)
		if err != nil {
			log.Errorf("Error finding process %d: %v", pid, err)
			continue
		}

		if err := proc.Signal(syscall.Signal(0)); err != nil {
			continue
		}

		if err := proc.Kill(); err != nil {
			log.Errorf("Error killing process %d: %v", pid, err)
		} else {
			log.Infof("Killed DMS process with PID %d", pid)
		}
	}

	dir := getRuntimeDir()
	entries, err := os.ReadDir(dir)
	if err != nil {
		return
	}

	for _, entry := range entries {
		if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") {
			pidFile := filepath.Join(dir, entry.Name())
			os.Remove(pidFile)
		}
	}
}

func runShellDaemon(session bool) {
	isSessionManaged = session
	isDaemonChild := slices.Contains(os.Args, "--daemon-child")

	if !isDaemonChild {
		fmt.Fprintf(os.Stderr, "dms %s\n", Version)

		cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
		cmd.Env = os.Environ()

		cmd.SysProcAttr = &syscall.SysProcAttr{
			Setsid: true,
		}

		if err := cmd.Start(); err != nil {
			log.Fatalf("Error starting daemon: %v", err)
		}

		log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
		return
	}

	fmt.Fprintf(os.Stderr, "dms %s\n", Version)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	socketPath := server.GetSocketPath()

	configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
	if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
		log.Warnf("Failed to write config state file: %v", err)
	}
	defer os.Remove(configStateFile)

	errChan := make(chan error, 2)

	go func() {
		defer func() {
			if r := recover(); r != nil {
				errChan <- fmt.Errorf("server panic: %v", r)
			}
		}()
		server.CLIVersion = Version
		if err := server.Start(false); err != nil {
			errChan <- fmt.Errorf("server error: %w", err)
		}
	}()

	ensureFontCache()
	log.Infof("Spawning quickshell with -p %s", configPath)

	cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
	cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
	if os.Getenv("QT_LOGGING_RULES") == "" {
		if qtRules := log.GetQtLoggingRules(); qtRules != "" {
			cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
		}
	}

	// ! TODO - remove when QS 0.3 is up and we can use the pragma
	cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")

	if isSessionManaged && hasSystemdRun() {
		cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
	}

	homeDir, err := os.UserHomeDir()
	if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
		if !strings.HasPrefix(configPath, homeDir) {
			cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
		}
	}

	if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
	}
	if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
	}
	if os.Getenv("QT_QPA_PLATFORM") == "" {
		cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
	}

	cmd.Env = appendLogEnv(cmd.Env)

	devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
	if err != nil {
		log.Fatalf("Error opening /dev/null: %v", err)
	}
	defer devNull.Close()

	cmd.Stdin = devNull
	cmd.Stdout = devNull
	tracker := &stderrTracker{parent: devNull}
	cmd.Stderr = tracker

	startTime := time.Now()
	if err := cmd.Start(); err != nil {
		log.Fatalf("Error starting daemon: %v", err)
	}

	// Write PID file for the quickshell child process
	if err := writePIDFile(cmd.Process.Pid); err != nil {
		log.Warnf("Failed to write PID file: %v", err)
	}
	defer removePIDFile()

	defer func() {
		if cmd.Process != nil {
			cmd.Process.Signal(syscall.SIGTERM)
		}
	}()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)

	go func() {
		if err := cmd.Wait(); err != nil {
			errChan <- fmt.Errorf("quickshell exited: %w", err)
		} else {
			errChan <- fmt.Errorf("quickshell exited")
		}
	}()

	for {
		select {
		case sig := <-sigChan:
			if sig == syscall.SIGUSR1 {
				if isSessionManaged {
					log.Infof("Received SIGUSR1, exiting for systemd restart...")
					cancel()
					cmd.Process.Signal(syscall.SIGTERM)
					os.Remove(socketPath)
					os.Exit(1)
				}
				log.Infof("Received SIGUSR1, spawning detached restart process...")
				execDetachedRestart(os.Getpid())
				return
			}

			// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
			select {
			case <-errChan:
				cancel()
				os.Remove(socketPath)
				exitCode := getProcessExitCode(cmd.ProcessState)
				logStartupFailure(startTime, exitCode, tracker)
				os.Exit(exitCode)
			case <-time.After(500 * time.Millisecond):
			}

			cancel()
			cmd.Process.Signal(syscall.SIGTERM)
			os.Remove(socketPath)
			return

		case <-errChan:
			cancel()
			if cmd.Process != nil {
				cmd.Process.Signal(syscall.SIGTERM)
			}
			os.Remove(socketPath)
			exitCode := getProcessExitCode(cmd.ProcessState)
			logStartupFailure(startTime, exitCode, tracker)
			os.Exit(exitCode)
		}
	}
}

var qsHasAnyDisplay = sync.OnceValue(func() bool {
	out, err := exec.Command("qs", "ipc", "--help").Output()
	if err != nil {
		return false
	}
	return strings.Contains(string(out), "--any-display")
})

func parseTargetsFromIPCShowOutput(output string) ipcTargets {
	targets := make(ipcTargets)
	var currentTarget string
	for line := range strings.SplitSeq(output, "\n") {
		if after, ok := strings.CutPrefix(line, "target "); ok {
			currentTarget = strings.TrimSpace(after)
			targets[currentTarget] = make(map[string][]string)
		}
		if strings.HasPrefix(line, "  function") && currentTarget != "" {
			argsList := []string{}
			currentFunc := strings.TrimPrefix(line, "  function ")
			funcDef := strings.SplitN(currentFunc, "(", 2)
			argList := strings.SplitN(funcDef[1], ")", 2)[0]
			args := strings.Split(argList, ",")
			if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
				argsList = append(argsList, funcDef[0])
				for _, arg := range args {
					argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0]
					argsList = append(argsList, argName)
				}
				targets[currentTarget][funcDef[0]] = argsList
			} else {
				targets[currentTarget][funcDef[0]] = make([]string, 0)
			}
		}
	}
	return targets
}

func buildQsIPCBaseArgs() ([]string, error) {
	cmdArgs := []string{"ipc"}
	switch pid, ok := getFirstDMSPID(); {
	case ok:
		cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
	default:
		if err := findConfig(nil, nil); err != nil {
			return nil, err
		}
		if qsHasAnyDisplay() {
			cmdArgs = append(cmdArgs, "--any-display")
		}
		cmdArgs = append(cmdArgs, "-p", configPath)
	}
	return cmdArgs, nil
}

func getShellIPCCompletions(args []string, _ string) []string {
	baseArgs, err := buildQsIPCBaseArgs()
	if err != nil {
		log.Debugf("Error building IPC args for completions: %v", err)
		return nil
	}
	cmdArgs := append(baseArgs, "show")
	cmd := exec.Command("qs", cmdArgs...)
	var targets ipcTargets

	if output, err := cmd.Output(); err == nil {
		targets = parseTargetsFromIPCShowOutput(string(output))
	} else {
		log.Debugf("Error getting IPC show output for completions: %v", err)
		return nil
	}

	if len(args) > 0 && args[0] == "call" {
		args = args[1:]
	}

	if len(args) == 0 {
		targetNames := make([]string, 0)
		targetNames = append(targetNames, "call", "list")
		for k := range targets {
			targetNames = append(targetNames, k)
		}
		return targetNames
	}
	if len(args) == 1 {
		if targetFuncs, ok := targets[args[0]]; ok {
			funcNames := make([]string, 0)
			for k := range targetFuncs {
				funcNames = append(funcNames, k)
			}
			return funcNames
		}
		return nil
	}
	if len(args) <= len(targets[args[0]]) {
		funcArgs := targets[args[0]][args[1]]
		if len(funcArgs) >= len(args) {
			return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])}
		}
	}

	return nil
}

func getFirstDMSPID() (int, bool) {
	dir := getRuntimeDir()
	entries, err := os.ReadDir(dir)
	if err != nil {
		return 0, false
	}

	for _, entry := range entries {
		if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
			continue
		}

		data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
		if err != nil {
			continue
		}

		pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
		if err != nil {
			continue
		}

		proc, err := os.FindProcess(pid)
		if err != nil {
			continue
		}

		if proc.Signal(syscall.Signal(0)) != nil {
			continue
		}

		return pid, true
	}

	return 0, false
}

func runShellIPCCommand(args []string) {
	if len(args) == 0 {
		printIPCHelp()
		return
	}

	if args[0] != "call" {
		args = append([]string{"call"}, args...)
	}

	baseArgs, err := buildQsIPCBaseArgs()
	if err != nil {
		log.Fatalf("Error finding config: %v", err)
	}
	cmdArgs := append(baseArgs, args...)
	cmd := exec.Command("qs", cmdArgs...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		log.Fatalf("Error running IPC command: %v", err)
	}
}

func printIPCHelp() {
	fmt.Println("Usage: dms ipc call <target> <function> [args...]")
	fmt.Println()

	baseArgs, err := buildQsIPCBaseArgs()
	if err != nil {
		printIPCHelpFailure(err)
		return
	}
	cmdArgs := append(baseArgs, "show")
	cmd := exec.Command("qs", cmdArgs...)

	output, err := cmd.Output()
	if err != nil {
		printIPCHelpFailure(err)
		return
	}

	targets := parseTargetsFromIPCShowOutput(string(output))
	if len(targets) == 0 {
		fmt.Println("No IPC targets available")
		return
	}

	fmt.Println("Targets:")

	targetNames := make([]string, 0, len(targets))
	for name := range targets {
		targetNames = append(targetNames, name)
	}
	slices.Sort(targetNames)

	for _, targetName := range targetNames {
		funcs := targets[targetName]
		funcNames := make([]string, 0, len(funcs))
		for fn := range funcs {
			funcNames = append(funcNames, fn)
		}
		slices.Sort(funcNames)
		fmt.Printf("  %-16s %s\n", targetName, strings.Join(funcNames, ", "))
	}
}

func printIPCHelpFailure(err error) {
	fmt.Println("Could not retrieve IPC targets.")
	if err != nil {
		fmt.Printf("  %v\n", err)
	}
	fmt.Println()
	fmt.Println("  Full docs:  https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
	fmt.Println("  Try:        dms ipc call <target> <function>")
}

// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() {
	if _, err := exec.LookPath("fc-list"); err != nil {
		return
	}
	if _, err := exec.LookPath("fc-cache"); err != nil {
		return
	}

	var fontsToCheck []string

	if configDir, err := os.UserConfigDir(); err == nil {
		settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
		if data, err := os.ReadFile(settingsPath); err == nil {
			var settings struct {
				FontFamily     string `json:"fontFamily"`
				MonoFontFamily string `json:"monoFontFamily"`
			}
			if err := json.Unmarshal(data, &settings); err == nil {
				if settings.FontFamily != "" && settings.FontFamily != "Inter Variable" {
					fontsToCheck = append(fontsToCheck, settings.FontFamily)
				}
				if settings.MonoFontFamily != "" && settings.MonoFontFamily != "Fira Code" {
					fontsToCheck = append(fontsToCheck, settings.MonoFontFamily)
				}
			}
		}
	}

	if len(fontsToCheck) == 0 {
		return
	}

	output, err := exec.Command("fc-list", ":", "family").Output()
	if err != nil || len(strings.TrimSpace(string(output))) == 0 {
		log.Warnf("Font cache appears empty or corrupt, rebuilding...")
		rebuildFontCache()
		return
	}

	cacheFonts := strings.ToLower(string(output))
	var missing []string
	for _, font := range fontsToCheck {
		if !fontInCache(strings.ToLower(font), cacheFonts) {
			missing = append(missing, font)
		}
	}

	if len(missing) > 0 {
		log.Warnf("Font(s) not found in cache: %s — rebuilding...", strings.Join(missing, ", "))
		rebuildFontCache()
	}
}

func fontInCache(target, cache string) bool {
	for _, line := range strings.Split(cache, "\n") {
		for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
			if strings.TrimSpace(fam) == target {
				return true
			}
		}
	}
	return false
}

func rebuildFontCache() {
	cmd := exec.Command("fc-cache", "-f")
	if output, err := cmd.CombinedOutput(); err != nil {
		log.Warnf("Failed to rebuild font cache: %v\n%s", err, string(output))
	} else {
		log.Infof("Font cache rebuilt successfully")
	}
}

type stderrTracker struct {
	mu     sync.Mutex
	buf    strings.Builder
	parent io.Writer
}

func (s *stderrTracker) Write(p []byte) (n int, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.buf.Len() < 8192 {
		s.buf.Write(p)
	}
	if s.parent != nil {
		return s.parent.Write(p)
	}
	return len(p), nil
}

func (s *stderrTracker) String() string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.buf.String()
}

// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
	if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
		return
	}
	if containsFontCrashSignature(tracker.String()) {
		log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
	} else {
		log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
	}
}

func containsFontCrashSignature(logStr string) bool {
	logStr = strings.ToLower(logStr)
	signatures := []string{
		"fontconfig",
		"freetype",
		"ft_load_glyph",
		"ft_face",
		"fc-list",
		"fc-cache",
		"glyph",
		"typeface",
	}
	for _, sig := range signatures {
		if strings.Contains(logStr, sig) {
			return true
		}
	}
	return false
}
