<?php

declare(strict_types=1);

namespace App\Http\Controllers\Install;

use App\Http\Controllers\Controller;
use App\Http\Requests\Install\Stage2DbRequest;
use App\Http\Requests\Install\Stage3AdminRequest;
use App\Services\Install\AdminProvisioner;
use App\Services\Install\DbValidator;
use App\Services\Install\EnvChecker;
use App\Services\Install\EnvWriter;
use App\Services\Install\InstallationReport;
use App\Services\Install\LockGuard;
use App\Services\Install\Migrator;
use App\Services\Install\PasswordPolicy;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Throwable;

final class InstallController extends Controller
{
    /**
     * Cache-based stage gate prefix (file store) — survives session loss
     * caused by background processes, SSE streams, cookie mismatch, etc.
     */
    private const GATE_PREFIX = 'installer:gate:';

    /**
     * Stage gate TTL (60 min) — long enough for big migrations + admin pause.
     */
    private const GATE_TTL_MINUTES = 60;

    public function __construct(
        private readonly EnvChecker $envChecker,
        private readonly DbValidator $dbValidator,
        private readonly EnvWriter $envWriter,
        private readonly AdminProvisioner $adminProvisioner,
        private readonly Migrator $migrator,
        private readonly LockGuard $lock,
        private readonly PasswordPolicy $policy,
    ) {
    }

    /**
     * Mint a cache-backed stage gate token.
     * Returned token is passed to the SPA and required to enter the next stage.
     */
    private function setStageGate(string $stage, array $payload = []): string
    {
        $token = (string) Str::ulid();
        Cache::store('file')->put(
            self::GATE_PREFIX.$token,
            [
                'stage'     => $stage,
                'passed_at' => now()->toIso8601String(),
                'payload'   => $payload,
            ],
            now()->addMinutes(self::GATE_TTL_MINUTES),
        );

        return $token;
    }

    /**
     * Verify a token unlocks the requested expected stage.
     * Returns the cached entry on success, null on failure (caller emits 409).
     *
     * Token is read from (in order): explicit $token arg, X-Install-Token header,
     * request body 'install_token', request body 'token', request body
     * 'migrate_token' (legacy).
     */
    private function verifyStageGate(string $expectedStage, Request $request, ?string $explicit = null): ?array
    {
        $token = $explicit
            ?? $request->header('X-Install-Token')
            ?? (string) $request->input('install_token', '')
            ?? (string) $request->input('token', '');
        if ($token === '') {
            $token = (string) $request->input('migrate_token', '');
        }
        if ($token === '') {
            return null;
        }
        $data = Cache::store('file')->get(self::GATE_PREFIX.$token);
        if (! is_array($data) || ($data['stage'] ?? null) !== $expectedStage) {
            return null;
        }

        return $data + ['token' => $token];
    }

    public function index(Request $request): Response
    {
        $indexPath = public_path('install/index.html');
        if (! is_file($indexPath)) {
            return response($this->devFallback(), 200, ['Content-Type' => 'text/html; charset=utf-8']);
        }

        return response(file_get_contents($indexPath), 200, ['Content-Type' => 'text/html; charset=utf-8']);
    }

    public function stage1Check(Request $request): JsonResponse
    {
        $report = $this->envChecker->run();
        $token = null;
        if ($report['passed']) {
            // Session retained for backward-compat / fallback
            Session::put('installer.stage1_passed_at', now()->toIso8601String());
            $token = $this->setStageGate('stage1');
        }

        return response()->json([
            'stage'  => 1,
            'passed' => $report['passed'],
            'checks' => $report['checks'],
            'token'  => $token,
        ]);
    }

