<?php
declare(strict_types=1);

use Medoo\Medoo;

/**
 * 数据库类
 * 继承 Medoo 查询构造器，补充原生 SQL 查询/操作方法
 */
class Db extends Medoo
{
    /** @var array<string, array> 全部库配置，首次调用 instance() 时从 database.php 加载 */
    private static array $config = [];

    /** @var array<string, self> 按库名缓存的单例连接 */
    private static array $instances = [];

    /** 事务嵌套计数，0 表示当前不在事务中 */
    private int $transactionCount = 0;

    public function __construct(array $options)
    {
        parent::__construct($options);
        register_shutdown_function([$this, 'assertTransactionClosed']);
    }

    /**
     * 获取单例连接，每个库名对应一个连接实例（全站共享）
     * 首次调用时加载 [projectDir]/config/database.php 到 self::$config，之后直接复用
     * @param string $name 库名，对应 database.php 配置数组中的键，默认 'default'
     */
    public static function instance(string $name = 'default'): self
    {
        if (!self::$config) {
            self::$config = require Mvc::$cfg['projectDir'] . '/config/database.php';
        }

        return self::$instances[$name] ??= new self(self::$config[$name]);
    }

    /**
     * 开启事务，支持嵌套调用：计数从 0 变为 1 时真正开启事务，之后只计数
     */
    public function begin(): void
    {
        if ($this->transactionCount === 0) {
            $this->pdo->beginTransaction();
        }
        $this->transactionCount++;
    }

    /**
     * 结束事务，计数减到 0 时真正提交事务
     * @throws MvcException end() 调用次数多于 begin() 时
     */
    public function end(): void
    {
        if ($this->transactionCount === 0) {
            throw new MvcException('Db::end() 调用次数多于 begin()，没有可结束的事务');
        }

        $this->transactionCount--;
        if ($this->transactionCount === 0) {
            $this->pdo->commit();
        }
    }

    /**
     * 程序结束时检查事务计数是否归零，不归零说明 begin()/end() 没有配对调用
     * @ignore 由 register_shutdown_function 自动调用
     */
    public function assertTransactionClosed(): void
    {
        if ($this->transactionCount !== 0) {
            throw new \RuntimeException("数据库事务未正确关闭：begin()/end() 调用次数不匹配，剩余计数 {$this->transactionCount}");
        }
    }

    /**
     * 执行原生 SQL 查询（SELECT），返回结果集
     * @param string $sql 原生 SQL，参数用 ? 占位
     * @param array $params 绑定参数
     * @return array
     */
    public function rawQuery(string $sql, array $params = []): array
    {
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);

        return $statement->fetchAll(\PDO::FETCH_ASSOC);
    }

    /**
     * 执行原生 SQL 操作（INSERT / UPDATE / DELETE），返回受影响行数
     * @param string $sql 原生 SQL，参数用 ? 占位
     * @param array $params 绑定参数
     * @return int 受影响行数
     */
    public function rawExecute(string $sql, array $params = []): int
    {
        $statement = $this->pdo->prepare($sql);
        $statement->execute($params);

        return $statement->rowCount();
    }
}
