//go:build linux

package daemon

import (
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
)

const (
	systemdServiceName = ServiceName + ".service"
)

type systemdManager struct {
	system bool // true = system-level (/etc/systemd/system), false = user-level (~/.config/systemd/user)
}

func newPlatformManager() (Manager, error) {
	if _, err := exec.LookPath("systemctl"); err != nil {
		return nil, fmt.Errorf("systemctl not found: systemd is required on Linux; if running in a container without systemd, use nohup, tmux, or screen instead")
	}

	isRoot := os.Getuid() == 0

	if isRoot {
		if err := checkSystemdRunning(true); err != nil {
			return nil, err
		}
		return &systemdManager{system: true}, nil
	}

	if err := checkSystemdRunning(false); err != nil {
		return nil, err
	}
	return &systemdManager{system: false}, nil
}

func (m *systemdManager) Platform() string {
	if m.system {
		return "systemd (system)"
	}
	return "systemd (user)"
}

func (m *systemdManager) Install(cfg Config) error {
	unitPath := m.unitPath()

	if err := os.MkdirAll(filepath.Dir(unitPath), 0755); err != nil {
		return fmt.Errorf("create systemd dir: %w", err)
	}
	if err := os.MkdirAll(filepath.Dir(cfg.LogFile), 0755); err != nil {
		return fmt.Errorf("create log dir: %w", err)
	}

	unit := m.buildUnit(cfg)
	// 0600: unit file may contain captured secret values (config.toml ${ENV}
	// placeholders and any EnvDiscoverer extension output). For system-level
	// units (/etc/systemd/system/) the file is owned by root and remains
	// readable by root only; for user-level units under
	// ~/.config/systemd/user it remains owner-only. WriteFile only applies
	// perm on create, so Chmod afterwards is required to harden reinstalls
	// of pre-existing 0644 units from earlier cc-connect versions.
	if err := os.WriteFile(unitPath, []byte(unit), 0600); err != nil {
		return fmt.Errorf("write unit file: %w", err)
	}
	if err := os.Chmod(unitPath, 0600); err != nil {
		return fmt.Errorf("chmod unit file: %w", err)
	}

	for _, cmdArgs := range [][]string{
		m.sysArgs("daemon-reload"),
		m.sysArgs("enable", systemdServiceName),
		m.sysArgs("restart", systemdServiceName),
	} {
		if out, err := runSystemctl(cmdArgs...); err != nil {
			return fmt.Errorf("systemctl %s: %s (%w)", strings.Join(cmdArgs, " "), out, err)
		}
	}

	return nil
}

func (m *systemdManager) Uninstall() error {
	if _, err := runSystemctl(m.sysArgs("disable", "--now", systemdServiceName)...); err != nil {
		slog.Warn("systemd: disable failed", "error", err)
	}

	unitPath := m.unitPath()
	if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("remove unit: %w", err)
	}

	if _, err := runSystemctl(m.sysArgs("daemon-reload")...); err != nil {
		slog.Warn("systemd: daemon-reload failed", "error", err)
	}
	return nil
}

func (m *systemdManager) Start() error {
	out, err := runSystemctl(m.sysArgs("start", systemdServiceName)...)
	if err != nil {
		return fmt.Errorf("start: %s (%w)", out, err)
	}
	return nil
}

func (m *systemdManager) Stop() error {
	out, err := runSystemctl(m.sysArgs("stop", systemdServiceName)...)
	if err != nil {
		return fmt.Errorf("stop: %s (%w)", out, err)
	}
	return nil
}

func (m *systemdManager) Restart() error {
	out, err := runSystemctl(m.sysArgs("restart", systemdServiceName)...)
	if err != nil {
		return fmt.Errorf("restart: %s (%w)", out, err)
	}
	return nil
}