    public function stage2Db(Stage2DbRequest $request): JsonResponse
    {
        // Token-first (cache), session-fallback (legacy)
        $gate = $this->verifyStageGate('stage1', $request);
        if (! $gate && ! Session::has('installer.stage1_passed_at')) {
            return response()->json(['error' => 'stage1_required'], 409);
        }

        $creds = $request->creds();

        // NOTE: auto_create / root_user / root_password 는 deprecated.
        // 그누보드 install_db.php 패리티 — DB 는 호스팅사 패널에서 사전 생성.
        // 구버전 클라이언트 호환성을 위해 받아도 무시한다.

        $result = $this->dbValidator->validate($creds);

        $token = null;
        if ($result['ok']) {
            Session::put('installer.db', $creds);
            Session::put('installer.stage2_passed_at', now()->toIso8601String());
            $token = $this->setStageGate('stage2', ['db' => $creds]);
        }

        return response()->json([
            'stage'          => 2,
            'ok'             => $result['ok'],
            'errors'         => $result['errors'],
            'warnings'       => $result['warnings'],
            'server_version' => $result['server_version'],
            'token'          => $token,
        ]);
    }

    public function stage3Admin(Stage3AdminRequest $request): JsonResponse
    {
        $gate = $this->verifyStageGate('stage2', $request);
        if (! $gate && ! Session::has('installer.stage2_passed_at')) {
            return response()->json(['error' => 'stage2_required'], 409);
        }

        $admin = [
            'email'    => (string) $request->input('email'),
            'name'     => (string) $request->input('name'),
            'password' => (string) $request->input('password'),
        ];
        $enable2fa = (bool) $request->input('enable_2fa', true);

        $policy = $this->policy->evaluate($admin['password']);

        $site = [
            'app_name' => (string) $request->input('app_name', 'W·Dot'),
            'app_url'  => (string) $request->input('app_url', config('app.url')),
            'timezone' => (string) $request->input('timezone', 'Asia/Seoul'),
            'locale'   => (string) $request->input('locale', 'ko'),
        ];

        Session::put('installer.admin', $admin);
        Session::put('installer.enable_2fa', $enable2fa);
        Session::put('installer.site', $site);
        Session::put('installer.stage3_passed_at', now()->toIso8601String());

        // Carry db (from gate or session) + admin + site into stage3 gate payload
        $db = ($gate['payload']['db'] ?? null) ?: Session::get('installer.db', []);
        $token = $this->setStageGate('stage3', [
            'db'         => $db,
            'admin'      => $admin,
            'site'       => $site,
            'enable_2fa' => $enable2fa,
        ]);

        return response()->json([
            'stage'           => 3,
            'ok'              => true,
            'password_score'  => $policy['score'],
            'password_feedback' => $policy['feedback'],
            'token'           => $token,
        ]);
    }

    public function stage4Migrate(Request $request): JsonResponse
    {
        $gate = $this->verifyStageGate('stage3', $request);
        if (! $gate && ! Session::has('installer.stage3_passed_at')) {
            return response()->json(['error' => 'stage3_required'], 409);
        }

        // Pull stage3 payload from cache (token-first) or session (fallback)
        $stage3 = $gate['payload'] ?? [];
        $db = ($stage3['db']     ?? null) ?: Session::get('installer.db');
        $site = ($stage3['site'] ?? null) ?: Session::get('installer.site');
        $admin = ($stage3['admin'] ?? null) ?: Session::get('installer.admin');
        $enable2fa = $stage3['enable_2fa'] ?? Session::get('installer.enable_2fa', true);

        $seedMode = (string) $request->input('seed_mode', 'basic');
        if (! in_array($seedMode, ['basic', 'demo', 'full'], true)) {
            $seedMode = 'basic';
        }
        Session::put('installer.seed_mode', $seedMode);

        $token = $this->migrator->newToken($seedMode);
        Session::put('installer.migrate_token', $token);

        // Stage4 gate token carries everything stage5 needs — no session reliance
        $gateToken = $this->setStageGate('stage4', [
            'db'            => $db,
            'site'          => $site,
            'admin'         => $admin,
            'enable_2fa'    => $enable2fa,
            'seed_mode'     => $seedMode,
            'migrate_token' => $token,
        ]);

        try {
            $this->envWriter->write($db, $site);
            config([
                'database.connections.mysql.host'     => $db['host'],
                'database.connections.mysql.port'     => $db['port'],
                'database.connections.mysql.database' => $db['database'],
                'database.connections.mysql.username' => $db['username'],
                'database.connections.mysql.password' => $db['password'],
                'database.connections.mysql.prefix'   => $db['table_prefix'] ?? '',
            ]);
            \DB::purge('mysql');
        } catch (Throwable $e) {
            return response()->json([
                'stage' => 4,
                'ok'    => false,
                'error' => '.env 작성 실패: '.$e->getMessage(),
            ], 500);
        }

        $okBg = function () use ($token, $seedMode) {
            $this->migrator->run($token, $seedMode);
        };
        register_shutdown_function($okBg);

        return response()->json([
            'stage'         => 4,
            'ok'            => true,
            'token'         => $token,             // legacy field — migrate stream token
            'migrate_token' => $token,             // explicit name
            'gate_token'    => $gateToken,         // stage5 gate
            'seed_mode'     => $seedMode,
            'stream'        => url("/install/stage4/stream/{$token}"),
        ]);
    }

