<?php
/**
 * @copyright (C)2016-2099 Hnaoyun Inc.
 * @author XingMeng
 * @email hnxsh@foxmail.com
 * @date 2016年11月6日
 *  数据库mysqli驱动
 */
namespace core\database;

use core\basic\Config;

class Mysqli implements Builder
{

    protected static $mysqli;

    /** @var bool 软失败：SQL 错误时回滚并返回 false，不终止请求（仅 VisitsCounter 刷盘等场景） */
    protected static $failSoft = false;

    /** @var bool 最近一次软失败是否发生过 SQL 错误 */
    protected static $failSoftError = false;

    /** @var int 软失败作用域嵌套深度；错误标记保留到最外层退出才清 */
    protected static $failSoftDepth = 0;

    protected $master;

    protected $slave;

    protected $begin = false;

    private function __construct()
    {}

    public function __destruct()
    {
        if ($this->begin) { // 存在待提交的事务时自动进行提交
            $this->commitTransaction();
        }
    }

    // 获取单一实例，使用单一实例数据库连接类
    public static function getInstance()
    {
        if (! self::$mysqli) {
            self::$mysqli = new self();
        }
        return self::$mysqli;
    }

    /**
     * 开启/关闭软失败模式（支持嵌套引用计数）。
     * 仅最外层开启时清错误标记；内层退出不关窗口、不清标记。
     */
    public static function setFailSoft($enabled)
    {
        if ($enabled) {
            if (self::$failSoftDepth === 0) {
                self::$failSoftError = false;
            }
            self::$failSoftDepth ++;
            self::$failSoft = true;
            return;
        }
        if (self::$failSoftDepth > 0) {
            self::$failSoftDepth --;
        }
        if (self::$failSoftDepth === 0) {
            self::$failSoft = false;
            self::$failSoftError = false;
        }
    }

    /** 软失败模式下是否发生过 SQL 错误（读后不清除，由最外层 setFailSoft(false) 重置） */
    public static function hadFailSoftError()
    {
        return self::$failSoftError;
    }

    // 连接数据库，接受数据库连接参数，返回数据库连接对象
    public function conn($cfg)
    {
        // PHP 8.1+ 默认抛 mysqli_sql_exception；关闭报告并用 catch 兜住，统一走友好错误页
        if (function_exists('mysqli_report')) {
            mysqli_report(MYSQLI_REPORT_OFF);
        }
        if (! extension_loaded('mysqli')) {
            if (extension_loaded('pdo_mysql')) {
                error('未检测到您服务器环境的mysqli数据库扩展，请检查php.ini中是否已经开启该扩展！<br>另外，检测到您服务器支持pdo_mysql扩展，您也可以修改数据库配置连接驱动为pdo_mysql试试！');
            } else {
                error('未检测到您服务器环境的mysqli数据库扩展，请检查php.ini中是否已经开启该扩展！');
            }
        }
        
        // 优化>php5.3版本 在win2008以上服务器连接
        if ($cfg['host'] == 'localhost') {
            $cfg['host'] = '127.0.0.1';
        }
        
        try {
            $conn = @new \Mysqli($cfg['host'], $cfg['user'], $cfg['passwd'], $cfg['dbname'], $cfg['port']);
        } catch (\Throwable $e) {
            // PHP 8.1+ 可能抛 mysqli_sql_exception；统一转为友好错误页
            error("连接数据库服务器失败：" . $e->getMessage());
        }
        if (mysqli_connect_errno()) {
            error("连接数据库服务器失败：" . iconv('gbk', 'utf-8', mysqli_connect_error()));
        }
        $charset = Config::get('database.charset') ?: 'utf8';
        $conn->set_charset($charset); // 设置编码
        return $conn;
    }

    // 关闭自动提交，开启事务模式
    public function beginTransaction()
    {
        if (! $this->master) {
            $cfg = Config::get('database');
            $this->master = $this->conn($cfg);
            $this->master->query("SET sql_mode='NO_ENGINE_SUBSTITUTION'");
        }
        if (! $this->begin) {
            $this->master->autocommit(false);
            $this->begin = true;
        }
    }

    // 提交事务
    public function commitTransaction()
    {
        if ($this->begin) {
            $ok = $this->master->commit(); // 提交事务
            $this->master->autocommit(true); // 提交后恢复自动提交
            $this->begin = false; // 关闭事务模式
            // 非 failSoft：保持历史行为，不因 commit 失败改返回值（调用方历来不检查）；
            // 仅 failSoft 返回 false，供 VisitsCounter 等把增量退回本地待刷。
            if (! $ok && self::$failSoft) {
                self::$failSoftError = true;
                return false;
            }
        }
        return true;
    }

    // 执行SQL语句,接受完整SQL语句，返回结果集对象
    public function query($sql, $type = 'master', $params = array())
    {
        $time_s = microtime(true);

        // 确保master连接已初始化（bindParams需要使用master进行转义）
        if (! $this->master) {
            $cfg = Config::get('database');
            $this->master = $this->conn($cfg);
            $this->master->query("SET sql_mode='NO_ENGINE_SUBSTITUTION'"); // MySql写入规避严格模式
        }

        // 参数绑定统一处理（消除master/slave分支中的重复调用）
        if (!empty($params)) {
            $sql = $this->bindParams($sql, $params);
        }

        switch ($type) {
            case 'master':
                if (Config::get('database.transaction') && ! $this->begin) { // 根据配置开启mysql事务，注意需要是InnoDB引擎
                    $this->beginTransaction();
                }
                $result = $this->master->query($sql);
                if ($result === false) {
                    $this->error($sql, 'master');
                    return false;
                }
                break;
            case 'slave':
                if (! $this->slave) {
                    // 未设置从服务器时直接读取主数据库配置
                    if (! $cfg = Config::get('database.slave')) {
                        $cfg = Config::get('database');
                    } else {
                        // 随机选择从数据库
                        if (is_multi_array($cfg)) {
                            $count = count($cfg);
                            $cfg = $cfg['slave' . mt_rand(1, $count)];
                        }
                    }
                    $this->slave = $this->conn($cfg);
                }
                $result = $this->slave->query($sql);
                if ($result === false) {
                    $this->error($sql, 'slave');
                    return false;
                }
                break;
        }
        return $result;
    }

