<?php

declare(strict_types=1);

namespace Install\Services;

use PDO;
use PDOException;

final class DatabaseChecker
{
    /**
     * @param array<string,mixed> $data
     * @param array<string,mixed> $config
     *
     * @return array{
     *     success:bool,
     *     message:string,
     *     diagnostics:array<int,array<string,mixed>>,
     *     connection:array<string,mixed>
     * }
     */
    public function check(
        array $data,
        array $config
    ): array {
        $host = trim((string)($data['db_host'] ?? 'localhost'));
        $port = (int)($data['db_port'] ?? 3306);
        $database = trim((string)($data['db_database'] ?? ''));
        $username = trim((string)($data['db_username'] ?? ''));
        $password = (string)($data['db_password'] ?? '');

        $this->assertRequired(
            $host,
            $database,
            $username,
            $port
        );

        $diagnostics = [];

        try {
            $serverPdo = $this->connect(
                $host,
                $port,
                null,
                $username,
                $password
            );

            $diagnostics[] = $this->diagnostic(
                'Serveur joignable',
                true,
                $host . ':' . $port,
                'Connexion au serveur réussie.'
            );

            $serverInfo = $this->readServerInformation($serverPdo);
            $compatibility = $this->checkCompatibility(
                $serverInfo,
                $config
            );

            $diagnostics[] = $this->diagnostic(
                'Version compatible',
                $compatibility['compatible'],
                $serverInfo['display_version'],
                $compatibility['message']
            );

            $databaseExists = $this->databaseExists(
                $serverPdo,
                $database
            );

            $diagnostics[] = $this->diagnostic(
                'Base de données trouvée',
                $databaseExists,
                $database,
                $databaseExists
                    ? 'La base existe et peut être sélectionnée.'
                    : 'La base demandée est introuvable.'
            );

            if (!$databaseExists) {
                return [
                    'success' => false,
                    'message' => 'La base de données demandée est introuvable.',
                    'diagnostics' => $diagnostics,
                    'connection' => [],
                ];
            }

            $databasePdo = $this->connect(
                $host,
                $port,
                $database,
                $username,
                $password
            );

            $diagnostics[] = $this->diagnostic(
                'Identifiants valides',
                true,
                $username,
                'L’utilisateur et le mot de passe sont acceptés.'
            );

            $databaseInfo = $this->readDatabaseInformation(
                $databasePdo
            );

            $diagnostics[] = $this->diagnostic(
                'Encodage de la base',
                strtolower($databaseInfo['character_set']) === 'utf8mb4',
                $databaseInfo['character_set'],
                strtolower($databaseInfo['character_set']) === 'utf8mb4'
                    ? 'Encodage recommandé détecté.'
                    : 'Utilisez de préférence utf8mb4.'
            );

            $diagnostics[] = $this->diagnostic(
                'Collation de la base',
                str_starts_with(
                    strtolower($databaseInfo['collation']),
                    'utf8mb4_'
                ),
                $databaseInfo['collation'],
                str_starts_with(
                    strtolower($databaseInfo['collation']),
                    'utf8mb4_'
                )
                    ? 'Collation compatible.'
                    : 'Une collation utf8mb4 est recommandée.'
            );

            $privileges = $this->checkPrivileges(
                $databasePdo,
                $database
            );

            foreach ($privileges['diagnostics'] as $diagnostic) {
                $diagnostics[] = $diagnostic;
            }

            $allRequiredChecksPassed =
                $compatibility['compatible']
                && $databaseExists
                && $privileges['ready'];

            return [
                'success' => $allRequiredChecksPassed,
                'message' => $allRequiredChecksPassed
                    ? 'Connexion à la base de données validée.'
                    : 'La connexion fonctionne, mais certains droits obligatoires sont absents.',
                'diagnostics' => $diagnostics,
                'connection' => [
                    'db_host' => $host,
                    'db_port' => $port,
                    'db_database' => $database,
                    'db_username' => $username,
                    'db_password' => $password,
                    'db_driver' => 'mysql',
                    'db_charset' => 'utf8mb4',
                    'server_product' => $serverInfo['product'],
                    'server_version' => $serverInfo['version'],
                    'database_character_set' => $databaseInfo['character_set'],
                    'database_collation' => $databaseInfo['collation'],
                ],
            ];
        } catch (PDOException $exception) {
            $diagnostics[] = $this->diagnostic(
                'Connexion à la base',
                false,
                'Échec',
                $this->friendlyError($exception)
            );

            return [
                'success' => false,
                'message' => $this->friendlyError($exception),
                'diagnostics' => $diagnostics,
                'connection' => [],
            ];
        }
    }

    private function assertRequired(
        string $host,
        string $database,
        string $username,
        int $port
    ): void {
        if ($host === '') {
            throw new \InvalidArgumentException(
                'Le serveur de base de données est obligatoire.'
            );
        }

        if ($database === '') {
            throw new \InvalidArgumentException(
                'Le nom de la base de données est obligatoire.'
            );
        }

        if ($username === '') {
            throw new \InvalidArgumentException(
                'L’utilisateur de la base de données est obligatoire.'
            );
        }

        if ($port < 1 || $port > 65535) {
            throw new \InvalidArgumentException(
                'Le port doit être compris entre 1 et 65535.'
            );
        }
    }

