<?php
/**
 * Shared process capability helpers for admin autonomy runtime.
 *
 * @package Niwa_AI
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Niwa_AI_Admin_Autonomy_Process_Runtime {
    public static function is_proc_open_available(): bool {
        if ( defined( 'NIWA_AI_FORCE_PROC_OPEN_UNAVAILABLE' ) && NIWA_AI_FORCE_PROC_OPEN_UNAVAILABLE ) {
            return false;
        }

        if ( function_exists( 'get_option' ) ) {
            $forced_unavailable = get_option( 'niwa_ai_force_proc_open_unavailable', false );
            if ( ! empty( $forced_unavailable ) && '0' !== (string) $forced_unavailable && 'false' !== strtolower( (string) $forced_unavailable ) ) {
                return false;
            }
        }

        $functions_available = function_exists( 'proc_open' )
            && function_exists( 'proc_get_status' )
            && function_exists( 'proc_close' );
        $available = $functions_available;

        if ( function_exists( 'apply_filters' ) ) {
            $available = (bool) apply_filters( 'niwa_ai_proc_open_runtime_available', $available );
        }

        return $functions_available && $available;
    }

    public static function can_spawn_local_cli(): bool {
        $available = DIRECTORY_SEPARATOR === '/'
            && self::is_proc_open_available();

        if ( function_exists( 'apply_filters' ) ) {
            $available = (bool) apply_filters( 'niwa_ai_admin_autonomy_local_cli_runner_enabled', $available );
        }

        return $available;
    }

    public static function resolve_php_binary(): string {
        foreach ( self::php_binary_candidates() as $candidate ) {
            if ( 'php' === $candidate ) {
                return $candidate;
            }

            if ( is_file( $candidate ) && is_executable( $candidate ) ) {
                return $candidate;
            }
        }

        return 'php';
    }

    public static function resolve_cli_php_binary(): string {
        foreach ( self::php_binary_candidates() as $candidate ) {
            $usable = 'php' === $candidate || ( is_file( $candidate ) && is_executable( $candidate ) );
            if ( $usable && self::php_binary_runs_as_cli( $candidate ) ) {
                return $candidate;
            }
        }

        return '';
    }

    private static function php_binary_candidates(): array {
        $candidates  = array();
        $sapi_binary = '';

        if ( defined( 'PHP_BINARY' ) && '' !== (string) PHP_BINARY ) {
            $sapi_binary = (string) PHP_BINARY;
        }

        if ( defined( 'PHP_BINDIR' ) && '' !== (string) PHP_BINDIR ) {
            $candidates[] = rtrim( (string) PHP_BINDIR, '/\\' ) . '/php';
        }

        if ( '' !== $sapi_binary && 'php' === basename( $sapi_binary ) ) {
            $candidates[] = $sapi_binary;
        }

        if ( DIRECTORY_SEPARATOR === '/' && function_exists( 'glob' ) ) {
            foreach ( array( '/usr/local/lsws/lsphp*/bin/php', '/opt/alt/php*/usr/bin/php', '/opt/cpanel/ea-php*/root/usr/bin/php' ) as $pattern ) {
                $matches = glob( $pattern );
                if ( is_array( $matches ) && ! empty( $matches ) ) {
                    rsort( $matches, SORT_NATURAL );
                    $candidates = array_merge( $candidates, $matches );
                }
            }
        }

        if ( '' !== $sapi_binary && 'php' !== basename( $sapi_binary ) ) {
            $candidates[] = $sapi_binary;
        }

        $candidates[] = '/usr/local/bin/php';
        $candidates[] = '/usr/bin/php';
        $candidates[] = 'php';

        return array_values( array_unique( array_filter( array_map( 'strval', $candidates ) ) ) );
    }

    private static function php_binary_runs_as_cli( string $binary ): bool {
        if ( '' === $binary ) {
            return false;
        }

        static $memo = array();
        if ( isset( $memo[ $binary ] ) ) {
            return $memo[ $binary ];
        }

        $cache_key = 'niwa_ai_php_cli_probe_' . md5( $binary );
        if ( function_exists( 'get_transient' ) ) {
            $cached = get_transient( $cache_key );
            if ( '1' === (string) $cached ) {
                return $memo[ $binary ] = true;
            }
            if ( '0' === (string) $cached ) {
                return $memo[ $binary ] = false;
            }
        }

        $command = escapeshellarg( $binary ) . ' -d display_errors=0 -r ' . escapeshellarg( 'echo PHP_SAPI;' );
        $output  = self::capture_command_output( $command, 5 );
        $is_cli  = is_string( $output ) && 'cli' === strtolower( trim( $output ) );

        if ( function_exists( 'set_transient' ) ) {
            set_transient( $cache_key, $is_cli ? '1' : '0', $is_cli ? 3600 : 300 );
        }

        return $memo[ $binary ] = $is_cli;
    }

    private static function capture_command_output( string $command, int $timeout_seconds = 5 ): ?string {
        $process = @proc_open(
            $command,
            array(
                0 => array( 'file', '/dev/null', 'r' ),
                1 => array( 'pipe', 'w' ),
                2 => array( 'file', '/dev/null', 'a' ),
            ),
            $pipes
        );

        if ( ! is_resource( $process ) ) {
            return null;
        }

        if ( isset( $pipes[1] ) && is_resource( $pipes[1] ) ) {
            stream_set_blocking( $pipes[1], false );
        }

        $output   = '';
        $deadline = microtime( true ) + max( 1, $timeout_seconds );
        do {
            if ( isset( $pipes[1] ) && is_resource( $pipes[1] ) ) {
                $chunk = stream_get_contents( $pipes[1] );
                if ( is_string( $chunk ) && '' !== $chunk ) {
                    $output .= $chunk;
                }
            }

            $status = function_exists( 'proc_get_status' ) ? proc_get_status( $process ) : array( 'running' => false );
            if ( empty( $status['running'] ) ) {
                break;
            }

            if ( microtime( true ) >= $deadline ) {
                @proc_terminate( $process, 9 );
                break;
            }

            usleep( 20000 );
        } while ( true );

        if ( isset( $pipes[1] ) && is_resource( $pipes[1] ) ) {
            $chunk = stream_get_contents( $pipes[1] );
            if ( is_string( $chunk ) ) {
                $output .= $chunk;
            }
            fclose( $pipes[1] );
        }

        @proc_close( $process );

        return $output;
    }

    public static function background_process_is_alive( int $pid ): bool {
        if ( $pid <= 0 ) {
            return false;
        }

        $proc_path = '/proc/' . $pid . '/stat';
        if ( is_readable( $proc_path ) ) {
            $stat = @file_get_contents( $proc_path );
            if ( false === $stat || '' === trim( (string) $stat ) ) {
                return false;
            }

            $close = strrpos( $stat, ')' );
            if ( false === $close ) {
                return true;
            }

            $rest  = ltrim( substr( $stat, $close + 1 ) );
            $state = isset( $rest[0] ) ? $rest[0] : '';

            return ! in_array( $state, array( 'Z', 'X', 'x' ), true );
        }

        if ( is_dir( '/proc' ) ) {
            // procfs is mounted but the entry is gone: the process already exited.
            return false;
        }

        if ( function_exists( 'posix_kill' ) ) {
            return (bool) @posix_kill( $pid, 0 );
        }

        return true;
    }

    private static function resolve_timeout_binary(): string {
        foreach ( array( '/usr/bin/timeout', '/bin/timeout' ) as $candidate ) {
            if ( is_file( $candidate ) && is_executable( $candidate ) ) {
                return $candidate;
            }
        }

        return '';
    }

    public static function spawn_background_php( $script, array $args = array(), $cwd = null, $timeout_seconds = 0 ): array {
        if ( ! self::can_spawn_local_cli() ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_unavailable',
            );
        }

        $script = (string) $script;
        if ( '' === $script || ! is_readable( $script ) ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_script_missing',
            );
        }

        if ( ! is_executable( '/bin/sh' ) ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_shell_missing',
            );
        }

        $binary = self::resolve_cli_php_binary();
        if ( '' === $binary ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_no_cli_binary',
            );
        }

        $cmd = escapeshellarg( $binary ) . ' ' . escapeshellarg( $script );
        foreach ( $args as $name => $value ) {
            if ( is_string( $name ) ) {
                $flag = preg_replace( '/[^A-Za-z0-9_-]/', '', $name );
                if ( '' === $flag ) {
                    continue;
                }
                $cmd .= ' --' . $flag . '=' . escapeshellarg( (string) $value );
            } else {
                $cmd .= ' ' . escapeshellarg( (string) $value );
            }
        }
        $timeout_seconds = max( 0, intval( $timeout_seconds ) );
        $timeout_binary  = $timeout_seconds > 0 ? self::resolve_timeout_binary() : '';
        if ( '' !== $timeout_binary ) {
            $cmd = escapeshellarg( $timeout_binary ) . ' --kill-after=15s ' . escapeshellarg( $timeout_seconds . 's' ) . ' ' . $cmd;
        }
        $cmd .= ' > /dev/null 2>&1 & echo $!';

        $process = @proc_open(
            '/bin/sh -c ' . escapeshellarg( $cmd ),
            array(
                0 => array( 'file', '/dev/null', 'r' ),
                1 => array( 'pipe', 'w' ),
                2 => array( 'file', '/dev/null', 'a' ),
            ),
            $pipes,
            is_string( $cwd ) && '' !== $cwd ? $cwd : null
        );

        if ( ! is_resource( $process ) ) {
            return array(
                'success' => false,
                'error'   => 'local_cli_proc_open_failed',
            );
        }

        $pid_raw = '';
        if ( isset( $pipes[1] ) && is_resource( $pipes[1] ) ) {
            $pid_raw = (string) stream_get_contents( $pipes[1] );
        }

        foreach ( (array) $pipes as $pipe ) {
            if ( is_resource( $pipe ) ) {
                fclose( $pipe );
            }
        }

        $exit_code = @proc_close( $process );
        if ( 0 !== $exit_code ) {
            return array(
                'success'   => false,
                'error'     => 'local_cli_shell_failed',
                'exit_code' => intval( $exit_code ),
            );
        }

        $pid = 0;
        if ( preg_match( '/\d+/', $pid_raw, $pid_matches ) ) {
            $pid = intval( $pid_matches[0] );
        }

        if ( $pid > 0 ) {
            // The shell forks the worker and returns immediately, so a zero exit
            // code only proves the shell ran -- not that the PHP worker survived
            // bootstrap. Confirm the PID is still alive after a short grace period
            // so an early death (missing wp-load, fatal during boot) is reported
            // as a failure and the caller can fall back to the web_loopback transport.
            usleep( 120000 );

            if ( ! self::background_process_is_alive( $pid ) ) {
                return array(
                    'success' => false,
                    'error'   => 'local_cli_process_exited_early',
                    'pid'     => $pid,
                );
            }
        }

        return array(
            'success'   => true,
            'transport' => 'local_cli',
            'pid'       => $pid,
        );
    }
}
