<?php

declare(strict_types=1);

namespace App\Services\Install;

use Symfony\Component\Process\Process;
use Throwable;

/**
 * EnvPatcher — 누락 환경 항목에 대한 설치 명령 생성 + (안전 가드 통과 시) 자동 실행.
 *
 * 두 모드:
 *   1) getInstallCommands()  — 명령 텍스트만 생성 (실행 X). 복사 버튼용.
 *   2) executeInstall()      — 실제 실행 (sudo, php-fpm reload, 검증).
 *
 * 안전 가드 (절대 준수):
 *   - install.lock 부재 (설치 완료 후 차단)
 *   - aaPanel 빌드 감지 시 자동 실행 X (수동 안내만)
 *   - rate limit (라우트 단계에서 throttle:installer 적용)
 *   - sudo 비밀번호는 메모리에서 즉시 폐기
 */
final class EnvPatcher
{
    public function __construct(
        private readonly SystemDetector $detector,
        private readonly LockGuard $lock,
    ) {
    }

    /**
     * 설치 명령 생성 (실행 X).
     *
     * @param  list<array{type:string,name:string}>  $items
     * @return list<array<string,mixed>>
     */
    public function getInstallCommands(array $items): array
    {
        $env = $this->detector->detect();
        $out = [];
        foreach ($items as $item) {
            $type = $item['type'] ?? '';
            $name = $item['name'] ?? '';
            if ($type === 'ext') {
                $out[] = $this->buildExtensionCommand($name, $env);
            } elseif ($type === 'dir') {
                $out[] = $this->buildDirectoryCommand($name, $env);
            } else {
                $out[] = [
                    'item' => $name,
                    'type' => $type,
                    'command' => null,
                    'manual_only' => true,
                    'fallback_note' => '알 수 없는 항목 타입: '.$type,
                ];
            }
        }
        return $out;
    }

    /**
     * 누락 확장 자동 실행.
     *
     * @return array<string,mixed>
     */
    public function executeInstall(string $extension, bool $dryRun = false, ?string $sudoPassword = null): array
    {
        $env = $this->detector->detect();
        $config = (array) config('install-patches.extensions.'.$extension, []);
        $pm = $env['package_manager'];
        $build = $env['php_build'];

        // 안전 가드 1: install.lock 부재
        if ($this->lock->verify()) {
            return $this->error('install_locked', '설치가 이미 완료되었습니다. 자동 패치를 사용할 수 없습니다.');
        }

        // 안전 가드 2: aaPanel 차단
        $blocked = (array) config('install-patches.safety.auto_install_blocked_builds', []);
        if (in_array($build, $blocked, true)) {
            return $this->error('blocked_build', "현재 빌드({$build}) 는 자동 설치가 비활성화되어 있습니다. 수동 설치를 사용하세요.");
        }

        // 안전 가드 3: 패키지 매니저 화이트리스트
        $allowed = (array) config('install-patches.safety.auto_install_allowed_pms', []);
        if (! in_array($pm, $allowed, true)) {
            return $this->error('pm_not_allowed', "패키지 매니저({$pm}) 는 자동 설치가 허용되지 않습니다. 수동 설치 명령을 사용하세요.");
        }

        // 안전 가드 4: shell_exec
        if (! $env['shell_exec_enabled']) {
            return $this->error('shell_exec_disabled', 'shell_exec 함수가 비활성되어 있어 자동 설치 불가.');
        }

        // 패키지 추출
        $packages = $config[$pm] ?? [];
        if (! $packages) {
            return $this->error('no_package_mapping', "패키지 매니저({$pm}) 에 대한 매핑이 없습니다.");
        }
        if ($this->isSpecialPackage($packages[0] ?? '')) {
            return $this->error('special_install_required', '이 확장은 특수 설치 절차가 필요합니다.');
        }

        $resolved = $this->resolvePackageNames($packages, $env);
        $cmd = $this->buildAptOrDnfCommand($pm, $resolved);

        if ($dryRun) {
            return [
                'ok'        => true,
                'dry_run'   => true,
                'command'   => $cmd,
                'fpm_reload_command' => $this->fpmReloadCommand($env),
            ];
        }

        // 실행
        $exec = $this->runCommand($cmd, $sudoPassword);

        // sudo password 메모리에서 즉시 폐기
        if ($sudoPassword !== null) {
            $sudoPassword = str_repeat("\0", strlen($sudoPassword));
            unset($sudoPassword);
        }

        if ($exec['exit_code'] !== 0) {
            return [
                'ok'        => false,
                'error'     => 'install_failed',
                'exit_code' => $exec['exit_code'],
                'stdout'    => $this->truncate($exec['stdout']),
                'stderr'    => $this->truncate($exec['stderr']),
                'command'   => $cmd,
            ];
        }

        // php-fpm reload
        $reloadOut = $this->reloadFpm($env);

        // grace
        $grace = (int) config('install-patches.safety.fpm_reload_grace_seconds', 2);
        if ($grace > 0) {
            sleep($grace);
        }

        // 검증
        $verified = $this->verifyExtensionLoaded($extension);

        return [
            'ok'        => $verified,
            'verified'  => $verified,
            'command'   => $cmd,
            'stdout'    => $this->truncate($exec['stdout']),
            'fpm_reload' => $reloadOut,
            'message'   => $verified
                ? "{$extension} 확장 설치 + 활성 확인 완료"
                : "설치 명령은 성공했지만 확장 활성 확인 실패 — php.ini / fpm pool 수동 확인 필요",
        ];
    }