    private function connect(
        string $host,
        int $port,
        ?string $database,
        string $username,
        string $password
    ): PDO {
        $dsn = 'mysql:host=' . $host
            . ';port=' . $port
            . ($database !== null
                ? ';dbname=' . $database
                : '')
            . ';charset=utf8mb4';

        return new PDO(
            $dsn,
            $username,
            $password,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_TIMEOUT => 8,
                PDO::ATTR_EMULATE_PREPARES => false,
            ]
        );
    }

    /**
     * @return array{
     *     product:string,
     *     version:string,
     *     display_version:string
     * }
     */
    private function readServerInformation(
        PDO $pdo
    ): array {
        $rawVersion = (string)$pdo
            ->query('SELECT VERSION()')
            ->fetchColumn();

        $product = stripos(
            $rawVersion,
            'mariadb'
        ) !== false
            ? 'MariaDB'
            : 'MySQL';

        preg_match(
            '/(\d+\.\d+\.\d+)/',
            $rawVersion,
            $matches
        );

        $version = (string)($matches[1] ?? $rawVersion);

        return [
            'product' => $product,
            'version' => $version,
            'display_version' => $product . ' ' . $version,
        ];
    }

    /**
     * @param array<string,mixed> $serverInfo
     * @param array<string,mixed> $config
     *
     * @return array{compatible:bool,message:string}
     */
    private function checkCompatibility(
        array $serverInfo,
        array $config
    ): array {
        $product = (string)($serverInfo['product'] ?? 'MySQL');
        $version = (string)($serverInfo['version'] ?? '0.0.0');

        $minimum = $product === 'MariaDB'
            ? (string)($config['minimum_mariadb_version'] ?? '10.6.0')
            : (string)($config['minimum_mysql_version'] ?? '8.0.0');

        $compatible = version_compare(
            $version,
            $minimum,
            '>='
        );

        return [
            'compatible' => $compatible,
            'message' => $compatible
                ? 'Version compatible avec Darlink ERP.'
                : $product
                    . ' '
                    . $minimum
                    . ' ou supérieur est requis.',
        ];
    }

    private function databaseExists(
        PDO $pdo,
        string $database
    ): bool {
        $statement = $pdo->prepare(
            'SELECT COUNT(*) '
            . 'FROM information_schema.schemata '
            . 'WHERE schema_name = :database'
        );

        $statement->execute([
            'database' => $database,
        ]);

        return (int)$statement->fetchColumn() > 0;
    }

    /**
     * @return array{character_set:string,collation:string}
     */
    private function readDatabaseInformation(
        PDO $pdo
    ): array {
        $row = $pdo->query(
            'SELECT '
            . '@@character_set_database AS character_set, '
            . '@@collation_database AS collation'
        )->fetch();

        return [
            'character_set' => (string)($row['character_set'] ?? ''),
            'collation' => (string)($row['collation'] ?? ''),
        ];
    }

    /**
     * @return array{
     *     ready:bool,
     *     diagnostics:array<int,array<string,mixed>>
     * }
     */
    private function checkPrivileges(
        PDO $pdo,
        string $database
    ): array {
        $required = [
            'SELECT',
            'INSERT',
            'UPDATE',
            'DELETE',
            'CREATE',
            'ALTER',
            'INDEX',
            'DROP',
        ];

        $grants = $pdo->query(
            'SHOW GRANTS FOR CURRENT_USER'
        )->fetchAll(PDO::FETCH_COLUMN);

        $grantText = strtoupper(
            implode(' ', array_map('strval', $grants))
        );

        $allPrivileges =
            str_contains($grantText, 'ALL PRIVILEGES')
            || str_contains($grantText, 'GRANT ALL');

        $diagnostics = [];
        $ready = true;

        foreach ($required as $privilege) {
            $granted = $allPrivileges
                || preg_match(
                    '/(^|[\s,])'
                    . preg_quote($privilege, '/')
                    . '([\s,]|$)/',
                    $grantText
                ) === 1;

            $diagnostics[] = $this->diagnostic(
                'Droit ' . $privilege,
                $granted,
                $granted ? 'Accordé' : 'Absent',
                $granted
                    ? 'Droit disponible pour l’installation.'
                    : 'Ce droit est obligatoire pour installer Darlink ERP.'
            );

            if (!$granted) {
                $ready = false;
            }
        }

        return [
            'ready' => $ready,
            'diagnostics' => $diagnostics,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function diagnostic(
        string $label,
        bool $success,
        string $value,
        string $message
    ): array {
        return [
            'label' => $label,
            'status' => $success ? 'success' : 'error',
            'value' => $value,
            'message' => $message,
            'required' => true,
        ];
    }

    private function friendlyError(
        PDOException $exception
    ): string {
        $message = $exception->getMessage();
        $code = (string)$exception->getCode();

        if (
            $code === '1045'
            || str_contains(
                strtolower($message),
                'access denied'
            )
        ) {
            return 'Accès refusé. Vérifiez l’utilisateur et le mot de passe de la base de données.';
        }

        if (
            $code === '1049'
            || str_contains(
                strtolower($message),
                'unknown database'
            )
        ) {
            return 'La base de données indiquée n’existe pas.';
        }

        if (
            $code === '2002'
            || str_contains(
                strtolower($message),
                'connection refused'
            )
            || str_contains(
                strtolower($message),
                'no connection could be made'
            )
        ) {
            return 'Le serveur de base de données est injoignable. Vérifiez le serveur et le port.';
        }

        if (
            str_contains(
                strtolower($message),
                'could not find driver'
            )
        ) {
            return 'Le pilote PDO MySQL n’est pas disponible sur ce serveur.';
        }

        return 'Connexion impossible : ' . $message;
    }
}
