<?php

declare(strict_types=1);

namespace BotGuard\Controllers\Install;

use BotGuard\Core\Config;
use BotGuard\Core\Controller;
use BotGuard\Core\Csrf;
use BotGuard\Core\Database;
use BotGuard\Core\Migrator;
use BotGuard\Core\Request;
use BotGuard\Core\Response;
use BotGuard\Core\Session;
use BotGuard\Core\View;
use BotGuard\Database\Seeds\DatabaseSeeder;
use BotGuard\Services\License\LicenseClient;
use Throwable;

/**
 * Multi-step installation wizard.
 *
 * Steps: Welcome → License → Requirements → Database → Administrator →
 * Website → Install → Finish. Collected input is carried in the session under
 * `__install` until the final Install step writes `.env`, runs migrations, seeds
 * the database, persists the license, and drops the install lock file.
 *
 * All routes are guarded by SetupMiddleware, which only permits them while the
 * app is NOT yet installed.
 */
final class InstallController extends Controller
{
    private const SESSION_KEY = '__install';

    /** Ordered wizard steps for progress display + navigation. */
    private const STEPS = ['welcome', 'license', 'requirements', 'database', 'administrator', 'website', 'install', 'finish'];

    public function __construct(
        View $view,
        Session $session,
        Csrf $csrf,
        Request $request,
        private LicenseClient $license,
    ) {
        parent::__construct($view, $session, $csrf, $request);
    }

    public static function lockFile(): string
    {
        return storage_path('installed.lock');
    }

    public static function isInstalled(): bool
    {
        return is_file(self::lockFile());
    }

    // --- Step 1: Welcome -------------------------------------------------

    public function index(Request $request): Response
    {
        return Response::redirect('/install/welcome');
    }

    public function welcome(Request $request): Response
    {
        return $this->step('welcome', ['title' => 'Welcome']);
    }

    // --- Step 2: License -------------------------------------------------

    public function license(Request $request): Response
    {
        return $this->step('license', ['title' => 'License', 'data' => $this->data()]);
    }

    public function saveLicense(Request $request): Response
    {
        $key = trim((string) $request->input('license_key'));
        $domain = trim((string) $request->input('domain', $request->server('HTTP_HOST', '')));
        $email = trim((string) $request->input('email'));
        $company = trim((string) $request->input('company'));

        $validator = $this->validate(['email' => 'required|email']);
        if ($validator->fails()) {
            return $this->redirect('/install/license', 'error', $validator->firstError());
        }

        $status = $this->license->activate($key, $domain, $email, $company);
        if (! $status->valid) {
            return $this->redirect('/install/license', 'error', $status->message);
        }

        $this->merge([
            'license_key'    => $key,
            'license_domain' => $domain,
            'license_email'  => $email,
            'license_company' => $company,
            'license_mode'   => $status->mode,
            'license_token'  => $status->token,
        ]);

        return $this->redirect('/install/requirements', 'success', $status->message);
    }

    // --- Step 3: Requirements -------------------------------------------

    public function requirements(Request $request): Response
    {
        $checks = $this->requirementChecks();
        $passed = array_reduce($checks, static fn (bool $c, array $r): bool => $c && (! $r['required'] || $r['passed']), true);

        return $this->step('requirements', ['title' => 'Requirements', 'checks' => $checks, 'passed' => $passed]);
    }

    // --- Step 4: Database ------------------------------------------------

    public function database(Request $request): Response
    {
        return $this->step('database', ['title' => 'Database', 'data' => $this->data()]);
    }

    public function saveDatabase(Request $request): Response
    {
        $config = [
            'driver'   => 'mysql',
            'host'     => trim((string) $request->input('db_host', '127.0.0.1')),
            'port'     => (int) $request->input('db_port', 3306),
            'database' => trim((string) $request->input('db_database')),
            'username' => trim((string) $request->input('db_username')),
            'password' => (string) $request->input('db_password', ''),
            'charset'  => 'utf8mb4',
            'options'  => [],
        ];

        $error = $this->testConnection($config);
        if ($error !== null) {
            $this->merge($this->dbToSession($config));

            return $this->redirect('/install/database', 'error', 'Connection failed: ' . $error);
        }

        $this->merge($this->dbToSession($config));

        return $this->redirect('/install/administrator', 'success', 'Database connection successful.');
    }