    /**
     * 디렉토리 권한 일괄 수정.
     *
     * @return array<string,mixed>
     */
    public function fixPermissions(bool $dryRun = false): array
    {
        if ($this->lock->verify()) {
            return $this->error('install_locked', '설치 완료 상태에서는 권한 수정 불가.');
        }

        $env = $this->detector->detect();
        $perms = (array) config('install-patches.directory_permissions', []);
        $results = [];

        foreach ($perms as $rel => $spec) {
            [$mode, $owner] = array_pad(explode(':', (string) $spec), 2, '');
            $owner = $owner === '__WEB_USER__' ? (string) $env['web_user'] : $owner;
            $path = base_path((string) $rel);
            if (! is_dir($path)) {
                @mkdir($path, 0o775, true);
            }
            $chmodCmd = sprintf('chmod -R %s %s', escapeshellarg($mode), escapeshellarg($path));
            $chownCmd = sprintf('chown -R %s:%s %s', escapeshellarg($owner), escapeshellarg($owner), escapeshellarg($path));
            $sudo = $env['is_root'] ? '' : 'sudo ';

            $fullChmod = $sudo.$chmodCmd;
            $fullChown = $sudo.$chownCmd;

            if ($dryRun) {
                $results[] = [
                    'path' => $rel,
                    'mode' => $mode,
                    'owner' => $owner,
                    'commands' => [$fullChmod, $fullChown],
                    'dry_run' => true,
                ];
                continue;
            }

            $r1 = $this->runCommand($fullChmod);
            $r2 = $this->runCommand($fullChown);
            $results[] = [
                'path'  => $rel,
                'mode'  => $mode,
                'owner' => $owner,
                'chmod_ok' => $r1['exit_code'] === 0,
                'chown_ok' => $r2['exit_code'] === 0,
                'stderr'   => trim(($r1['stderr'] ?? '').' '.($r2['stderr'] ?? '')),
            ];
        }

        return ['ok' => true, 'dry_run' => $dryRun, 'results' => $results];
    }

    // ============================================================
    // Internal helpers
    // ============================================================