    // 数据是否存在模型，接受完整SQL语句，返回boolean数据
    public function isExist($sql)
    {
        $result = $this->query($sql, 'slave');
        if ($result->num_rows) {
            $result->free();
            return true;
        } else {
            return false;
        }
    }

    // 获取记录总量模型，接受数据库表名，返回int数据
    public function rows($table)
    {
        $sql = "SELECT count(*) FROM $table";
        $result = $this->query($sql, 'slave');
        if (! ! $row = $result->fetch_array(2)) {
            $result->free();
            return $row[0];
        } else {
            return 0;
        }
    }

    // 读取字段数量模型，接受数据库表名，返回int数据
    public function fields($table)
    {
        $sql = "SELECT * FROM $table LIMIT 1";
        $result = $this->query($sql, 'slave');
        if ($result) {
            return $result->field_count;
        } else {
            return 0;
        }
    }

    /**
     * 获取表信息,接受数据库表名，返回表字段信息数组
     *
     * @param $table 表名
     */
    public function tableFields($table)
    {
        $sql = "describe $table";
        $result = $this->query($sql, 'slave');
        $rows = array();
        if ($this->slave->affected_rows) {
            while (! ! $row = $result->fetch_object()) {
                $rows[] = $row->Field;
            }
            $result->free();
        }
        return $rows;
    }

    /**
     * 查询一条数据模型，接受完整SQL语句，有数据返回对象数组，否则空数组
     *
     * @$type 可以是MYSQLI_ASSOC ,MYSQLI_NUM ,MYSQLI_BOTH,不设置则返回对象数组
     */
    public function one($sql, $type = null, $params = array())
    {
        $result = $this->query($sql, 'slave', $params);
        $row = array();
        if ($this->slave->affected_rows) {
            if ($type) {
                $row = $result->fetch_array($type);
            } else {
                $row = $result->fetch_object();
            }
            $result->free();
        }
        return $row;
    }

    /**
     * 查询多条数据模型，接受完整SQL语句，有数据返回二维对象数组，否则空数组
     * @$type 可以是MYSQLI_ASSOC ,MYSQLI_NUM ,MYSQLI_BOTH,不设置则返回对象模式
     */
    public function all($sql, $type = null, $params = array())
    {
        $result = $this->query($sql, 'slave', $params);
        $rows = array();
        if ($this->slave->affected_rows) {
            if ($type) {
                while (! ! $array = $result->fetch_array($type)) { // 关联数组或数字数组或同时
                    $rows[] = $array;
                }
            } else {
                while (! ! $objects = $result->fetch_object()) {
                    $rows[] = $objects;
                }
            }
            $result->free();
        }
        return $rows;
    }

    // 数据增、删、改模型，接受完整SQL语句，返回影响的行数的int数据
    public function amd($sql, $params = array())
    {
        $result = $this->query($sql, 'master', $params);
        $num = $this->master->affected_rows;
        if ($num > 0) {
            return $num;
        } else {
            return 0;
        }
    }

    // 最近一次插入数据的自增字段值，返回int数据
    public function insertId()
    {
        return $this->master->insert_id;
    }

    // 执行多条SQL模型，成功返回true,否则false
    public function multi($sql)
    {
        $ok = $this->master->multi_query($sql);
        if ($ok === false) {
            $this->error($sql, 'master');
            return false;
        }
        // multi_query 成功时返回 true；UPDATE/INSERT 等无结果集，不能对 true 调 free()
        do {
            if ($res = $this->master->store_result()) {
                $res->free();
            }
        } while ($this->master->more_results() && $this->master->next_result());
        return true;
    }

    // 显示执行错误
    protected function error($sql, $conn)
    {
        $raw = mysqli_error($this->$conn);
        $err = '错误：' . $raw;
        if (preg_match('/XPATH/i', $err)) {
            $err = '';
        }
        if ($this->begin) { // 如果是事务模式，发生错误，则回滚
            $this->$conn->rollback();
            $this->begin = false;
        }
        if (self::$failSoft) {
            self::$failSoftError = true;
            return;
        }
        // 详情只入日志；非调试模式对外仅给通用文案，避免暴露表名/列名/约束名
        @error_log('[PbootCMS][mysqli] 执行SQL发生错误！错误：' . $raw . ' 语句：' . $sql);
        if (Config::get('debug')) {
            error('执行SQL发生错误！' . $err);
        } else {
            error('数据库执行错误，请稍后重试或联系管理员！');
        }
    }

    //返回对象结果集
    public function fetchQuery($obj){
        return $obj->fetch_all();
    }

    // 安全替换参数占位符
    private function bindParams($sql, $params)
    {
        $offset = 0;
        foreach ($params as $param) {
            $pos = strpos($sql, '?', $offset);
            if ($pos !== false) {
                if ($param === null) {
                    $replacement = 'NULL';
                } elseif (is_int($param) || is_float($param)) {
                    $replacement = $param;
                } else {
                    $replacement = "'" . $this->master->real_escape_string($param) . "'";
                }
                $sql = substr_replace($sql, $replacement, $pos, 1);
                $offset = $pos + strlen($replacement);
            }
        }
        return $sql;
    }
}

