<?php

declare(strict_types=1);

/**
 * This file is part of CodeIgniter Queue.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace CodeIgniter\Queue\Compatibility;

use Closure;
use CodeIgniter\CLI\CLI;

if (trait_exists('CodeIgniter\CLI\SignalTrait')) {
    trait SignalTrait
    {
        use \CodeIgniter\CLI\SignalTrait;
    }
} else {
    /**
     * Signal Trait
     *
     * Provides PCNTL signal handling capabilities for CLI commands.
     * Requires the PCNTL extension (Unix only).
     *
     * Bundled compatibility version for CI4 < 4.7
     */
    trait SignalTrait
    {
        /**
         * Whether the process should continue running (false = termination requested).
         */
        private bool $running = true;

        /**
         * Whether signals are currently blocked.
         */
        private bool $signalsBlocked = false;

        /**
         * Array of registered signals.
         *
         * @var list<int>
         */
        private array $registeredSignals = [];

        /**
         * Signal-to-method mapping.
         *
         * @var array<int, string>
         */
        private array $signalMethodMap = [];

        /**
         * Cached result of PCNTL extension availability.
         */
        private static ?bool $isPcntlAvailable = null;

        /**
         * Cached result of POSIX extension availability.
         */
        private static ?bool $isPosixAvailable = null;

        /**
         * Check if PCNTL extension is available (cached).
         */
        protected function isPcntlAvailable(): bool
        {
            if (self::$isPcntlAvailable === null) {
                if (is_windows()) {
                    self::$isPcntlAvailable = false;
                } else {
                    self::$isPcntlAvailable = extension_loaded('pcntl');
                    if (! self::$isPcntlAvailable) {
                        CLI::write('PCNTL extension is not available. Signal handling will be disabled.', 'yellow');
                    }
                }
            }

            return self::$isPcntlAvailable;
        }

        /**
         * Check if POSIX extension is available (cached).
         */
        protected function isPosixAvailable(): bool
        {
            if (self::$isPosixAvailable === null) {
                self::$isPosixAvailable = is_windows() ? false : extension_loaded('posix');
            }

            return self::$isPosixAvailable;
        }

        /**
         * Register signal handlers.
         *
         * @param list<int>          $signals   List of signals to handle
         * @param array<int, string> $methodMap Optional signal-to-method mapping
         */
        protected function registerSignals(
            array $signals = [],
            array $methodMap = [],
        ): void {
            if (! $this->isPcntlAvailable()) {
                return;
            }

            if ($signals === []) {
                $signals = [SIGTERM, SIGINT, SIGHUP, SIGQUIT];
            }

            if (! $this->isPosixAvailable() && (in_array(SIGTSTP, $signals, true) || in_array(SIGCONT, $signals, true))) {
                CLI::write('POSIX extension is not available. SIGTSTP and SIGCONT signals will be disabled.', 'yellow');
                $signals = array_diff($signals, [SIGTSTP, SIGCONT]);

                // Remove from method map as well
                unset($methodMap[SIGTSTP], $methodMap[SIGCONT]);

                if ($signals === []) {
                    return;
                }
            }

            // Enable async signals for immediate response
            pcntl_async_signals(true);

            $this->signalMethodMap = $methodMap;

            foreach ($signals as $signal) {
                if (pcntl_signal($signal, [$this, 'handleSignal'])) {
                    $this->registeredSignals[] = $signal;
                } else {
                    $signal = $this->getSignalName($signal);
                    CLI::write("Failed to register signal handler for {$signal}.", 'red');
                }
            }
        }

        /**
         * Handle incoming signals.
         */
        protected function handleSignal(int $signal): void
        {
            $this->callCustomHandler($signal);

            // Apply standard Unix signal behavior for registered signals
            switch ($signal) {
                case SIGTERM:
                case SIGINT:
                case SIGQUIT:
                case SIGHUP:
                    $this->running = false;
                    break;

                case SIGTSTP:
                    // Restore default handler and re-send signal to actually suspend
                    pcntl_signal(SIGTSTP, SIG_DFL);
                    posix_kill(posix_getpid(), SIGTSTP);
                    break;

                case SIGCONT:
                    // Re-register SIGTSTP handler after resume
                    pcntl_signal(SIGTSTP, [$this, 'handleSignal']);
                    break;
            }
        }

        /**
         * Call custom signal handler if one is mapped for this signal.
         * Falls back to generic onInterruption() method if no explicit mapping exists.
         */
        private function callCustomHandler(int $signal): void
        {
            // Check for explicit mapping first
            $method = $this->signalMethodMap[$signal] ?? null;

            if ($method !== null && method_exists($this, $method)) {
                $this->{$method}($signal);

                return;
            }

            // If no explicit mapping, try generic catch-all method
            if (method_exists($this, 'onInterruption')) { // @phpstan-ignore-line
                $this->onInterruption($signal);
            }
        }

        /**
         * Check if command should terminate.
         */
        protected function shouldTerminate(): bool
        {
            return ! $this->running;
        }

        /**
         * Check if the process is currently running (not terminated).
         */
        protected function isRunning(): bool
        {
            return $this->running;
        }

        /**
         * Request immediate termination.
         */
        protected function requestTermination(): void
        {
            $this->running = false;
        }

        /**
         * Reset all states (for testing or restart scenarios).
         */
        protected function resetState(): void
        {
            $this->running = true;

            // Unblock signals if they were blocked
            if ($this->signalsBlocked) {
                $this->unblockSignals();
            }
        }

        /**
         * Execute a callable with ALL signals blocked to prevent ANY interruption during critical operations.
         *
         * This blocks ALL interruptible signals including:
         * - Termination signals (SIGTERM, SIGINT, etc.)
         * - Pause/resume signals (SIGTSTP, SIGCONT)
         * - Custom signals (SIGUSR1, SIGUSR2)
         *
         * Only SIGKILL (unblockable) can still terminate the process.
         * Use this for database transactions, file operations, or any critical atomic operations.
         *
         * @template TReturn
         *
         * @param Closure():TReturn $operation
         *
         * @return TReturn
         */
        protected function withSignalsBlocked(Closure $operation)
        {
            $this->blockSignals();

            try {
                return $operation();
            } finally {
                $this->unblockSignals();
            }
        }

        /**
         * Block ALL interruptible signals during critical sections.
         * Only SIGKILL (unblockable) can terminate the process.
         */
        protected function blockSignals(): void
        {
            if (! $this->signalsBlocked && $this->isPcntlAvailable()) {
                // Block ALL signals that could interrupt critical operations
                pcntl_sigprocmask(SIG_BLOCK, [
                    SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals
                    SIGTSTP, SIGCONT,                 // Pause/resume signals
                    SIGUSR1, SIGUSR2,                 // Custom signals
                    SIGPIPE, SIGALRM,                 // Other common signals
                ]);
                $this->signalsBlocked = true;
            }
        }

        /**
         * Unblock previously blocked signals.
         */
        protected function unblockSignals(): void
        {
            if ($this->signalsBlocked && $this->isPcntlAvailable()) {
                // Unblock the same signals we blocked
                pcntl_sigprocmask(SIG_UNBLOCK, [
                    SIGTERM, SIGINT, SIGHUP, SIGQUIT, // Termination signals
                    SIGTSTP, SIGCONT,                 // Pause/resume signals
                    SIGUSR1, SIGUSR2,                 // Custom signals
                    SIGPIPE, SIGALRM,                 // Other common signals
                ]);
                $this->signalsBlocked = false;
            }
        }

        /**
         * Check if signals are currently blocked.
         */
        protected function signalsBlocked(): bool
        {
            return $this->signalsBlocked;
        }

        /**
         * Add or update signal-to-method mapping at runtime.
         */
        protected function mapSignal(int $signal, string $method): void
        {
            $this->signalMethodMap[$signal] = $method;
        }

        /**
         * Get human-readable signal name.
         */
        protected function getSignalName(int $signal): string
        {
            return match ($signal) {
                SIGTERM => 'SIGTERM',
                SIGINT  => 'SIGINT',
                SIGHUP  => 'SIGHUP',
                SIGQUIT => 'SIGQUIT',
                SIGUSR1 => 'SIGUSR1',
                SIGUSR2 => 'SIGUSR2',
                SIGPIPE => 'SIGPIPE',
                SIGALRM => 'SIGALRM',
                SIGTSTP => 'SIGTSTP',
                SIGCONT => 'SIGCONT',
                default => "Signal {$signal}",
            };
        }

        /**
         * Unregister all signals (cleanup).
         */
        protected function unregisterSignals(): void
        {
            if (! $this->isPcntlAvailable()) {
                return;
            }

            foreach ($this->registeredSignals as $signal) {
                pcntl_signal($signal, SIG_DFL);
            }

            $this->registeredSignals = [];
            $this->signalMethodMap   = [];
        }

        /**
         * Check if signals are registered.
         */
        protected function hasSignals(): bool
        {
            return $this->registeredSignals !== [];
        }

        /**
         * Get list of registered signals.
         *
         * @return list<int>
         */
        protected function getSignals(): array
        {
            return $this->registeredSignals;
        }

        /**
         * Get comprehensive process state information.
         *
         * @return array{
         *      pid: int,
         *      running: bool,
         *      pcntl_available: bool,
         *      registered_signals: int,
         *      registered_signals_names: array<int, string>,
         *      signals_blocked: bool,
         *      explicit_mappings: int,
         *      memory_usage_mb: float,
         *      memory_peak_mb: float,
         *      session_id?: false|int,
         *      process_group?: false|int,
         *      has_controlling_terminal?: bool
         *  }
         */
        protected function getProcessState(): array
        {
            $pid   = getmypid();
            $state = [
                // Process identification
                'pid'     => $pid,
                'running' => $this->running,

                // Signal handling status
                'pcntl_available'          => $this->isPcntlAvailable(),
                'registered_signals'       => count($this->registeredSignals),
                'registered_signals_names' => array_map([$this, 'getSignalName'], $this->registeredSignals),
                'signals_blocked'          => $this->signalsBlocked,
                'explicit_mappings'        => count($this->signalMethodMap),

                // System resources
                'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
                'memory_peak_mb'  => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
            ];

            // Add terminal control info if POSIX extension is available
            if ($this->isPosixAvailable()) {
                $state['session_id']               = posix_getsid($pid);
                $state['process_group']            = posix_getpgid($pid);
                $state['has_controlling_terminal'] = posix_isatty(STDIN);
            }

            return $state;
        }
    }
}