    /**
     * @param  array<string,mixed>  $env
     * @return array<string,mixed>
     */
    private function buildExtensionCommand(string $ext, array $env): array
    {
        $config = (array) config('install-patches.extensions.'.$ext, []);
        $pm = $env['package_manager'];
        $build = $env['php_build'];

        // 빌드별 우선 (aapanel / source) 확인
        $packages = null;
        $usedKey = null;

        if ($build === 'aapanel' && isset($config['aapanel'])) {
            $packages = $config['aapanel'];
            $usedKey = 'aapanel';
        } elseif ($build === 'source' && isset($config['source'])) {
            $packages = $config['source'];
            $usedKey = 'source';
        } elseif ($pm && isset($config[$pm])) {
            $packages = $config[$pm];
            $usedKey = $pm;
        }

        if ($packages === null) {
            return [
                'item' => $ext,
                'type' => 'extension',
                'command' => null,
                'manual_only' => true,
                'fallback_note' => '이 시스템에 대한 패키지 매핑이 없습니다. 수동 설치 필요.',
            ];
        }

        // 특수 케이스
        $first = $packages[0] ?? '';
        if ($first === 'ini-only') {
            return [
                'item' => $ext,
                'type' => 'extension',
                'command' => null,
                'manual_only' => true,
                'fallback_note' => "{$ext} 는 이미 컴파일되어 있을 수 있습니다. {$env['php_ini_dir']}/{$ext}.ini 에서 extension={$ext} 활성화 필요.",
            ];
        }
        if ($first === 'source-build') {
            return [
                'item' => $ext,
                'type' => 'extension',
                'command' => null,
                'manual_only' => true,
                'fallback_note' => 'aaPanel: PHP 컴파일 옵션에서 활성 필요. 패널 → PHP → 설정 → 확장.',
            ];
        }
        if (str_starts_with($first, 'manual:')) {
            $noteId = substr($first, 7);
            return [
                'item' => $ext,
                'type' => 'extension',
                'command' => null,
                'manual_only' => true,
                'fallback_note' => $this->resolveManualNote($noteId, $env),
            ];
        }
        if (str_starts_with($first, 'pecl:')) {
            $name = substr($first, 5);
            return [
                'item' => $ext,
                'type' => 'extension',
                'command' => $this->prefixSudo("pecl install {$name}", $env)
                    ."\necho \"extension={$ext}.so\" | "
                    .$this->prefixSudo("tee {$env['php_ini_dir']}/{$ext}.ini", $env),
                'os' => $env['os'],
                'package_manager' => $pm,
                'php_build' => $build,
                'sudo_required' => ! $env['is_root'],
                'requires_restart' => true,
                'fpm_reload_command' => $this->fpmReloadCommand($env),
                'verification' => "php -m | grep -i '^{$ext}\$'",
                'safe_to_auto_execute' => false,
            ];
        }

        $resolved = $this->resolvePackageNames($packages, $env);
        $cmd = $this->buildAptOrDnfCommand($pm, $resolved);
        $cmd = $env['is_root'] ? preg_replace('/^sudo\s+/', '', $cmd) : $cmd;

        $allowedPms = (array) config('install-patches.safety.auto_install_allowed_pms', []);
        $safe = $env['has_sudo']
            && in_array($pm, $allowedPms, true)
            && ! in_array($build, (array) config('install-patches.safety.auto_install_blocked_builds', []), true);

        return [
            'item' => $ext,
            'type' => 'extension',
            'os' => $env['os'],
            'os_version' => $env['os_version'],
            'package_manager' => $pm,
            'php_build' => $build,
            'packages' => $resolved,
            'command' => $cmd,
            'sudo_required' => ! $env['is_root'],
            'requires_restart' => true,
            'fpm_reload_command' => $this->fpmReloadCommand($env),
            'verification' => "php -m | grep -i '^{$ext}\$'",
            'safe_to_auto_execute' => $safe,
            'used_mapping' => $usedKey,
        ];
    }

    /**
     * @param  array<string,mixed>  $env
     * @return array<string,mixed>
     */
    private function buildDirectoryCommand(string $rel, array $env): array
    {
        $perm = (string) config('install-patches.directory_permissions.'.$rel, '775:__WEB_USER__');
        [$mode, $owner] = array_pad(explode(':', $perm), 2, '');
        $owner = $owner === '__WEB_USER__' ? (string) $env['web_user'] : $owner;
        $abs = base_path($rel);

        $sudo = $env['is_root'] ? '' : 'sudo ';
        $cmd = sprintf('%schmod -R %s %s && %schown -R %s:%s %s',
            $sudo, escapeshellarg($mode), escapeshellarg($abs),
            $sudo, escapeshellarg($owner), escapeshellarg($owner), escapeshellarg($abs),
        );

        return [
            'item' => $rel,
            'type' => 'directory',
            'command' => $cmd,
            'sudo_required' => ! $env['is_root'],
            'safe_to_auto_execute' => $env['has_sudo'] || $env['is_root'],
            'verification' => "test -w {$abs} && echo OK",
        ];
    }

    /**
     * @param  list<string>  $packages
     * @param  array<string,mixed>  $env
     * @return list<string>
     */
    private function resolvePackageNames(array $packages, array $env): array
    {
        $v = $env['php_major_minor'];
        $vn = str_replace('.', '', (string) $v);
        $out = [];
        foreach ($packages as $p) {
            $p = str_replace(['{V}', '{VN}'], [$v, $vn], (string) $p);
            $out[] = $p;
        }
        return $out;
    }

