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

//go:build freebsd || openbsd || netbsd || dragonfly

package service

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"text/template"
)

// rcdManager implements Manager for BSD rc.d (FreeBSD, OpenBSD, NetBSD, DragonFly).
// rc.d only supports system-level services (requires root).
type rcdManager struct{}

// rc.d script — works on FreeBSD and (with minor differences) OpenBSD/NetBSD.
// daemon(8) is universal on FreeBSD and present on the others; we keep the script
// portable by relying on standard rc.subr facilities.
const rcdScriptTemplate = `#!/bin/sh
#
# PROVIDE: {{.Name}}
# REQUIRE: NETWORKING SERVERS
# KEYWORD: shutdown
#
# Add the following to /etc/rc.conf to enable {{.Name}}:
# {{.Name}}_enable="YES"

. /etc/rc.subr

name="{{.Name}}"
rcvar="{{.Name}}_enable"
desc="{{.Description}}"

load_rc_config "${name}"

: ${{"{"}}{{.Name}}_enable:="NO"{{"}"}}
: ${{"{"}}{{.Name}}_user:="{{.User}}"{{"}"}}
{{- if .WorkingDirectory}}
: ${{"{"}}{{.Name}}_chdir:="{{.WorkingDirectory}}"{{"}"}}
{{- end}}

pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
procname="{{.BinaryPath}}"
command_args="-c -f -p ${pidfile} -t ${name} -- {{.BinaryPath}}{{if .Args}} {{.Args}}{{end}}"

run_rc_command "$1"
`

type rcdTemplateData struct {
	Name             string
	Description      string
	BinaryPath       string
	Args             string
	User             string
	WorkingDirectory string
}

func (m *rcdManager) InitSystem() InitSystem {
	return InitRcd
}

// scriptPath returns the path to the rc.d init script.
// FreeBSD, NetBSD, DragonFly use /usr/local/etc/rc.d for third-party services.
// OpenBSD uses /etc/rc.d.
func (m *rcdManager) scriptPath() string {
	if runtime.GOOS == "openbsd" {
		return filepath.Join("/etc/rc.d", ServiceName)
	}
	return filepath.Join("/usr/local/etc/rc.d", ServiceName)
}

func (m *rcdManager) IsInstalled() bool {
	_, err := os.Stat(m.scriptPath())
	return err == nil
}

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

	if !isRoot() {
		return ErrInsufficientPrivileges
	}

	cfg.Level = LevelSystem

	user := cfg.User
	if user == "" {
		user = currentUsername()
	}

	data := rcdTemplateData{
		Name:             ServiceName,
		Description:      cfg.Description,
		BinaryPath:       cfg.BinaryPath,
		Args:             strings.Join(cfg.Args, " "),
		User:             user,
		WorkingDirectory: cfg.WorkingDirectory,
	}

	tmpl, err := template.New("rcd").Parse(rcdScriptTemplate)
	if err != nil {
		return fmt.Errorf("failed to parse rc.d script template: %w", err)
	}

	dir := filepath.Dir(m.scriptPath())
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("failed to create %s: %w", dir, err)
	}

	f, err := os.OpenFile(m.scriptPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
	if err != nil {
		return fmt.Errorf("failed to create rc.d script: %w", err)
	}
	defer f.Close()

	if err := tmpl.Execute(f, data); err != nil {
		return fmt.Errorf("failed to render rc.d script: %w", err)
	}

	// Enable the service in rc.conf.local (FreeBSD/NetBSD/DragonFly use rc.conf;
	// OpenBSD uses rcctl). We append rather than rewriting to avoid clobbering
	// existing user configuration.
	if runtime.GOOS == "openbsd" {
		if commandExists("rcctl") {
			if err := exec.Command("rcctl", "enable", ServiceName).Run(); err != nil {
				return fmt.Errorf("failed to enable service via rcctl: %w", err)
			}
		}
	} else {
		if err := appendRcConfEnable(ServiceName); err != nil {
			return fmt.Errorf("failed to enable service in rc.conf: %w", err)
		}
	}

	return nil
}

// appendRcConfEnable adds `<name>_enable="YES"` to /etc/rc.conf.local if not
// already present. This is idempotent.
func appendRcConfEnable(name string) error {
	const path = "/etc/rc.conf.local"
	line := fmt.Sprintf("%s_enable=\"YES\"\n", name)

	// If the line is already present, do nothing.
	if data, err := os.ReadFile(path); err == nil && strings.Contains(string(data), name+"_enable") {
		return nil
	}

	f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = f.WriteString(line)
	return err
}

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

	_ = m.Stop()

	if runtime.GOOS == "openbsd" {
		if commandExists("rcctl") {
			_ = exec.Command("rcctl", "disable", ServiceName).Run()
		}
	}

	if err := os.Remove(m.scriptPath()); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to remove rc.d script: %w", err)
	}

	return nil
}

func (m *rcdManager) Start() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}
	return m.runService("start")
}

func (m *rcdManager) Stop() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}
	return m.runService("stop")
}

func (m *rcdManager) Restart() error {
	if !m.IsInstalled() {
		return ErrServiceNotInstalled
	}
	return m.runService("restart")
}

func (m *rcdManager) runService(action string) error {
	var cmd *exec.Cmd
	if runtime.GOOS == "openbsd" && commandExists("rcctl") {
		cmd = exec.Command("rcctl", action, ServiceName)
	} else if commandExists("service") {
		cmd = exec.Command("service", ServiceName, action)
	} else {
		cmd = exec.Command(m.scriptPath(), action)
	}
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (m *rcdManager) Status() (Status, error) {
	st := Status{
		InitSystem: InitRcd,
		Level:      "system",
		UnitName:   ServiceName,
	}

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

	// Check if running via the service/rcctl status command.
	var out []byte
	var err error
	if runtime.GOOS == "openbsd" && commandExists("rcctl") {
		out, err = exec.Command("rcctl", "check", ServiceName).Output()
	} else if commandExists("service") {
		out, err = exec.Command("service", ServiceName, "status").Output()
	} else {
		out, err = exec.Command(m.scriptPath(), "status").Output()
	}
	if err == nil {
		s := strings.ToLower(string(out))
		if strings.Contains(s, "running") || strings.Contains(s, "ok") || strings.Contains(s, "(ok)") {
			st.Running = true
		}
	}

	// Enabled check.
	if runtime.GOOS == "openbsd" && commandExists("rcctl") {
		if out, err := exec.Command("rcctl", "get", ServiceName, "status").Output(); err == nil {
			if strings.TrimSpace(string(out)) == "1" {
				st.Enabled = true
			}
		}
	} else {
		if data, err := os.ReadFile("/etc/rc.conf.local"); err == nil {
			if strings.Contains(string(data), ServiceName+"_enable=\"YES\"") {
				st.Enabled = true
			}
		}
		if !st.Enabled {
			if data, err := os.ReadFile("/etc/rc.conf"); err == nil {
				if strings.Contains(string(data), ServiceName+"_enable=\"YES\"") {
					st.Enabled = true
				}
			}
		}
	}

	// PID from pidfile.
	if pidData, err := os.ReadFile(fmt.Sprintf("/var/run/%s.pid", ServiceName)); err == nil {
		var pid int
		if _, err := fmt.Sscanf(string(pidData), "%d", &pid); err == nil && pid > 0 {
			st.PID = pid
		}
	}

	return st, nil
}