    // --- Step 5: Administrator ------------------------------------------

    public function administrator(Request $request): Response
    {
        return $this->step('administrator', ['title' => 'Administrator', 'data' => $this->data()]);
    }

    public function saveAdministrator(Request $request): Response
    {
        $validator = $this->validate([
            'admin_name'     => 'required|minlen:2|maxlen:120',
            'admin_email'    => 'required|email',
            'admin_password' => 'required|minlen:8|confirmed',
        ]);

        if ($validator->fails()) {
            return $this->redirect('/install/administrator', 'error', $validator->firstError());
        }

        $this->merge([
            'admin_name'     => (string) $request->input('admin_name'),
            'admin_email'    => (string) $request->input('admin_email'),
            'admin_password' => (string) $request->input('admin_password'),
        ]);

        return $this->redirect('/install/website', 'success', 'Administrator account details saved.');
    }

    // --- Step 6: Website settings ---------------------------------------

    public function website(Request $request): Response
    {
        $data = $this->data();
        $data['app_url'] ??= rtrim((string) config('app.url'), '/');
        $data['app_name'] ??= (string) config('app.name', 'BotGuard');

        return $this->step('website', [
            'title'     => 'Website',
            'data'      => $data,
            'timezones' => \DateTimeZone::listIdentifiers(),
            'providers' => ['puzzle', 'recaptcha', 'hcaptcha', 'turnstile'],
        ]);
    }

    public function saveWebsite(Request $request): Response
    {
        $validator = $this->validate([
            'app_name' => 'required|maxlen:120',
            'app_url'  => 'required|url|maxlen:255',
        ]);

        if ($validator->fails()) {
            return $this->redirect('/install/website', 'error', $validator->firstError());
        }

        $this->merge([
            'app_name'         => (string) $request->input('app_name'),
            'app_url'          => rtrim((string) $request->input('app_url'), '/'),
            'app_timezone'     => (string) $request->input('app_timezone', 'UTC'),
            'captcha_provider' => (string) $request->input('captcha_provider', 'puzzle'),
        ]);

        return Response::redirect('/install/install');
    }

    // --- Step 7: Install (review + run) ---------------------------------

    public function install(Request $request): Response
    {
        return $this->step('install', ['title' => 'Install', 'data' => $this->data()]);
    }

    /**
     * Perform the installation: write .env, migrate, seed, persist license, lock.
     */
    public function run(Request $request): Response
    {
        $data = $this->data();

        // Guard: required prior steps must be present.
        foreach (['db_database', 'admin_email', 'app_url'] as $required) {
            if (empty($data[$required])) {
                return $this->redirect('/install/database', 'error', 'Please complete all steps before installing.');
            }
        }

        try {
            $dbConfig = [
                'driver'   => 'mysql',
                'host'     => $data['db_host'],
                'port'     => (int) $data['db_port'],
                'database' => $data['db_database'],
                'username' => $data['db_username'],
                'password' => $data['db_password'] ?? '',
                'charset'  => 'utf8mb4',
                'options'  => [],
            ];

            // 1) Write the .env file.
            $this->writeEnv($data);

            // 2) Make the wizard's app settings visible to the seeder this request.
            Config::set('app', [
                'name' => $data['app_name'],
                'url'  => $data['app_url'],
            ]);
            Config::set('captcha', array_merge((array) config('captcha'), ['provider' => $data['captcha_provider'] ?? 'puzzle']));

            // 3) Run migrations + seed the registry, then create the admin.
            $db = new Database($dbConfig);
            (new Migrator($db, base_path('database/migrations')))->run();

            $seeder = new DatabaseSeeder($db);
            $seeder->seedModules();
            $seeder->seedThresholds();
            $seeder->seedSettings();
            $seeder->createAdmin($data['admin_name'], $data['admin_email'], $data['admin_password'], $data['license_company'] ?? null);

            // 4) Persist license + captcha settings.
            $this->persistSettings($db, $data);

            // 5) Drop the lock file so the app switches out of install mode.
            $this->writeLock($data);
        } catch (Throwable $e) {
            return $this->redirect('/install/install', 'error', 'Installation failed: ' . $e->getMessage());
        }

        // Clear wizard state and finish.
        $this->session->forget(self::SESSION_KEY);

        return Response::redirect('/install/finish');
    }