    /**
     * @param  list<string>  $packages
     */
    private function buildAptOrDnfCommand(string $pm, array $packages): string
    {
        $escaped = array_map(static fn ($p) => escapeshellarg($p), $packages);
        $joined = implode(' ', $escaped);
        return match ($pm) {
            'apt'    => 'sudo apt-get update -y && sudo apt-get install -y '.$joined,
            'dnf'    => 'sudo dnf install -y '.$joined,
            'yum'    => 'sudo yum install -y '.$joined,
            'apk'    => 'sudo apk add --no-cache '.$joined,
            'pacman' => 'sudo pacman -S --noconfirm '.$joined,
            default  => 'echo "지원되지 않는 패키지 매니저: '.$pm.'"',
        };
    }

    private function isSpecialPackage(string $first): bool
    {
        return $first === 'ini-only'
            || $first === 'source-build'
            || str_starts_with($first, 'manual:')
            || str_starts_with($first, 'pecl:');
    }

    /**
     * @param  array<string,mixed>  $env
     */
    private function fpmReloadCommand(array $env): string
    {
        $svc = $env['php_fpm_service'];
        $sudo = $env['is_root'] ? '' : 'sudo ';
        return "{$sudo}systemctl reload {$svc} || {$sudo}service {$svc} reload";
    }

    /**
     * @param  array<string,mixed>  $env
     */
    private function prefixSudo(string $cmd, array $env): string
    {
        return $env['is_root'] ? $cmd : 'sudo '.$cmd;
    }

    /**
     * @param  array<string,mixed>  $env
     * @return array<string,mixed>
     */
    private function reloadFpm(array $env): array
    {
        $cmd = $this->fpmReloadCommand($env);
        return $this->runCommand($cmd);
    }

    /**
     * @return array{exit_code:int,stdout:string,stderr:string}
     */
    private function runCommand(string $cmd, ?string $sudoPassword = null): array
    {
        $timeout = (int) config('install-patches.safety.command_timeout_seconds', 180);

        if ($sudoPassword !== null && $sudoPassword !== '') {
            // sudo -S 를 통해 stdin 으로 비밀번호 전달 (가시 노출 X)
            $finalCmd = preg_replace('/\bsudo\b/', 'sudo -S', $cmd, 1);
            $process = Process::fromShellCommandline($finalCmd, null, null, $sudoPassword."\n");
        } else {
            $process = Process::fromShellCommandline($cmd);
        }
        $process->setTimeout($timeout);

        try {
            $process->run();
            return [
                'exit_code' => $process->getExitCode() ?? -1,
                'stdout'    => (string) $process->getOutput(),
                'stderr'    => (string) $process->getErrorOutput(),
            ];
        } catch (Throwable $e) {
            return [
                'exit_code' => -1,
                'stdout'    => '',
                'stderr'    => 'Process exception: '.$e->getMessage(),
            ];
        }
    }

    private function verifyExtensionLoaded(string $ext): bool
    {
        // 1. extension_loaded — 같은 프로세스에서는 reload 후에도 false 가능 (FPM 별도 프로세스)
        if (extension_loaded($ext)) {
            return true;
        }
        // 2. shell out to php -m (FPM 과 무관한 CLI 바이너리에서 새로 로드됨)
        $out = @shell_exec('php -m 2>/dev/null');
        if (! is_string($out)) {
            return false;
        }
        return (bool) preg_match('/^'.preg_quote($ext, '/').'$/mi', $out);
    }

    /**
     * @param  array<string,mixed>  $env
     */
    private function resolveManualNote(string $id, array $env): string
    {
        return match ($id) {
            'aapanel_php_extension' => sprintf(
                'aaPanel UI: 좌측 메뉴 → 소프트웨어 → PHP %s → 설정 → 확장 탭 에서 활성.',
                $env['php_major_minor'],
            ),
            default => '수동 설치 필요.',
        };
    }

    private function truncate(string $s, int $limit = 4000): string
    {
        if (strlen($s) <= $limit) {
            return $s;
        }
        return substr($s, 0, $limit)."\n... [truncated]";
    }

    /**
     * @return array<string,mixed>
     */
    private function error(string $code, string $message): array
    {
        return ['ok' => false, 'error' => $code, 'message' => $message];
    }
}