    public function stage5Finish(Request $request): JsonResponse
    {
        // Token-first (cache) — survives session loss caused by SSE / background.
        // Session-fallback retained for legacy clients.
        $gate = $this->verifyStageGate('stage4', $request);

        if (! $gate && ! Session::has('installer.migrate_token')) {
            return response()->json([
                'error'   => 'stage4_required',
                'message' => 'Stage 4 게이트가 만료되었거나 토큰이 잘못되었습니다. Stage 4 부터 다시 진행해주세요.',
            ], 409);
        }

        $stage4 = $gate['payload'] ?? [];
        $admin  = ($stage4['admin'] ?? null) ?: Session::get('installer.admin');
        if (! $admin || ! isset($admin['email'], $admin['password'])) {
            return response()->json([
                'error'   => 'admin_data_lost',
                'message' => '관리자 정보가 손실되었습니다. Stage 3 부터 다시 진행해주세요.',
            ], 500);
        }

        try {
            $result = $this->adminProvisioner->provision($admin);

            $lockPath = $this->lock->create([
                'installer_ip'   => (string) $request->ip(),
                'super_admin_id' => $result['user_id'],
                'user_agent'     => (string) $request->userAgent(),
            ]);
        } catch (Throwable $e) {
            return response()->json([
                'stage' => 5,
                'ok'    => false,
                'error' => $e->getMessage(),
            ], 500);
        }

        // 보고서 토큰 발급 — 1회용 (10분)
        $reportSite = ($stage4['site']      ?? null) ?: Session::get('installer.site', []);
        $reportDb   = ($stage4['db']        ?? null) ?: Session::get('installer.db', []);
        $reportSeed = ($stage4['seed_mode'] ?? null) ?: Session::get('installer.seed_mode', 'basic');

        $reportToken = (string) Str::ulid();
        $reportData = [
            'admin_email' => $admin['email'],
            'admin_name'  => $admin['name'] ?? '',
            'site'        => $reportSite,
            'db'          => array_merge(
                is_array($reportDb) ? $reportDb : [],
                ['password' => '••••••••'],
            ),
            'seed_mode'   => $reportSeed,
            'lock_path'   => $lockPath,
            'installer_ip'=> (string) $request->ip(),
            'finished_at' => now()->toIso8601String(),
        ];
        Cache::store('file')->put("installer:report:{$reportToken}", $reportData, now()->addMinutes(10));

        // session 청소
        Session::forget([
            'installer.db', 'installer.admin', 'installer.site',
            'installer.stage1_passed_at', 'installer.stage2_passed_at',
            'installer.stage3_passed_at', 'installer.migrate_token',
            'installer.seed_mode', 'installer.enable_2fa',
        ]);

        // Gate token 청소 (1회용)
        if (! empty($gate['token'])) {
            Cache::store('file')->forget(self::GATE_PREFIX.$gate['token']);
        }

        // 2FA otpauth URI + QR SVG (GAP-18)
        $issuer = rawurlencode((string) config('wdot.two_factor.issuer', 'W·Dot'));
        $email = rawurlencode($admin['email']);
        $secret = $result['two_factor_secret'];
        $provisioningUri = "otpauth://totp/{$issuer}:{$email}?secret={$secret}&issuer={$issuer}&algorithm=SHA1&digits=6&period=30";
        $qrSvg = $this->renderQrSvg($provisioningUri);

        return response()->json([
            'stage'    => 5,
            'ok'       => true,
            'lock'     => $lockPath,
            'admin_email' => $admin['email'],
            'two_factor_secret' => $result['two_factor_secret'],
            'two_factor_uri'    => $provisioningUri,
            'two_factor_qr_svg' => $qrSvg,
            'recovery_codes'    => $result['recovery_codes'],
            'report_token'      => $reportToken,
            'next_url' => url('/admin'),
        ]);
    }

