<?php

declare(strict_types=1);

final class MyronInstallerConfigService
{
    private const BLOCKED_ADMIN_PATHS = [
        '/login',
        '/admin',
        '/dashboard',
        '/wp-admin',
        '/app',
        '/app/site',
        '/app/install',
        '/install',
        '/site',
        '/api',
    ];

    public function __construct(
        private readonly string $appRoot,
    ) {
    }

    /**
     * @param array<string, mixed> $input
     * @return array<string, string>
     */
    public function normalizeDatabaseConfig(array $input): array
    {
        $driver = strtolower(trim((string) ($input['db_driver'] ?? 'mysql')));
        if ($driver !== 'mysql') {
            throw new InvalidArgumentException('MyronCMS public first-run installation requires MySQL or MariaDB.');
        }

        $host = trim((string) ($input['db_host'] ?? ''));
        $name = trim((string) ($input['db_name'] ?? ''));
        $user = trim((string) ($input['db_user'] ?? ''));
        $password = trim((string) ($input['db_pass'] ?? ''));
        $port = trim((string) ($input['db_port'] ?? '3306'));
        $socket = trim((string) ($input['db_socket'] ?? ''));

        if ($host === '' && $socket === '') {
            throw new InvalidArgumentException('Database host is required.');
        }
        if ($name === '') {
            throw new InvalidArgumentException('Database name is required.');
        }
        if ($user === '') {
            throw new InvalidArgumentException('Database username is required.');
        }
        if ($port !== '' && preg_match('/^[0-9]{2,5}$/', $port) !== 1) {
            throw new InvalidArgumentException('Database port must be a valid port number.');
        }

        return [
            'MYRON_DB_DRIVER' => 'mysql',
            'MYRON_DB_HOST' => $host,
            'MYRON_DB_PORT' => $port !== '' ? $port : '3306',
            'MYRON_DB_SOCKET' => $socket,
            'MYRON_DB_NAME' => $name,
            'MYRON_DB_USER' => $user,
            'MYRON_DB_PASS' => $password,
            'MYRON_DB_CHARSET' => 'utf8mb4',
            'MYRON_DB_SQLITE_PATH' => '',
        ];
    }

    /**
     * @param array<string, mixed> $input
     * @return array{site_title:string, app_url:string, public_base_path:string, admin_base_path:string, admin_email:string, admin_password:string}
     */
    public function normalizeInstallInput(array $input): array
    {
        $siteTitle = trim((string) ($input['site_title'] ?? ''));
        if ($siteTitle === '') {
            throw new InvalidArgumentException('Site name is required.');
        }

        $appUrl = $this->normalizeSiteUrl((string) ($input['site_url'] ?? ''));
        $publicBasePath = $this->normalizePublicBasePath((string) ($input['public_base_path'] ?? '/'));
        $adminBasePath = $this->normalizeAdminBasePath((string) ($input['admin_login_path'] ?? $input['admin_base_path'] ?? ''));
        $adminEmail = myronNormalizeEmail((string) ($input['admin_email'] ?? ''));
        if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('A valid administrator email is required.');
        }

        $adminPassword = (string) ($input['admin_password'] ?? '');
        if (strlen($adminPassword) < 8) {
            throw new InvalidArgumentException('Administrator password must be at least 8 characters.');
        }

