<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Services\SupportCenter;

use App\Support\Filesystem\SafeFilesystemProbe;
use Throwable;

/**
 * 统一解析系统更新可用的 PHP CLI 可执行文件，避免将 PHP-FPM 当作 CLI。
 */
final class PhpCliBinaryResolver
{
    public function __construct(
        private readonly SafeFilesystemProbe $filesystemProbe,
    ) {}

    public function resolve(): PhpCliBinaryResolution
    {
        return $this->resolveWith(
            sapi: PHP_SAPI,
            phpBinary: $this->rawPhpBinaryConstant(),
            configuredBinary: $this->configuredBinary(),
        );
    }

    /**
     * 按给定运行时参数解析（供单元测试注入 fpm / 自定义 CLI 路径场景）。
     */
    public function resolveWith(?string $sapi, ?string $phpBinary, ?string $configuredBinary): PhpCliBinaryResolution
    {
        try {
            $configured = is_string($configuredBinary) ? trim($configuredBinary) : '';
            $configured = $configured !== '' ? $configured : null;

            if ($configured !== null) {
                return $this->evaluateCandidate(
                    path: $configured,
                    source: PhpCliBinaryResolution::SOURCE_CONFIG,
                );
            }

            if ($this->isCliSapi($sapi)) {
                $candidate = is_string($phpBinary) ? trim($phpBinary) : '';
                $candidate = $candidate !== '' ? $candidate : null;

                if ($candidate === null) {
                    return new PhpCliBinaryResolution(
                        status: PhpCliBinaryResolution::STATUS_UNCONFIGURED,
                        path: null,
                        source: PhpCliBinaryResolution::SOURCE_NONE,
                        isNonCliBinary: false,
                    );
                }

                $normalized = $this->normalizePhpBinaryForCli($candidate);

                return $this->evaluateCandidate(
                    path: $normalized,
                    source: PhpCliBinaryResolution::SOURCE_PHP_BINARY,
                );
            }

            $runtimeBinary = is_string($phpBinary) ? trim($phpBinary) : '';
            $runtimeBinary = $runtimeBinary !== '' ? $runtimeBinary : null;

            if ($runtimeBinary !== null && $this->isNonCliBinaryPath($runtimeBinary)) {
                $inferred = $this->resolveInferredCliFromRuntimeBinary($runtimeBinary);

                if ($inferred instanceof PhpCliBinaryResolution) {
                    return $inferred;
                }

                return new PhpCliBinaryResolution(
                    status: PhpCliBinaryResolution::STATUS_NON_CLI_RUNTIME,
                    path: $runtimeBinary,
                    source: PhpCliBinaryResolution::SOURCE_PHP_BINARY,
                    isNonCliBinary: true,
                );
            }

            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_UNCONFIGURED,
                path: $runtimeBinary,
                source: $runtimeBinary === null
                    ? PhpCliBinaryResolution::SOURCE_NONE
                    : PhpCliBinaryResolution::SOURCE_PHP_BINARY,
                isNonCliBinary: false,
            );
        } catch (Throwable) {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_UNCONFIGURED,
                path: null,
                source: PhpCliBinaryResolution::SOURCE_NONE,
                isNonCliBinary: false,
            );
        }
    }

    /**
     * 仅返回已通过 Web 侧安全校验的 CLI 路径。
     */
    public function resolveVerifiedBinary(): ?string
    {
        $resolution = $this->resolve();

        return $resolution->isVerified() ? $resolution->path : null;
    }

    /**
     * 供 Symfony Process / 终端命令拼装：永不返回 php-fpm / php-cgi。
     */
    public function resolveForProcessExecution(): ?string
    {
        $resolution = $this->resolve();

        if (! $resolution->isUsableForProcess()) {
            return null;
        }

        return $resolution->path;
    }

    /**
     * 终端建议命令优先使用已配置或已校验的 CLI；无法确定时返回 null（调用方回退为 `php`）。
     */
    public function resolveSuggestedBinary(): ?string
    {
        return $this->resolveForProcessExecution();
    }

    /**
     * 原始 PHP_BINARY 为 php-cgi，且建议命令已改用同目录 php CLI 时为 true（供终端提示文案）。
     * 必须用替换前的原始路径判断：resolve() 返回的 path 已是规范化后的 CLI。
     */
    public function isSuggestedBinarySubstitutedFromCgi(?string $rawPhpBinary = null, ?string $suggestedBinary = null): bool
    {
        $rawPhpBinary ??= $this->rawPhpBinaryConstant();
        $suggestedBinary ??= $this->resolveSuggestedBinary();

        if (! is_string($rawPhpBinary) || trim($rawPhpBinary) === '') {
            return false;
        }

        if (! is_string($suggestedBinary) || trim($suggestedBinary) === '') {
            return false;
        }

        $rawPhpBinary = trim($rawPhpBinary);
        $suggestedBinary = trim($suggestedBinary);

        if (! $this->isPhpCgiBinaryPath($rawPhpBinary)) {
            return false;
        }

        if ($this->isPhpCgiBinaryPath($suggestedBinary)) {
            return false;
        }

        return str_replace('\\', '/', $suggestedBinary) !== str_replace('\\', '/', $rawPhpBinary);
    }

    public function configuredBinary(): ?string
    {
        $configured = trim((string) config('dberp.support_center.php_cli_binary', ''));

        return $configured !== '' ? $configured : null;
    }

    public function isCliSapi(?string $sapi = null): bool
    {
        $sapi ??= PHP_SAPI;

        return strtolower($sapi) === 'cli';
    }

    public function isNonCliBinaryPath(string $binary): bool
    {
        $normalized = str_replace('\\', '/', trim($binary));

        if ($normalized === '') {
            return false;
        }

        $basename = strtolower((string) basename($normalized));

        if (preg_match('/^php-fpm(?:[\d.]+)?(?:\.exe)?$/i', $basename) === 1) {
            return true;
        }

        if (preg_match('/^php-cgi(?:\.exe)?$/i', $basename) === 1) {
            return true;
        }

        return str_contains(strtolower($normalized), '/sbin/php-fpm');
    }

    /**
     * 从 php-fpm / php-cgi 等运行时二进制路径推断同安装目录下的 CLI 候选（不访问 php-fpm 本身）。
     *
     * @return list<string>
     */
    public function inferCliCandidatesFromRuntimeBinary(string $runtimeBinary): array
    {
        $normalized = str_replace('\\', '/', trim($runtimeBinary));

        if ($normalized === '') {
            return [];
        }

        /** @var list<string> $candidates */
        $candidates = [];

        // 宝塔 / 1Panel 等：/www/server/php/83/sbin/php-fpm → /www/server/php/83/bin/php
        if (preg_match('#^(/.+)/sbin/php-fpm(?:[\d.]*)?$#i', $normalized, $matches) === 1) {
            $candidates[] = $matches[1].'/bin/php';
        }

        $sbinToBin = (string) preg_replace('#/sbin/php-fpm(?:[\d.]*)$#i', '/bin/php', $normalized);

        if ($sbinToBin !== $normalized) {
            $candidates[] = $sbinToBin;
        }

        $cgiSibling = (string) preg_replace('/php-cgi(\.exe)?$/iu', 'php$1', $normalized);

        if ($cgiSibling !== $normalized) {
            $candidates[] = $cgiSibling;
        }

        if (defined('PHP_BINDIR') && str_starts_with($normalized, '/')) {
            $bindir = str_replace('\\', '/', trim((string) PHP_BINDIR));

            if ($bindir !== '' && str_starts_with($bindir, '/') && ! str_contains(strtolower($bindir), 'php-fpm') && ! str_contains(strtolower($bindir), 'sbin')) {
                $candidates[] = rtrim($bindir, '/').'/php';
            }
        }

        $unique = [];

        foreach ($candidates as $candidate) {
            $candidate = trim($candidate);

            if ($candidate === '' || $this->isNonCliBinaryPath($candidate)) {
                continue;
            }

            $unique[$candidate] = $candidate;
        }

        return array_values($unique);
    }

    private function resolveInferredCliFromRuntimeBinary(string $runtimeBinary): ?PhpCliBinaryResolution
    {
        foreach ($this->inferCliCandidatesFromRuntimeBinary($runtimeBinary) as $candidate) {
            $resolution = $this->evaluateCandidate(
                path: $candidate,
                source: PhpCliBinaryResolution::SOURCE_INFERRED,
            );

            if ($resolution->isVerified() || $resolution->isUnverifiable()) {
                return $resolution;
            }
        }

        return null;
    }

    private function evaluateCandidate(string $path, string $source): PhpCliBinaryResolution
    {
        $path = trim($path);

        if ($path === '') {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_UNCONFIGURED,
                path: null,
                source: $source,
                isNonCliBinary: false,
            );
        }

        if ($this->isNonCliBinaryPath($path)) {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_NON_CLI_RUNTIME,
                path: $path,
                source: $source,
                isNonCliBinary: true,
            );
        }

        if (! $this->filesystemProbe->isPathAllowedByOpenBasedir($path)) {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_UNVERIFIABLE,
                path: $path,
                source: $source,
                isNonCliBinary: false,
            );
        }

        $exists = $this->filesystemProbe->safeIsFile($path);

        if (! $exists) {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_MISSING,
                path: $path,
                source: $source,
                isNonCliBinary: false,
            );
        }

        if (! $this->filesystemProbe->safeIsExecutable($path)) {
            return new PhpCliBinaryResolution(
                status: PhpCliBinaryResolution::STATUS_NOT_EXECUTABLE,
                path: $path,
                source: $source,
                isNonCliBinary: false,
            );
        }

        return new PhpCliBinaryResolution(
            status: PhpCliBinaryResolution::STATUS_VERIFIED,
            path: $path,
            source: $source,
            isNonCliBinary: false,
        );
    }

    private function rawPhpBinaryConstant(): ?string
    {
        if (! defined('PHP_BINARY')) {
            return null;
        }

        $binary = trim((string) PHP_BINARY);

        return $binary !== '' ? $binary : null;
    }

    private function normalizePhpBinaryForCli(string $binary): string
    {
        if (! $this->isPhpCgiBinaryPath($binary)) {
            return $binary;
        }

        $cliBinary = (string) preg_replace('/php-cgi(\.exe)?$/iu', 'php$1', $binary);

        if ($cliBinary === $binary) {
            return $binary;
        }

        if ($this->filesystemProbe->safeIsFile($cliBinary) && $this->filesystemProbe->safeIsExecutable($cliBinary)) {
            return $cliBinary;
        }

        return $binary;
    }

    private function isPhpCgiBinaryPath(string $binary): bool
    {
        return (bool) preg_match('/php-cgi(?:\.exe)?$/iu', $binary);
    }
}
