<?php

namespace App\Core;

/**
 * Database — PDO Singleton
 *
 * Provides a single PDO connection instance per request lifecycle.
 * The connection is created lazily on first access and reused for
 * all subsequent queries within the same request.
 *
 * PDO configuration (ADR-024, DBP v1.0 Ch.4):
 *   ATTR_ERRMODE            = ERRMODE_EXCEPTION  (throw on error, never silent)
 *   ATTR_DEFAULT_FETCH_MODE = FETCH_ASSOC        (arrays only — never objects)
 *   ATTR_EMULATE_PREPARES   = false              (real prepared statements)
 *   charset                 = utf8mb4            (full Unicode including emoji)
 *
 * Connection parameters are read from config('database.*') which is
 * populated from the .env file by Bootstrap (INIT-003/INIT-005).
 *
 * Validation of the live connection (SELECT 1 test) is performed in
 * INIT-012 with actual database credentials.
 *
 * Usage (in BaseModel):
 *   $pdo = Database::getConnection();
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.4, ADR-024, INIT-008, INIT-012
 */
class Database
{
    /**
     * The single PDO connection instance.
     * Null until the first call to getConnection().
     * @var \PDO|null
     */
    private static ?\PDO $connection = null;

    /**
     * Private constructor — this class must not be instantiated.
     * All access is through static methods.
     */
    private function __construct()
    {
    }

    /**
     * Return the shared PDO connection, creating it on first access.
     *
     * @return \PDO  The live database connection.
     * @throws \RuntimeException If connection parameters are missing or invalid.
     */
    public static function getConnection(): \PDO
    {
        if (self::$connection === null) {
            self::$connection = self::connect();
        }

        return self::$connection;
    }

    /**
     * Create and return a new PDO connection using config/database.php values.
     *
     * Called once per request lifecycle by getConnection().
     * On failure: logs the error and delegates to ErrorHandler (never shows
     * raw PDO exception messages to the browser).
     *
     * @return \PDO
     */
    private static function connect(): \PDO
    {
        $driver  = (string) config('database.driver',  'mysql');
        $host    = (string) config('database.host',    '127.0.0.1');
        $port    = (string) config('database.port',    '3306');
        $name    = (string) config('database.name',    '');
        $user    = (string) config('database.user',    '');
        $pass    = (string) config('database.pass',    '');
        $charset = (string) config('database.charset', 'utf8mb4');

        $dsn = sprintf(
            '%s:host=%s;port=%s;dbname=%s;charset=%s',
            $driver,
            $host,
            $port,
            $name,
            $charset
        );

        // Read PDO options from config; fall back to the required defaults.
        $defaultOptions = [
            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
            \PDO::ATTR_EMULATE_PREPARES   => false,
        ];

        $configOptions = config('database.options', []);
        $options = is_array($configOptions)
            ? array_replace($defaultOptions, $configOptions)
            : $defaultOptions;

        try {
            return new \PDO($dsn, $user, $pass, $options);

        } catch (\PDOException $e) {
            Logger::error('Database connection failed', [
                'dsn'     => preg_replace('/dbname=[^;]+/', 'dbname=***', $dsn),
                'message' => $e->getMessage(),
            ]);

            // Never expose raw connection errors to the browser.
            // ErrorHandler shows errors/500.php in production.
            ErrorHandler::handleException($e);
            exit; // Unreachable; satisfies static analysis return-type expectations.
        }
    }

    /**
     * Reset the singleton connection.
     * Used in tests to force a fresh connection between test cases.
     * NOT for use in production request paths.
     *
     * @return void
     */
    public static function reset(): void
    {
        self::$connection = null;
    }

    /**
     * Inject a pre-built PDO instance.
     * Used in tests to provide a mock or in-memory SQLite connection.
     * NOT for use in production request paths.
     *
     * @param \PDO $pdo
     * @return void
     */
    public static function setConnection(\PDO $pdo): void
    {
        self::$connection = $pdo;
    }
}
