<?php

declare(strict_types=1);

namespace App\Core;

use PDO;
use PDOException;
use RuntimeException;

/**
 * Database
 *
 * Thin PDO singleton. Every query in the application — from BaseModel
 * all the way down to a one-off report query — ultimately goes through
 * PDO prepared statements obtained here. There is no raw mysqli, no
 * string-concatenated SQL, anywhere in this codebase.
 */
final class Database
{
    private static ?PDO $connection = null;

    private function __construct()
    {
        // Not instantiable — use Database::connection().
    }

    public static function connection(): PDO
    {
        if (self::$connection instanceof PDO) {
            return self::$connection;
        }

        $config = require dirname(__DIR__, 2) . '/config/database.php';
        $cfg = $config['connections'][$config['default']];

        $dsn = sprintf(
            'mysql:host=%s;port=%s;dbname=%s;charset=%s',
            $cfg['host'],
            $cfg['port'],
            $cfg['database'],
            $cfg['charset']
        );

        try {
            self::$connection = new PDO($dsn, $cfg['username'], $cfg['password'], $cfg['options']);
        } catch (PDOException $e) {
            // Never leak DSN/credentials in the exception message shown
            // to a user — log the real error, throw a generic one.
            Logger::error('Database connection failed', ['error' => $e->getMessage()]);

            throw new RuntimeException('Unable to connect to the database.', previous: $e);
        }

        return self::$connection;
    }

    /**
     * Run $callback inside a transaction. Commits on success, rolls
     * back and re-throws on any exception. Nested calls are safe: only
     * the outermost call actually begins/commits the transaction.
     */
    public static function transaction(callable $callback): mixed
    {
        $pdo = self::connection();
        $isOutermost = !$pdo->inTransaction();

        if ($isOutermost) {
            $pdo->beginTransaction();
        }

        try {
            $result = $callback($pdo);

            if ($isOutermost) {
                $pdo->commit();
            }

            return $result;
        } catch (\Throwable $e) {
            if ($isOutermost && $pdo->inTransaction()) {
                $pdo->rollBack();
            }

            throw $e;
        }
    }

    /** Mainly for tests / CLI tools that need a clean connection. */
    public static function disconnect(): void
    {
        self::$connection = null;
    }
}