func (m *systemdManager) Status() (*Status, error) {
	st := &Status{Platform: m.Platform()}

	unitPath := m.unitPath()
	if _, err := os.Stat(unitPath); err != nil {
		return st, nil
	}
	st.Installed = true

	out, err := runSystemctl(m.sysArgs("show", systemdServiceName,
		"--no-page", "--property", "ActiveState,MainPID")...)
	if err != nil {
		return st, nil
	}

	props := parseKeyValue(out)
	if strings.EqualFold(props["ActiveState"], "active") {
		st.Running = true
	}
	if pid, err := strconv.Atoi(props["MainPID"]); err == nil && pid > 0 {
		st.PID = pid
	}
	return st, nil
}

// ── helpers ─────────────────────────────────────────────────

// sysArgs prepends --user flag for user-level managers.
func (m *systemdManager) sysArgs(args ...string) []string {
	if m.system {
		return args
	}
	return append([]string{"--user"}, args...)
}

func (m *systemdManager) unitPath() string {
	if m.system {
		return filepath.Join("/etc/systemd/system", systemdServiceName)
	}
	home, _ := os.UserHomeDir()
	return filepath.Join(home, ".config", "systemd", "user", systemdServiceName)
}

func (m *systemdManager) buildUnit(cfg Config) string {
	var sb strings.Builder
	sb.WriteString("[Unit]\n")
	sb.WriteString("Description=cc-connect - AI Agent Chat Bridge\n")
	sb.WriteString("After=network-online.target\n")
	sb.WriteString("Wants=network-online.target\n\n")

	sb.WriteString("[Service]\n")
	sb.WriteString("Type=simple\n")
	fmt.Fprintf(&sb, "ExecStart=%s\n", cfg.BinaryPath)
	fmt.Fprintf(&sb, "WorkingDirectory=%s\n", cfg.WorkDir)
	sb.WriteString("Restart=on-failure\n")
	sb.WriteString("RestartSec=10\n")
	fmt.Fprintf(&sb, "Environment=\"CC_LOG_FILE=%s\"\n", cfg.LogFile)
	fmt.Fprintf(&sb, "Environment=\"CC_LOG_MAX_SIZE=%d\"\n", cfg.LogMaxSize)
	fmt.Fprintf(&sb, "Environment=\"CC_LOG_MAX_BACKUPS=%d\"\n", cfg.LogMaxBackups)
	if cfg.EnvPATH != "" {
		fmt.Fprintf(&sb, "Environment=\"PATH=%s\"\n", cfg.EnvPATH)
	}
	if len(cfg.EnvExtra) > 0 {
		keys := make([]string, 0, len(cfg.EnvExtra))
		for key := range cfg.EnvExtra {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			if !isValidEnvName(key) {
				slog.Warn("daemon: systemd: dropping invalid env name from EnvExtra",
					"key", key)
				continue
			}
			value := cfg.EnvExtra[key]
			if value == "" {
				continue
			}
			fmt.Fprintf(&sb, "Environment=\"%s=%s\"\n", key, escapeSystemdEnvValue(value))
		}
	}
	sb.WriteString("\n[Install]\n")
	if m.system {
		sb.WriteString("WantedBy=multi-user.target\n")
	} else {
		sb.WriteString("WantedBy=default.target\n")
	}
	return sb.String()
}

// escapeSystemdEnvValue prepares a value for inclusion inside the double
// quotes of an `Environment="KEY=VALUE"` directive. Per systemd.exec(5),
// backslashes and double quotes need escaping; literal newlines and tabs
// must be encoded as `\n` / `\t` so the unit file remains a single line.
func escapeSystemdEnvValue(v string) string {
	var b strings.Builder
	b.Grow(len(v))
	for _, r := range v {
		switch r {
		case '\\':
			b.WriteString(`\\`)
		case '"':
			b.WriteString(`\"`)
		case '\n':
			b.WriteString(`\n`)
		case '\r':
			b.WriteString(`\r`)
		case '\t':
			b.WriteString(`\t`)
		default:
			b.WriteRune(r)
		}
	}
	return b.String()
}

