<?php

declare(strict_types=1);

namespace App\Services\Install;

use App\Models\User;
use Illuminate\Support\Facades\Log;
use RuntimeException;

/**
 * LockGuard — install.lock 생성/검증/해제 + immutable bit (Linux).
 *
 *   - JSON + HMAC-SHA256 (APP_KEY 서명) 으로 위조 차단
 *   - 잠금 해제는 DB 의 super-admin 인증 필요
 *   - 모든 잠금 작업이 로그에 남음
 *   - **chattr +i** (root 권한 시) 로 부주의 삭제 차단 (GAP-07)
 *   - **HMAC 재서명** 지원 (APP_KEY 회전 시, GAP-09)
 *
 * @see /_qa-audit/INSTALLER_GAP_VS_GNUBOARD.md GAP-07/09
 */
final class LockGuard
{
    private const FILENAME = 'install.lock';

    private const VERSION = '1.1.0';

    private const HMAC_ALGO = 'sha256';

    public function path(): string
    {
        return storage_path(self::FILENAME);
    }

    public function exists(): bool
    {
        return is_file($this->path());
    }

    /**
     * Lock 파일이 존재하고 HMAC 서명이 유효한지 검증.
     */
    public function verify(): bool
    {
        if (! $this->exists()) {
            return false;
        }

        $raw = @file_get_contents($this->path());
        if ($raw === false) {
            return false;
        }

        $lock = json_decode($raw, true);
        if (! is_array($lock)) {
            return false;
        }

        $required = ['installed_at', 'installer_version', 'installer_ip', 'super_admin_id', 'hmac'];
        foreach ($required as $key) {
            if (! array_key_exists($key, $lock)) {
                return false;
            }
        }

        $expected = $this->computeHmac($lock);

        return hash_equals($expected, (string) $lock['hmac']);
    }

    /**
     * 원본 lock payload (검증 없이) 읽기 — 회전/조회용.
     *
     * @return array<string,mixed>|null
     */
    public function read(): ?array
    {
        if (! $this->exists()) {
            return null;
        }
        $raw = @file_get_contents($this->path());
        if ($raw === false) {
            return null;
        }
        $data = json_decode($raw, true);

        return is_array($data) ? $data : null;
    }

    /**
     * 새 install.lock 작성.
     *
     * @param  array{installer_ip:string, super_admin_id:string, user_agent?:string}  $context
     */
    public function create(array $context): string
    {
        if ($this->exists()) {
            throw new RuntimeException('install.lock 이 이미 존재합니다.');
        }

        $payload = [
            'installed_at'         => now()->toIso8601String(),
            'installer_version'    => self::VERSION,
            'installer_ip'         => $context['installer_ip'],
            'super_admin_id'       => $context['super_admin_id'],
            'installer_user_agent' => $context['user_agent'] ?? null,
            'app_key_hash'         => hash('sha256', (string) config('app.key')),
        ];

        $payload['hmac'] = $this->computeHmac($payload);

        $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false) {
            throw new RuntimeException('install.lock JSON 직렬화 실패');
        }

        $path = $this->path();
        @mkdir(dirname($path), 0775, true);

        if (file_put_contents($path, $json, LOCK_EX) === false) {
            throw new RuntimeException("install.lock 쓰기 실패: {$path}");
        }
        @chmod($path, 0600);

        // GAP-07: Linux 환경에서는 immutable bit 시도 (root 만 성공, 실패해도 무시)
        $this->trySetImmutable($path, true);

        Log::info('install.lock created', [
            'path' => $path,
            'ip'   => $context['installer_ip'],
            'sid'  => $context['super_admin_id'],
        ]);

        return $path;
    }

    /**
     * 잠금 해제 — super-admin 검증 후 install.lock 삭제.
     */
    public function release(User $admin, string $ip): void
    {
        if (method_exists($admin, 'hasRole') && ! $admin->hasRole('super-admin')) {
            throw new RuntimeException('super-admin 권한이 필요합니다.');
        }

        if (! $this->exists()) {
            return;
        }

        // immutable bit 해제 시도 (root 시)
        $this->trySetImmutable($this->path(), false);

        @unlink($this->path());

        Log::warning('install.lock released', [
            'by_admin_id' => $admin->getKey(),
            'ip'          => $ip,
            'at'          => now()->toIso8601String(),
        ]);
    }

    /**
     * GAP-09: HMAC 만 새 APP_KEY 로 재서명 — `wdot:rotate-install-key`.
     * lock 의 다른 필드는 보존, hmac + app_key_hash 만 갱신.
     *
     * @return array{old_hmac:string,new_hmac:string,was_valid:bool}
     */
    public function rotateHmac(): array
    {
        if (! $this->exists()) {
            throw new RuntimeException('install.lock 이 없습니다.');
        }
        $lock = $this->read();
        if ($lock === null) {
            throw new RuntimeException('install.lock 파싱 실패');
        }

        $oldHmac = (string) ($lock['hmac'] ?? '');
        $wasValid = $this->verify();

        // 새 APP_KEY 로 hmac 재계산
        $lock['app_key_hash'] = hash('sha256', (string) config('app.key'));
        unset($lock['hmac']);
        $newHmac = $this->computeHmac($lock);
        $lock['hmac'] = $newHmac;

        // 쓰기 (immutable 해제 → 쓰기 → 다시 immutable)
        $path = $this->path();
        $this->trySetImmutable($path, false);
        $json = json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if ($json === false || file_put_contents($path, $json, LOCK_EX) === false) {
            throw new RuntimeException("install.lock 재서명 쓰기 실패");
        }
        @chmod($path, 0600);
        $this->trySetImmutable($path, true);

        Log::warning('install.lock HMAC rotated', [
            'old_hmac_prefix' => substr($oldHmac, 0, 8),
            'new_hmac_prefix' => substr($newHmac, 0, 8),
            'was_valid'       => $wasValid,
        ]);

        return [
            'old_hmac'  => $oldHmac,
            'new_hmac'  => $newHmac,
            'was_valid' => $wasValid,
        ];
    }

    /**
     * HMAC 계산 — 필드 순서를 고정하여 재현 가능한 서명을 만든다.
     *
     * @param  array<string,mixed>  $payload
     */
    private function computeHmac(array $payload): string
    {
        $canonical = [
            'installed_at'      => $payload['installed_at'] ?? null,
            'installer_version' => $payload['installer_version'] ?? null,
            'installer_ip'      => $payload['installer_ip'] ?? null,
            'super_admin_id'    => $payload['super_admin_id'] ?? null,
            'app_key_hash'      => $payload['app_key_hash'] ?? hash('sha256', (string) config('app.key')),
        ];

        $message = json_encode($canonical, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        return hash_hmac(self::HMAC_ALGO, (string) $message, (string) config('app.key'));
    }

    /**
     * Linux 의 chattr +i / -i 시도. 실패해도 silent (root 가 아니거나 FS 미지원).
     */
    private function trySetImmutable(string $path, bool $set): void
    {
        if (PHP_OS_FAMILY !== 'Linux') {
            return;
        }
        if (! function_exists('shell_exec')) {
            return;
        }
        $flag = $set ? '+i' : '-i';
        // 2>&1 로 stderr 흡수. 실패는 무시.
        @shell_exec("chattr {$flag} ".escapeshellarg($path).' 2>&1');
    }
}
