// Package pidfile provides PID file management for preventing concurrent
// instances of the application. It supports creating, checking, and removing
// PID files with process liveness detection.
//
// The liveness check uses Signal(0) on Unix (since os.FindProcess always
// succeeds) and os.FindProcess on Windows (which does check existence).
package pidfile

import (
	"fmt"
	"os"
	"runtime"
	"strconv"
	"strings"
	"syscall"
)

// DefaultPath is the default PID file location in the working directory.
const DefaultPath = "huntr.pid"

// Create writes the current process PID to the file at path.
// Returns an error if a PID file already exists and the process is running
// (or stale, when force is false).
//
// When force is true, existing PID files are overridden with a warning.
// When force is false, existing PID files cause an error with instructions.
func Create(path string, force bool) error {
	data, err := os.ReadFile(path)
	if err == nil {
		// PID file exists -- check if the process is still running
		pid, parseErr := strconv.Atoi(strings.TrimSpace(string(data)))
		if parseErr == nil && isProcessRunning(pid) {
			if !force {
				return fmt.Errorf(
					"another instance appears to be running (PID %d)\n"+
						"If this is stale, remove %s manually or use --force",
					pid, path)
			}
			fmt.Fprintf(os.Stderr, "Warning: overriding PID file (process %d may be running)\n", pid)
		} else {
			// Stale PID file (process not running) -- auto-clean it
			fmt.Fprintf(os.Stderr, "Cleaned up stale PID file (%s)\n", path)
			os.Remove(path)
		}
	}

	// Write current PID
	return os.WriteFile(path, []byte(strconv.Itoa(os.Getpid())), 0644)
}

// Remove deletes the PID file at path. No error is returned if the file
// does not exist.
func Remove(path string) {
	os.Remove(path)
}

// ReadPID reads the PID from the file at path.
// Returns the PID and nil error on success.
// Returns 0 and an error if the file does not exist or contains invalid data.
func ReadPID(path string) (int, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, fmt.Errorf("read PID file: %w", err)
	}

	pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
	if err != nil {
		return 0, fmt.Errorf("invalid PID in file: %w", err)
	}

	return pid, nil
}

// IsProcessRunning checks whether a process with the given PID is alive.
// Exported wrapper around isProcessRunning for use by status and stop commands.
//
// On Unix, uses Signal(0) to check liveness (os.FindProcess is a no-op).
// On Windows, os.FindProcess opens a handle which fails if process does not exist.
func IsProcessRunning(pid int) bool {
	return isProcessRunning(pid)
}

// isProcessRunning checks whether a process with the given PID is alive.
//
// On Unix, os.FindProcess always succeeds (it is a no-op), so we must
// send Signal(0) to actually check liveness. Signal(0) returns nil if the
// process exists and the caller has permission to signal it.
//
// On Windows, os.FindProcess opens a handle to the process, which fails
// if the process does not exist.
func isProcessRunning(pid int) bool {
	process, err := os.FindProcess(pid)
	if err != nil {
		return false
	}

	if runtime.GOOS != "windows" {
		// Unix: FindProcess is a no-op, must signal to check
		err = process.Signal(syscall.Signal(0))
		return err == nil
	}

	// Windows: FindProcess checks existence
	return true
}
