//go:build linux
// +build linux

/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

package forker

import (
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"os/user"
	"strings"
	"syscall"
)

func (p *Forker) StartNewProcess() (*exec.Cmd, error) {
	if err := validateExecutable(p.data.ExecPath); err != nil {
		return nil, fmt.Errorf("%w for '%s': %w", ErrExecCheck, p.data.ExecPath, err)
	}

	var stdOut, stdErr *os.File
	// Default stdout and stderr targets
	stdOut = os.Stdout
	stdErr = os.Stderr

	// Create output files [stdout, stderr, ret]
	if p.data.StdoutPath != "" {
		stdoutFile, err := os.Create(p.data.StdoutPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create stdout file: %w", err)
		}
		stdOut = stdoutFile
		defer stdoutFile.Close()
	}

	if p.data.StderrPath != "" {
		stderrFile, err := os.Create(p.data.StderrPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create stderr file: %w", err)
		}
		stdErr = stderrFile
		defer stderrFile.Close()
	}

	retPath := "/dev/null" // default redirected path
	if p.data.RetPath != "" {
		retPath = p.data.RetPath

		retFile, err := os.Create(retPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create ret file: %w", err)
		}
		retFile.Close()
		os.Chmod(retPath, 0666)
	}

	// Create the command to run the batch script using cmd.exe
	// Building full bash cmd
	// Join the command with args, escaping each
	var cmdParts []string
	cmdParts = append(cmdParts, shellEscape(p.data.ExecPath))
	for _, param := range p.data.ExecParams {
		cmdParts = append(cmdParts, shellEscape(param))
	}

	fullCmd := strings.Join(cmdParts, " ")

	// Runs the command, then echoes its exit code to retPath
	bashCmd := fmt.Sprintf("%s ; echo $? > %s", fullCmd, shellEscape(retPath))

	var cmd *exec.Cmd
	if p.data.DirectExec {
		cmd = exec.Command(p.data.ExecPath, p.data.ExecParams...)
	} else {
		cmd = exec.Command("/bin/bash", "-c", bashCmd)
	}

	// Set the standard output and error to the file (passing file descriptor)
	cmd.Stdout = stdOut
	cmd.Stderr = stdErr

	// setup stdinpipe
	stdinPipe, err := cmd.StdinPipe()
	if err != nil {
		return nil, fmt.Errorf("failed to get stdin pipe: %w", err)
	}

	// accept extra files
	cmd.ExtraFiles = p.data.ExtraFiles

	// New session
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setpgid: !p.data.Detached,
		Setsid:  p.data.Detached,
	}
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, p.data.Env...)

	// Check for logon user
	if p.data.Username != "" {
		if os.Geteuid() != 0 && os.Getegid() != 0 {
			return nil, fmt.Errorf("agent does not to run as 'root'")
		}

		userInfo, err := user.Lookup(p.data.Username)
		if err != nil {
			return nil, fmt.Errorf("error looking up user '%s': %w", p.data.Username, err)
		}

		uid, err := parseUID(userInfo.Uid)
		if err != nil {
			return nil, fmt.Errorf("invalid uid: %w", err)
		}

		gid, err := parseUID(userInfo.Gid)
		if err != nil {
			return nil, fmt.Errorf("invalid gid: %w", err)
		}

		cmd.Dir = userInfo.HomeDir
		cmd.Env = append(cmd.Env,
			"HOME="+userInfo.HomeDir,
			"USER="+userInfo.Username,
			"LOGNAME="+userInfo.Username,
		)

		// Set credentials
		cmd.SysProcAttr.Credential = &syscall.Credential{
			Uid: uid,
			Gid: gid,
		}
	}

	// Run the command
	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("failed to start process: %w", err)
	}

	if len(p.data.StdinBytes) > 0 {
		_, err = stdinPipe.Write(p.data.StdinBytes)
		if err != nil {
			return nil, fmt.Errorf("failed to write to stdin pipe: %w", err)
		}
	}
	stdinPipe.Close()

	// Clean up goroutine to kill the process group if the parent receives SIGINT or SIGTERM
	if !p.data.Detached {
		go func(pid int) {
			c := make(chan os.Signal, 1)
			signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
			<-c
			syscall.Kill(-pid, syscall.SIGKILL)
			os.Exit(1)
		}(cmd.Process.Pid)
	}

	return cmd, nil
}

func parseUID(id string) (uint32, error) {
	var uid uint32
	_, err := fmt.Sscanf(id, "%d", &uid)
	if err != nil {
		return 0, fmt.Errorf("invalid UID format")
	}
	return uid, nil
}

func shellEscape(s string) string {
	return "'" + strings.ReplaceAll(s, `'`, `'\''`) + "'"
}