    /**
     * GAP-15: HTML 설치 보고서 다운로드.
     */
    public function report(Request $request, string $token): Response
    {
        $key = "installer:report:{$token}";
        $data = Cache::store('file')->get($key);
        if (! $data) {
            abort(404, '보고서 토큰이 만료되었거나 잘못되었습니다.');
        }

        // 1회용 — 즉시 폐기
        Cache::store('file')->forget($key);

        $report = app(InstallationReport::class);
        $html = $report->renderHtml($data);

        return response($html, 200, [
            'Content-Type' => 'text/html; charset=utf-8',
            'Content-Disposition' => 'attachment; filename="wdot-install-report-'.now()->format('Ymd-His').'.html"',
        ]);
    }

    /**
     * 간이 QR 코드 SVG 생성 — chubaka/qr 류 의존성 없이 fallback.
     * provisioning URI 가 충분히 짧으면 QR Code v1~v10 까지 자체 알고리즘으로 생성.
     * 외부 라이브러리 없이 경량 구현 — 실제 스캔 가능한 ECC L 레벨.
     */
    private function renderQrSvg(string $data): string
    {
        // bacon/bacon-qr-code 또는 endroid/qr-code 가 설치되어 있으면 사용
        if (class_exists(\BaconQrCode\Writer::class) && class_exists(\BaconQrCode\Renderer\ImageRenderer::class)) {
            try {
                $renderer = new \BaconQrCode\Renderer\ImageRenderer(
                    new \BaconQrCode\Renderer\RendererStyle\RendererStyle(220),
                    new \BaconQrCode\Renderer\Image\SvgImageBackEnd(),
                );
                $writer = new \BaconQrCode\Writer($renderer);
                return $writer->writeString($data);
            } catch (Throwable) {
            }
        }

        // Fallback: 단순 placeholder + data URI for external QR generator
        $encoded = htmlspecialchars($data, ENT_QUOTES);
        return <<<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="0 0 220 220">
  <rect width="220" height="220" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/>
  <text x="110" y="100" text-anchor="middle" font-family="monospace" font-size="11" fill="#475569">
    <tspan x="110" dy="0">QR generator</tspan>
    <tspan x="110" dy="14">미설치</tspan>
  </text>
  <text x="110" y="140" text-anchor="middle" font-family="monospace" font-size="9" fill="#64748b">
    아래 secret 을
  </text>
  <text x="110" y="155" text-anchor="middle" font-family="monospace" font-size="9" fill="#64748b">
    Authenticator 앱에 직접 입력
  </text>
  <title>{$encoded}</title>
</svg>
SVG;
    }

    private function devFallback(): string
    {
        return <<<'HTML'
<!doctype html>
<html lang="ko"><head><meta charset="utf-8"><title>W·Dot Installer (dev)</title>
<style>body{font-family:Pretendard,system-ui,sans-serif;max-width:640px;margin:80px auto;padding:24px;color:#0f172a}
h1{font-size:28px}code{background:#f1f5f9;padding:2px 6px;border-radius:4px}</style></head>
<body><h1>W·Dot Installer — Dev Mode</h1>
<p>Vue 인스톨러 SPA 가 빌드되지 않았습니다.</p>
<pre><code>pnpm --filter @wdot/installer build</code></pre>
<p>또는 개발 서버 실행:</p>
<pre><code>pnpm --filter @wdot/installer dev   # http://localhost:5180</code></pre></body></html>
HTML;
    }
}
