<?php

declare(strict_types=1);

namespace App\Services\Install;

use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use ParagonIE\ConstantTime\Base32;
use RuntimeException;

/**
 * Stage 3 — super-admin 계정 + 2FA secret 생성.
 *
 * 비밀번호 정책:
 *   - 최소 12 자
 *   - zxcvbn score >= 3 (별도 PasswordPolicy::evaluate())
 *   - bcrypt hash (cost 12)
 *
 * 2FA:
 *   - 무작위 20 바이트 → Base32 인코딩 (TOTP RFC 6238)
 *   - QR 데이터는 controller 에서 otpauth:// URI 로 조립
 *
 * @see /_decisions/INSTALLER_DESIGN.md §3.2 Step 3
 */
final class AdminProvisioner
{
    /**
     * @param  array{email:string, name:string, password:string}  $input
     * @return array{user_id:string, two_factor_secret:string, recovery_codes:list<string>}
     */
    public function provision(array $input): array
    {
        return DB::transaction(function () use ($input) {
            // 이미 존재 시 거부 (재설치 시나리오는 wdot:reset 후에만 가능)
            if (User::query()->where('email', $input['email'])->exists()) {
                throw new RuntimeException("이미 등록된 이메일입니다: {$input['email']}");
            }

            $userId = (string) Str::uuid();
            $secret = $this->generateTotpSecret();
            $recoveryCodes = $this->generateRecoveryCodes(8);

            // Resolve default tenant (seeded earlier); blocks install if missing.
            $tenantId = DB::table('tenants')->orderBy('created_at')->value('id');
            if (! $tenantId) {
                throw new RuntimeException('No tenant found. Run basic seeder first.');
            }

            $user = new User();
            $user->forceFill([
                'id'                => $userId,
                'tenant_id'         => $tenantId,
                'name'              => $input['name'],
                'email'             => $input['email'],
                'password'          => Hash::make($input['password']),
                'email_verified_at' => now(),
                'two_factor_secret' => encrypt($secret),
                'created_at'        => now(),
                'updated_at'        => now(),
            ])->save();

            // Recovery codes go into the dedicated two_factor_recovery_codes table
            // (per migration 2026_05_21_200005) — one bcrypt-hashed row per code.
            $rows = [];
            foreach ($recoveryCodes as $code) {
                $rows[] = [
                    'id'         => (string) Str::uuid(),
                    'user_id'    => $userId,
                    'code_hash'  => password_hash($code, PASSWORD_BCRYPT),
                    'used_at'    => null,
                    'created_at' => now(),
                    'updated_at' => now(),
                ];
            }
            if ($rows) {
                DB::table('two_factor_recovery_codes')->insert($rows);
            }

            // super_admin 역할 부여 (spatie/laravel-permission, underscore — see DefaultRolesSeeder)
            if (method_exists($user, 'assignRole')) {
                $user->assignRole('super_admin');
            }

            return [
                'user_id'           => $userId,
                'two_factor_secret' => $secret,
                'recovery_codes'    => $recoveryCodes,
            ];
        });
    }

    /**
     * Base32 인코딩된 TOTP secret 생성 (20 bytes → 32 chars).
     */
    private function generateTotpSecret(): string
    {
        $bytes = random_bytes(20);

        // pragmarx/google2fa 의 Base32 인코딩과 호환되는 알파벳 사용 (RFC 4648 Base32)
        if (class_exists(Base32::class)) {
            return Base32::encodeUpper($bytes);
        }

        // Fallback — base32 인코딩 (의존성 없을 때)
        return self::base32Encode($bytes);
    }

    /**
     * @return list<string>
     */
    private function generateRecoveryCodes(int $count): array
    {
        $codes = [];
        for ($i = 0; $i < $count; $i++) {
            // 8-8 형식 (예: ab12-cd34)
            $codes[] = strtolower(bin2hex(random_bytes(4))).'-'.strtolower(bin2hex(random_bytes(4)));
        }

        return $codes;
    }

    /**
     * RFC 4648 Base32 encoder (의존성 없는 fallback).
     */
    private static function base32Encode(string $data): string
    {
        $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        $binary = '';
        for ($i = 0, $len = strlen($data); $i < $len; $i++) {
            $binary .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT);
        }
        $out = '';
        foreach (str_split($binary, 5) as $chunk) {
            $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
            $out .= $alphabet[bindec($chunk)];
        }

        return $out;
    }
}
