<?php

namespace App\Services;

use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use PDO;
use RuntimeException;

class ApplicationInstaller
{
    public function status(): array
    {
        return [
            'database_connection' => (string) config('database.default', 'mysql'),
            'app_key_configured' => $this->appKeyConfigured(),
            'storage_link_exists' => file_exists(public_path('storage')),
            'database_ready' => $this->databaseReady(),
            'tables_installed' => $this->tablesInstalled(),
            'web_installer_locked' => $this->webInstallerLocked(),
        ];
    }

    public function webInstallerAvailable(): bool
    {
        if (app()->environment('local')) {
            return true;
        }

        return ! $this->webInstallerLocked();
    }

    public function formDefaults(?string $requestRoot = null): array
    {
        $appUrl = (string) config('app.url', 'http://localhost');

        if ($this->looksLikePlaceholderUrl($appUrl) && filled($requestRoot)) {
            $appUrl = rtrim($requestRoot, '/');
        }

        $defaultConnection = strtolower((string) config('database.default', 'mysql'));
        $connectionConfig = config("database.connections.{$defaultConnection}");

        if (! is_array($connectionConfig) || ! in_array($defaultConnection, ['mysql', 'mariadb'], true)) {
            $defaultConnection = 'mysql';
            $connectionConfig = config('database.connections.mysql', []);
        }

        $defaultHost = (string) ($connectionConfig['host'] ?? '');

        if ($defaultHost === '' || (! app()->environment('local') && $defaultHost === '127.0.0.1')) {
            $defaultHost = 'localhost';
        }

        return [
            'app_name' => (string) config('app.name', 'Saleem Sial Analytics'),
            'app_url' => $appUrl,
            'db_connection' => $defaultConnection,
            'db_host' => $defaultHost,
            'db_port' => (string) ($connectionConfig['port'] ?? '3306'),
            'db_database' => (string) ($connectionConfig['database'] ?? ''),
            'db_username' => (string) ($connectionConfig['username'] ?? ''),
            'db_password' => (string) ($connectionConfig['password'] ?? ''),
        ];
    }

    public function configureForWebInstall(array $input): array
    {
        $environment = $this->normalizeEnvironmentValues($input);

        $this->writeEnvironmentFile($environment);
        $this->applyRuntimeConfiguration($environment);

        return ['Saved installation settings'];
    }

    public function run(): array
    {
        $steps = [];

        $this->prepareDatabase();
        $steps[] = 'Database ready';

        $this->runArtisan('optimize:clear');
        $steps[] = 'Cleared cached Laravel state';

        if (! $this->appKeyConfigured()) {
            $this->generateApplicationKey();
            $steps[] = 'Generated APP_KEY';
        } else {
            $steps[] = 'APP_KEY already configured';
        }

        if (! file_exists(public_path('storage'))) {
            $this->runArtisan('storage:link');
            $steps[] = 'Created public storage link';
        } else {
            $steps[] = 'Public storage link already exists';
        }

        $this->runArtisan('migrate', ['--seed' => true, '--force' => true]);
        $steps[] = 'Ran migrations and seeders';

        return $steps;
    }

    public function completeWebInstall(): void
    {
        if (app()->environment('local')) {
            return;
        }

        $directory = dirname($this->webInstallerLockPath());

        if (! is_dir($directory) && ! mkdir($directory, 0777, true) && ! is_dir($directory)) {
            throw new RuntimeException('Unable to create the installer lock directory.');
        }

        if (file_put_contents($this->webInstallerLockPath(), now()->toDateTimeString()) === false) {
            throw new RuntimeException('Unable to create the installer lock file.');
        }
    }

