// SPDX-License-Identifier: GPL-3.0-or-later

//go:build darwin

package service

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// launchdManager implements Manager for macOS launchd.
type launchdManager struct{}

// plistLabel is the reverse-DNS label for the launchd plist.
const plistLabel = "com.privatebackup.backend"

// plist represents a macOS property list for launchd.
type plist struct {
	Label            string
	ProgramArguments []string
	RunAtLoad        bool
	KeepAlive        bool
	WorkingDirectory string
	StandardOutPath  string
	StandardErrPath  string
	EnvironmentVars  map[string]string
	ThrottleInterval int
}

// renderPlist generates the XML plist content.
func renderPlist(p plist) ([]byte, error) {
	var buf bytes.Buffer
	buf.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
`)
	writeKey := func(key, val string) {
		buf.WriteString("\t<key>" + xmlEscape(key) + "</key>\n")
		buf.WriteString("\t<string>" + xmlEscape(val) + "</string>\n")
	}
	writeBool := func(key string, val bool) {
		buf.WriteString("\t<key>" + xmlEscape(key) + "</key>\n")
		if val {
			buf.WriteString("\t<true/>\n")
		} else {
			buf.WriteString("\t<false/>\n")
		}
	}
	writeInt := func(key string, val int) {
		buf.WriteString("\t<key>" + xmlEscape(key) + "</key>\n")
		buf.WriteString(fmt.Sprintf("\t<integer>%d</integer>\n", val))
	}

	writeKey("Label", p.Label)

	// ProgramArguments
	buf.WriteString("\t<key>ProgramArguments</key>\n\t<array>\n")
	for _, arg := range p.ProgramArguments {
		buf.WriteString("\t\t<string>" + xmlEscape(arg) + "</string>\n")
	}
	buf.WriteString("\t</array>\n")

	writeBool("RunAtLoad", p.RunAtLoad)
	writeBool("KeepAlive", p.KeepAlive)

	if p.WorkingDirectory != "" {
		writeKey("WorkingDirectory", p.WorkingDirectory)
	}
	if p.StandardOutPath != "" {
		writeKey("StandardOutPath", p.StandardOutPath)
	}
	if p.StandardErrPath != "" {
		writeKey("StandardErrorPath", p.StandardErrPath)
	}

	if p.ThrottleInterval > 0 {
		writeInt("ThrottleInterval", p.ThrottleInterval)
	}

	// Environment variables
	if len(p.EnvironmentVars) > 0 {
		buf.WriteString("\t<key>EnvironmentVariables</key>\n\t<dict>\n")
		for k, v := range p.EnvironmentVars {
			buf.WriteString("\t\t<key>" + xmlEscape(k) + "</key>\n")
			buf.WriteString("\t\t<string>" + xmlEscape(v) + "</string>\n")
		}
		buf.WriteString("\t</dict>\n")
	}

	buf.WriteString("</dict>\n</plist>\n")
	return buf.Bytes(), nil
}

func xmlEscape(s string) string {
	var buf bytes.Buffer
	if err := xml.EscapeText(&buf, []byte(s)); err != nil {
		return s
	}
	return buf.String()
}

func (m *launchdManager) InitSystem() InitSystem {
	return InitLaunchd
}

// plistPath returns the path to the plist file for the given level.
func (m *launchdManager) plistPath(level ServiceLevel) string {
	if level == LevelSystem {
		return filepath.Join("/Library/LaunchDaemons", plistLabel+".plist")
	}
	homeDir, _ := os.UserHomeDir()
	return filepath.Join(homeDir, "Library", "LaunchAgents", plistLabel+".plist")
}

// installedLevel determines which level the service is installed at.
func (m *launchdManager) installedLevel() ServiceLevel {
	if _, err := os.Stat(m.plistPath(LevelSystem)); err == nil {
		return LevelSystem
	}
	return LevelUser
}

func (m *launchdManager) IsInstalled() bool {
	if _, err := os.Stat(m.plistPath(LevelSystem)); err == nil {
		return true
	}
	if _, err := os.Stat(m.plistPath(LevelUser)); err == nil {
		return true
	}
	return false
}

func (m *launchdManager) Install(cfg Config) error {
	if m.IsInstalled() {
		return ErrServiceAlreadyInstalled
	}

	if cfg.Level == LevelSystem && !isRoot() {
		return ErrInsufficientPrivileges
	}

	plistFile := m.plistPath(cfg.Level)

	// Ensure parent directory exists.
	if err := os.MkdirAll(filepath.Dir(plistFile), 0755); err != nil {
		return fmt.Errorf("failed to create plist directory: %w", err)
	}

	// Determine log paths.
	homeDir, _ := os.UserHomeDir()
	logDir := filepath.Join(homeDir, "Library", "Logs", "PrivateBackup")
	if err := os.MkdirAll(logDir, 0755); err != nil {
		return fmt.Errorf("failed to create log directory: %w", err)
	}

	stdoutLog := filepath.Join(logDir, "backend.log")
	stderrLog := filepath.Join(logDir, "backend-error.log")
	if cfg.LogPath != "" {
		stdoutLog = cfg.LogPath
		stderrLog = cfg.LogPath
	}

	// Build program arguments.
	progArgs := []string{cfg.BinaryPath}
	progArgs = append(progArgs, cfg.Args...)

	p := plist{
		Label:            plistLabel,
		ProgramArguments: progArgs,
		RunAtLoad:        true,
		KeepAlive:        true,
		WorkingDirectory: cfg.WorkingDirectory,
		StandardOutPath:  stdoutLog,
		StandardErrPath:  stderrLog,
		EnvironmentVars:  cfg.Environment,
		ThrottleInterval: 10, // minimum seconds between restarts
	}

	content, err := renderPlist(p)
	if err != nil {
		return fmt.Errorf("failed to render plist: %w", err)
	}

	if err := os.WriteFile(plistFile, content, 0644); err != nil {
		return fmt.Errorf("failed to write plist: %w", err)
	}

	return nil
}

func (m *launchdManager) Uninstall() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}

	level := m.installedLevel()
	plistFile := m.plistPath(level)

	// Unload first (stop + remove from launchd).
	_ = m.launchctl("unload", plistFile)

	// Remove plist file.
	if err := os.Remove(plistFile); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to remove plist: %w", err)
	}

	return nil
}

func (m *launchdManager) Start() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}
	level := m.installedLevel()
	return m.launchctl("load", "-w", m.plistPath(level))
}

func (m *launchdManager) Stop() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}
	level := m.installedLevel()
	return m.launchctl("unload", m.plistPath(level))
}

func (m *launchdManager) Restart() error {
	// launchd doesn't have a native restart; stop then start.
	if err := m.Stop(); err != nil {
		return err
	}
	return m.Start()
}

func (m *launchdManager) Status() (Status, error) {
	st := Status{
		InitSystem: InitLaunchd,
	}

	if !m.IsInstalled() {
		return st, nil
	}
	st.Installed = true

	level := m.installedLevel()
	if level == LevelSystem {
		st.Level = "system"
	} else {
		st.Level = "user"
	}
	st.UnitName = plistLabel

	// Check if loaded/running via launchctl list.
	out, err := exec.Command("launchctl", "list").Output()
	if err == nil {
		for _, line := range strings.Split(string(out), "\n") {
			if strings.Contains(line, plistLabel) {
				st.Running = true
				st.Enabled = true // If loaded, it's "enabled" since RunAtLoad=true.
				// Parse PID from the first column (may be "-" if not running).
				fields := strings.Fields(line)
				if len(fields) >= 1 && fields[0] != "-" {
					if pid := 0; true {
						fmt.Sscanf(fields[0], "%d", &pid)
						if pid > 0 {
							st.PID = pid
						}
					}
				}
				break
			}
		}
	}

	// If the plist exists with RunAtLoad, it's enabled even if not currently loaded.
	if !st.Enabled {
		st.Enabled = true // plist exists → will load on next login/boot.
	}

	return st, nil
}

// launchctl runs a launchctl command.
func (m *launchdManager) launchctl(args ...string) error {
	cmd := exec.Command("launchctl", args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}