    // --- Step 8: Finish --------------------------------------------------

    public function finish(Request $request): Response
    {
        // Shown once immediately after run(); the install lock now exists so
        // SetupMiddleware whitelists this single post-install route.
        return $this->rawStep('finish', ['title' => 'Installation complete']);
    }

    // --- Helpers ---------------------------------------------------------

    /**
     * Render an install step wrapped in the installer chrome.
     *
     * @param array<string, mixed> $data
     */
    private function step(string $step, array $data): Response
    {
        return $this->rawStep($step, $data);
    }

    /**
     * @param array<string, mixed> $data
     */
    private function rawStep(string $step, array $data): Response
    {
        $this->view->share('csrf', $this->csrf);
        $this->view->share('session', $this->session);

        $html = $this->view->render('install.' . $step, array_merge($data, [
            'steps'       => self::STEPS,
            'currentStep' => $step,
        ]));

        return (new Response($html))->withHeader('Content-Type', 'text/html; charset=utf-8');
    }

    /**
     * @return array<string, mixed>
     */
    private function data(): array
    {
        $data = $this->session->get(self::SESSION_KEY, []);

        return is_array($data) ? $data : [];
    }

    /**
     * @param array<string, mixed> $values
     */
    private function merge(array $values): void
    {
        $this->session->put(self::SESSION_KEY, array_merge($this->data(), $values));
    }

    /**
     * @param array<string, mixed> $config
     * @return array<string, mixed>
     */
    private function dbToSession(array $config): array
    {
        return [
            'db_host'     => $config['host'],
            'db_port'     => $config['port'],
            'db_database' => $config['database'],
            'db_username' => $config['username'],
            'db_password' => $config['password'],
        ];
    }

    /**
     * Attempt a real connection with the supplied config. Returns an error
     * string on failure, or null on success.
     *
     * @param array<string, mixed> $config
     */
    private function testConnection(array $config): ?string
    {
        if (empty($config['database']) || empty($config['username'])) {
            return 'Database name and username are required.';
        }

        try {
            $db = new Database($config);
            $db->pdo()->query('SELECT 1');

            return null;
        } catch (Throwable $e) {
            return $e->getMessage();
        }
    }

    /**
     * @return array<int, array{label:string, required:bool, passed:bool, value:string}>
     */
    private function requirementChecks(): array
    {
        $writable = static fn (string $path): bool => is_dir($path) ? is_writable($path) : is_writable(dirname($path));

        return [
            ['label' => 'PHP 8.3+', 'required' => true, 'passed' => PHP_VERSION_ID >= 80300, 'value' => PHP_VERSION],
            ['label' => 'PDO MySQL extension', 'required' => true, 'passed' => extension_loaded('pdo_mysql'), 'value' => extension_loaded('pdo_mysql') ? 'loaded' : 'missing'],
            ['label' => 'mbstring extension', 'required' => true, 'passed' => extension_loaded('mbstring'), 'value' => extension_loaded('mbstring') ? 'loaded' : 'missing'],
            ['label' => 'JSON extension', 'required' => true, 'passed' => extension_loaded('json'), 'value' => extension_loaded('json') ? 'loaded' : 'missing'],
            ['label' => 'cURL extension (remote CAPTCHA/license)', 'required' => false, 'passed' => extension_loaded('curl'), 'value' => extension_loaded('curl') ? 'loaded' : 'missing'],
            ['label' => 'storage/ writable', 'required' => true, 'passed' => $writable(storage_path()), 'value' => storage_path()],
            ['label' => '.env writable', 'required' => true, 'passed' => $writable(base_path('.env')), 'value' => base_path('.env')],
        ];
    }