// runSystemctl is a var (not a func) so tests can stub it without
// requiring a real systemctl process on the test host.
var runSystemctl = func(args ...string) (string, error) {
	cmd := exec.Command("systemctl", args...)
	out, err := cmd.CombinedOutput()
	return strings.TrimSpace(string(out)), err
}

func checkSystemdRunning(system bool) error {
	var args []string
	if system {
		args = []string{"is-system-running"}
	} else {
		args = []string{"--user", "is-system-running"}
	}

	out, _ := runSystemctl(args...)
	state := strings.TrimSpace(strings.ToLower(out))

	// These states all mean systemd is usable for managing services
	switch state {
	case "running", "degraded", "starting", "initializing":
		return nil
	}

	// "offline" = systemd exists but is not PID 1 (WSL2 without systemd, some containers)
	// "not been booted" / empty = no systemd at all
	wsl := isWSL2()

	if system {
		if wsl {
			return fmt.Errorf("systemd is not active in this WSL2 instance.\n" +
				"  Add the following to /etc/wsl.conf and restart WSL (wsl --shutdown):\n" +
				"    [boot]\n" +
				"    systemd=true\n" +
				"  Or use: nohup cc-connect > cc-connect.log 2>&1 &")
		}
		if state == "offline" || strings.Contains(state, "not been booted") {
			return fmt.Errorf("systemd is not active (state: %s).\n"+
				"  If running in a container, systemd is typically not available.\n"+
				"  Use nohup, tmux, or screen instead:\n"+
				"    nohup cc-connect > cc-connect.log 2>&1 &", state)
		}
		return fmt.Errorf("systemd check failed (state: %s).\n"+
			"  Use nohup as alternative: nohup cc-connect > cc-connect.log 2>&1 &", state)
	}

	// User-level failures
	if wsl {
		return fmt.Errorf("systemd user session not available in WSL2.\n" +
			"  Add the following to /etc/wsl.conf and restart WSL (wsl --shutdown):\n" +
			"    [boot]\n" +
			"    systemd=true\n" +
			"  Or use: nohup cc-connect > cc-connect.log 2>&1 &")
	}

	user := os.Getenv("USER")
	return fmt.Errorf("systemd user session not available.\n"+
		"  This often happens when connecting via SSH without a systemd login session.\n"+
		"  Try one of:\n"+
		"    1. Run as root: sudo cc-connect daemon install (uses system-level systemd)\n"+
		"    2. loginctl enable-linger %s && export XDG_RUNTIME_DIR=/run/user/$(id -u)\n"+
		"    3. Use nohup/tmux instead: nohup cc-connect > cc-connect.log 2>&1 &", user)
}

func isWSL2() bool {
	data, err := os.ReadFile("/proc/version")
	if err != nil {
		return false
	}
	lower := strings.ToLower(string(data))
	return strings.Contains(lower, "microsoft") || strings.Contains(lower, "wsl")
}

func parseKeyValue(text string) map[string]string {
	m := make(map[string]string)
	for _, line := range strings.Split(text, "\n") {
		line = strings.TrimSpace(line)
		if line == "" || !strings.Contains(line, "=") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		m[parts[0]] = parts[1]
	}
	return m
}

// CheckLinger returns true if linger is enabled for the user, false otherwise.
// If linger is not enabled, user-level systemd services will stop when
// the user's last login session ends (e.g., SSH disconnect).
func CheckLinger() (enabled bool, user string) {
	user = os.Getenv("USER")
	if user == "" {
		user = "unknown"
	}

	// Check if we're in system mode (root)
	if os.Getuid() == 0 {
		return true, user // Linger check not relevant for system mode
	}

	// Check linger status via loginctl
	out, err := exec.Command("loginctl", "show-user", user, "-p", "Linger").Output()
	if err != nil {
		// loginctl not available or error - assume linger is disabled
		slog.Debug("linger check failed", "error", err)
		return false, user
	}

	linger := strings.TrimSpace(string(out))
	return linger == "Linger=yes", user
}