        return [
            'site_title' => $siteTitle,
            'app_url' => $appUrl,
            'public_base_path' => $publicBasePath,
            'admin_base_path' => $adminBasePath,
            'admin_email' => $adminEmail,
            'admin_password' => $adminPassword,
        ];
    }

    /**
     * @param array<string, string> $databaseConfig
     */
    public function assertDatabaseConnection(array $databaseConfig): void
    {
        $pdo = $this->createConnection($databaseConfig);
        $pdo->query('SELECT 1');
    }

    /**
     * @param array<string, string> $databaseConfig
     */
    public function createConnection(array $databaseConfig): PDO
    {
        $driver = strtolower($databaseConfig['MYRON_DB_DRIVER'] ?? 'mysql');
        if ($driver === 'sqlite') {
            $sqlitePath = trim((string) ($databaseConfig['MYRON_DB_SQLITE_PATH'] ?? ''));
            if ($sqlitePath === '') {
                throw new InvalidArgumentException('SQLite database path is required.');
            }

            $directory = dirname($sqlitePath);
            if (file_exists($directory) && !is_dir($directory)) {
                throw new RuntimeException('SQLite database directory is not writable.');
            }
            if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
                throw new RuntimeException('Unable to create the SQLite database directory.');
            }

            $pdo = new PDO('sqlite:' . $sqlitePath, null, null, $this->pdoOptions());
            $pdo->exec('PRAGMA foreign_keys = ON');
            return $pdo;
        }

        $charset = $databaseConfig['MYRON_DB_CHARSET'] ?? 'utf8mb4';
        if (trim((string) ($databaseConfig['MYRON_DB_SOCKET'] ?? '')) !== '') {
            $dsn = sprintf(
                'mysql:unix_socket=%s;dbname=%s;charset=%s',
                $databaseConfig['MYRON_DB_SOCKET'],
                $databaseConfig['MYRON_DB_NAME'],
                $charset
            );
        } else {
            $dsn = sprintf(
                'mysql:host=%s;port=%s;dbname=%s;charset=%s',
                $databaseConfig['MYRON_DB_HOST'],
                $databaseConfig['MYRON_DB_PORT'] !== '' ? $databaseConfig['MYRON_DB_PORT'] : '3306',
                $databaseConfig['MYRON_DB_NAME'],
                $charset
            );
        }

        return new PDO(
            $dsn,
            $databaseConfig['MYRON_DB_USER'],
            $databaseConfig['MYRON_DB_PASS'],
            $this->pdoOptions()
        );
    }

    public function generateAppKey(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }

    /**
     * @return array<int, mixed>
     */
    private function pdoOptions(): array
    {
        return [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ];
    }

    private function normalizeSiteUrl(string $value): string
    {
        $url = rtrim(trim($value), '/');
        if ($url === '') {
            throw new InvalidArgumentException('Site URL is required.');
        }
        if (filter_var($url, FILTER_VALIDATE_URL) === false) {
            throw new InvalidArgumentException('Site URL must be a valid absolute URL.');
        }

        $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
        if (!in_array($scheme, ['http', 'https'], true)) {
            throw new InvalidArgumentException('Site URL must start with http:// or https://.');
        }

        return $url;
    }

    private function normalizeAdminBasePath(string $value): string
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            throw new InvalidArgumentException('Admin login path is required.');
        }
        if (preg_match('/^https?:\\/\\//i', $trimmed) === 1 || str_starts_with($trimmed, '//')) {
            throw new InvalidArgumentException('Admin login path must be a path, not a full URL.');
        }

        $path = '/' . trim($trimmed, '/');
        $path = preg_replace('#/+#', '/', $path) ?: '/';
        if ($path === '/' || preg_match('#[^A-Za-z0-9/_-]#', $path) === 1) {
            throw new InvalidArgumentException('Admin login path can contain only letters, numbers, slashes, dashes, and underscores.');
        }

        $lower = strtolower($path);
        if (in_array($lower, self::BLOCKED_ADMIN_PATHS, true)) {
            throw new InvalidArgumentException('Choose a less predictable admin login path.');
        }

        if (str_starts_with($lower, '/app/')) {
            throw new InvalidArgumentException('Choose an admin login path outside the reserved /app path.');
        }

        return $path;
    }

    private function normalizePublicBasePath(string $value): string
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            $trimmed = '/';
        }
        if (preg_match('/^https?:\\/\\//i', $trimmed) === 1 || str_starts_with($trimmed, '//')) {
            throw new InvalidArgumentException('Public website path must be a path, not a full URL.');
        }

        $path = '/' . trim($trimmed, '/');
        $path = preg_replace('#/+#', '/', $path) ?: '/';
        if (preg_match('#[^A-Za-z0-9/_-]#', $path) === 1) {
            throw new InvalidArgumentException('Public website path can contain only letters, numbers, slashes, dashes, and underscores.');
        }

        $lower = strtolower($path);
        if (in_array($lower, ['/app', '/app/site', '/app/install', '/api'], true)) {
            throw new InvalidArgumentException('Public website path cannot use a reserved runtime path.');
        }

        return $path === '/' ? '/' : $path;
    }
}