    /**
     * Build and write the .env file from wizard data.
     *
     * @param array<string, mixed> $data
     */
    private function writeEnv(array $data): void
    {
        $appKey = bin2hex(random_bytes(32));
        $q = static fn (string $v): string => '"' . str_replace('"', '\\"', $v) . '"';

        $lines = [
            'APP_NAME=' . $q((string) $data['app_name']),
            'APP_ENV=production',
            'APP_DEBUG=false',
            'APP_URL=' . $q((string) $data['app_url']),
            'APP_TIMEZONE=' . $q((string) ($data['app_timezone'] ?? 'UTC')),
            'APP_KEY=' . $appKey,
            '',
            'DB_DRIVER=mysql',
            'DB_HOST=' . $q((string) $data['db_host']),
            'DB_PORT=' . (int) $data['db_port'],
            'DB_DATABASE=' . $q((string) $data['db_database']),
            'DB_USERNAME=' . $q((string) $data['db_username']),
            'DB_PASSWORD=' . $q((string) ($data['db_password'] ?? '')),
            '',
            'SESSION_NAME=BOTGUARD_SESSID',
            'SESSION_LIFETIME=7200',
            'RATE_LIMIT_PER_MINUTE=120',
            '',
            'CAPTCHA_PROVIDER=' . $q((string) ($data['captcha_provider'] ?? 'puzzle')),
            '',
            'THRESHOLD_HUMAN=80',
            'THRESHOLD_SUSPICIOUS=60',
            '',
        ];

        $written = @file_put_contents(base_path('.env'), implode("\n", $lines) . "\n");
        if ($written === false) {
            throw new \RuntimeException('Could not write the .env file. Check permissions on the project root.');
        }
    }

    /**
     * Persist license + captcha info into the settings table.
     *
     * @param array<string, mixed> $data
     */
    private function persistSettings(Database $db, array $data): void
    {
        $rows = [
            ['key' => 'license_key', 'value' => (string) ($data['license_key'] ?? ''), 'type' => 'secret', 'group' => 'license'],
            ['key' => 'license_domain', 'value' => (string) ($data['license_domain'] ?? ''), 'type' => 'string', 'group' => 'license'],
            ['key' => 'license_email', 'value' => (string) ($data['license_email'] ?? ''), 'type' => 'string', 'group' => 'license'],
            ['key' => 'license_company', 'value' => (string) ($data['license_company'] ?? ''), 'type' => 'string', 'group' => 'license'],
            ['key' => 'license_mode', 'value' => (string) ($data['license_mode'] ?? 'offline'), 'type' => 'string', 'group' => 'license'],
            ['key' => 'license_token', 'value' => (string) ($data['license_token'] ?? ''), 'type' => 'secret', 'group' => 'license'],
            ['key' => 'captcha_provider', 'value' => (string) ($data['captcha_provider'] ?? 'puzzle'), 'type' => 'string', 'group' => 'captcha'],
        ];

        foreach ($rows as $row) {
            $exists = $db->selectOne('SELECT id FROM `settings` WHERE `key` = ?', [$row['key']]);
            if ($exists === null) {
                $db->insert('settings', $row);
            } else {
                $db->update('settings', ['value' => $row['value'], 'type' => $row['type']], ['key' => $row['key']]);
            }
        }
    }

    /**
     * @param array<string, mixed> $data
     */
    private function writeLock(array $data): void
    {
        $payload = json_encode([
            'installed_at' => now(),
            'version'      => '1.0.0',
            'app_url'      => $data['app_url'] ?? '',
            'license_mode' => $data['license_mode'] ?? 'offline',
        ], JSON_PRETTY_PRINT);

        if (@file_put_contents(self::lockFile(), $payload) === false) {
            throw new \RuntimeException('Could not write the install lock file to storage/.');
        }
    }
}