    protected function prepareDatabase(): void
    {
        $connection = strtolower((string) config('database.default', 'mysql'));

        if (! in_array($connection, ['mysql', 'mariadb'], true)) {
            throw new RuntimeException('Web installer supports MySQL and MariaDB only.');
        }

        $host = (string) config("database.connections.{$connection}.host", '127.0.0.1');
        $port = (string) config("database.connections.{$connection}.port", '3306');
        $database = (string) config("database.connections.{$connection}.database", '');
        $username = (string) config("database.connections.{$connection}.username", 'root');
        $password = (string) config("database.connections.{$connection}.password", '');

        if ($database === '') {
            throw new RuntimeException('DB_DATABASE is required for MySQL installation.');
        }

        try {
            $this->connectToMysqlDatabase($host, $port, $database, $username, $password);

            return;
        } catch (\PDOException $exception) {
            $errorCode = $this->mysqlErrorCode($exception);

            if (in_array($errorCode, [1044, 1045], true)) {
                throw new RuntimeException(
                    sprintf(
                        'Could not connect to the MySQL database [%s]. In cPanel, add user [%s] to database [%s] with ALL PRIVILEGES, then try again. If your host suggests it, use DB host [localhost] instead of [127.0.0.1].',
                        $database,
                        $username,
                        $database
                    ),
                    0,
                    $exception
                );
            }

            if ($errorCode !== 1049) {
                throw new RuntimeException('Unable to connect to the MySQL database. ' . $exception->getMessage(), 0, $exception);
            }
        }

        try {
            $pdo = new PDO(
                sprintf('mysql:host=%s;port=%s;charset=utf8mb4', $host, $port),
                $username,
                $password,
                [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
            );

            $pdo->exec(
                'CREATE DATABASE IF NOT EXISTS `' . str_replace('`', '``', $database) . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'
            );
        } catch (\PDOException $exception) {
            $errorCode = $this->mysqlErrorCode($exception);

            if (in_array($errorCode, [1044, 1045, 1142], true)) {
                throw new RuntimeException(
                    sprintf(
                        'The database [%s] must already exist on cPanel and user [%s] must have ALL PRIVILEGES on it. Create it in cPanel -> MySQL Databases, assign the user, then run the installer again.',
                        $database,
                        $username
                    ),
                    0,
                    $exception
                );
            }

            throw new RuntimeException('Unable to prepare the MySQL database. ' . $exception->getMessage(), 0, $exception);
        }

        $this->connectToMysqlDatabase($host, $port, $database, $username, $password);
    }

    protected function databaseReady(): bool
    {
        try {
            DB::connection()->getPdo();

            return true;
        } catch (\Throwable) {
            return false;
        }
    }

    protected function tablesInstalled(): bool
    {
        try {
            return Schema::hasTable('migrations') && Schema::hasTable('users');
        } catch (\Throwable) {
            return false;
        }
    }

    protected function runArtisan(string $command, array $parameters = []): void
    {
        $exitCode = Artisan::call($command, $parameters);

        if ($exitCode !== 0) {
            $message = trim(Artisan::output());

            throw new RuntimeException($message !== '' ? $message : sprintf('The [%s] command failed.', $command));
        }
    }

    protected function appKeyConfigured(): bool
    {
        return filled(config('app.key')) && ! $this->usingTemporaryAppKey();
    }

    protected function webInstallerLocked(): bool
    {
        return file_exists($this->webInstallerLockPath());
    }

    protected function webInstallerLockPath(): string
    {
        return storage_path('app/install.lock');
    }

    protected function usingTemporaryAppKey(): bool
    {
        return filter_var((string) env('INSTALLER_TEMP_APP_KEY', false), FILTER_VALIDATE_BOOL);
    }

    protected function connectToMysqlDatabase(string $host, string $port, string $database, string $username, string $password): PDO
    {
        return new PDO(
            sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database),
            $username,
            $password,
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
    }

    protected function mysqlErrorCode(\PDOException $exception): int
    {
        if (isset($exception->errorInfo[1]) && is_numeric($exception->errorInfo[1])) {
            return (int) $exception->errorInfo[1];
        }

        return is_numeric($exception->getCode()) ? (int) $exception->getCode() : 0;
    }

    protected function looksLikePlaceholderUrl(string $url): bool
    {
        return $url === '' || Str::contains($url, ['your-domain.com', 'localhost', '127.0.0.1']);
    }

    protected function normalizeEnvironmentValues(array $input): array
    {
        $connection = strtolower((string) ($input['db_connection'] ?? 'mysql'));

        return [
            'APP_NAME' => trim((string) ($input['app_name'] ?? 'Saleem Sial Analytics')) ?: 'Saleem Sial Analytics',
            'APP_ENV' => app()->environment('local') ? 'local' : 'production',
            'APP_DEBUG' => app()->environment('local') ? 'true' : 'false',
            'APP_URL' => rtrim((string) ($input['app_url'] ?? 'http://localhost'), '/'),
            'DB_CONNECTION' => $connection,
            'DB_HOST' => (string) ($input['db_host'] ?? 'localhost'),
            'DB_PORT' => (string) ($input['db_port'] ?? '3306'),
            'DB_DATABASE' => (string) ($input['db_database'] ?? ''),
            'DB_USERNAME' => (string) ($input['db_username'] ?? ''),
            'DB_PASSWORD' => (string) ($input['db_password'] ?? ''),
            'FILESYSTEM_DISK' => app()->environment('local') ? 'local' : 'public',
            'SESSION_DRIVER' => 'database',
            'QUEUE_CONNECTION' => 'database',
            'CACHE_STORE' => 'database',
        ];
    }

    protected function writeEnvironmentFile(array $values): void
    {
        $environmentPath = base_path('.env');

        if (file_exists($environmentPath)) {
            $content = file_get_contents($environmentPath);
        } else {
            $templatePath = $this->environmentTemplatePath();

            $content = $templatePath !== null
                ? file_get_contents($templatePath)
                : $this->defaultEnvironmentTemplate();
        }

        if ($content === false) {
            throw new RuntimeException('Unable to read the environment template.');
        }

        foreach ($values as $key => $value) {
            $content = $this->replaceOrAppendEnvironmentValue($content, $key, $value);
        }

        if (file_put_contents($environmentPath, $content) === false) {
            throw new RuntimeException('Unable to write the .env file. Check file permissions on the server.');
        }
    }

    protected function environmentTemplatePath(): ?string
    {
        $serverTemplate = base_path('.env.server.example');
        $defaultTemplate = base_path('.env.example');

        if (! app()->environment('local') && file_exists($serverTemplate)) {
            return $serverTemplate;
        }

        if (file_exists($defaultTemplate)) {
            return $defaultTemplate;
        }

        if (file_exists($serverTemplate)) {
            return $serverTemplate;
        }

        return null;
    }

    protected function defaultEnvironmentTemplate(): string
    {
        return <<<'ENV'
APP_NAME="Saleem Sial Analytics"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=https://your-domain.com

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=public
QUEUE_CONNECTION=database

CACHE_STORE=database

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_SCHEME=null
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-email-password
MAIL_FROM_ADDRESS="hello@saleemsial.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"
ENV;
    }

    protected function replaceOrAppendEnvironmentValue(string $content, string $key, string $value): string
    {
        $line = $key . '=' . $this->formatEnvironmentValue($value);
        $pattern = '/^' . preg_quote($key, '/') . '=.*$/m';

        if (preg_match($pattern, $content) === 1) {
            return (string) preg_replace($pattern, $line, $content, 1);
        }

        return rtrim($content) . PHP_EOL . $line . PHP_EOL;
    }

    protected function formatEnvironmentValue(string $value): string
    {
        if ($value === '') {
            return '';
        }

        if (preg_match('/^[A-Za-z0-9._:\\/\\-]+$/', $value) === 1) {
            return $value;
        }

        return '"' . addcslashes($value, "\\\"") . '"';
    }

    protected function applyRuntimeConfiguration(array $environment): void
    {
        $connection = $environment['DB_CONNECTION'];

        $configuration = [
            'app.name' => $environment['APP_NAME'],
            'app.debug' => $environment['APP_DEBUG'] === 'true',
            'app.url' => $environment['APP_URL'],
            'app.key' => config('app.key'),
            'database.default' => $connection,
            "database.connections.{$connection}.host" => $environment['DB_HOST'],
            "database.connections.{$connection}.port" => $environment['DB_PORT'],
            "database.connections.{$connection}.database" => $environment['DB_DATABASE'],
            "database.connections.{$connection}.username" => $environment['DB_USERNAME'],
            "database.connections.{$connection}.password" => $environment['DB_PASSWORD'],
            'filesystems.default' => $environment['FILESYSTEM_DISK'],
        ];

        if (! $this->isInstallRequest()) {
            $configuration['session.driver'] = $environment['SESSION_DRIVER'];
            $configuration['queue.default'] = $environment['QUEUE_CONNECTION'];
            $configuration['cache.default'] = $environment['CACHE_STORE'];
        }

        config($configuration);

        DB::purge();
        DB::purge('mysql');
        DB::purge('mariadb');
        DB::purge('sqlite');
    }

    protected function generateApplicationKey(): void
    {
        $key = 'base64:' . base64_encode(
            Encrypter::generateKey((string) config('app.cipher', 'AES-256-CBC'))
        );

        $this->writeEnvironmentFile([
            'APP_KEY' => $key,
        ]);

        config([
            'app.key' => $key,
        ]);

        putenv('APP_KEY=' . $key);
        putenv('INSTALLER_TEMP_APP_KEY=0');

        $_ENV['APP_KEY'] = $key;
        $_SERVER['APP_KEY'] = $key;
        $_ENV['INSTALLER_TEMP_APP_KEY'] = '0';
        $_SERVER['INSTALLER_TEMP_APP_KEY'] = '0';
    }

    protected function isInstallRequest(): bool
    {
        if (! app()->bound('request')) {
            return false;
        }

        return request()->is('install') || request()->is('install/*');
    }
}
