<?php

namespace Api\PhpUtils\Mysql;

use Medoo\Medoo;
use Api\PhpUtils\Log\FileLog;

/**
 * MySQL基类,支持一主多从
 * 
 * 1. MySQL配置信息在yaconf的mysql.DATABASE_NAME, TABLE_NAME, CONFIG_INDEX 在子类中设置
 * 
 * 2. 数据库分库分表，需要发号器全局唯一id,请用MysqlClusterBase基类
 * 
 * 3. 提供常用的CURD方法，共他方法会调用 __callStatic()方法
 * 
 * 4. 返回值说明：
 *    return false  是校验参数错误，数据库执行错误, debug模式等异常的标识，错误信息请看日志，
 *                  使用error()方法可以获取最后的一条错误, 对错误进行了一次强制重试，
 *    return []     查询结果为空
 *    return number 插入为lasterInsertId:String 按需自己intval()，更新和删除返回影响条数
 * 
 * 5. 根据镜像中php的版本选择使用version1.7.10 版本的Medoo类，不要升级！
 * @see https://medoo.in  (英文网站)
 * @see https://medoo.lvtao.net (中文网站)
 */

/**
 * @method static MysqlBase insert($data)
 * @method static MysqlBase update($data, $where)
 * @method static MysqlBase delete($where)
 * @method static MysqlBase select($columns, $where)
 * @method static MysqlBase selectMaster($columns, $where)
 * @method static MysqlBase getMaster($columns, $where)
 * @method static MysqlBase get($columns, $where)
 * @method static MysqlBase count($where)
 * @method static MysqlBase action( $callback )
 */
abstract class MysqlBase
{
    const DATABASE_TYPE = 'mysql'; //数据库类型
    const TABLE_NAME = ''; //表名，必填
    const CONFIG_INDEX = ''; //yaconf中的index索引，必填
    const WRITE = 'write';
    const READ = 'read';
    const PRIMARY_KEY = 'id';

    /**
     * yaconf中的数据库配置项
     *
     * @var array
     */
    private static $iniConf = [];

    /**
     * 数据库配置
     *
     * @var array
     */
    private static $dbConf = [];

    /**
     * 数据库连接信息，分主从
     *
     * @var array
     */
    private static $dbConnect = [];

    /**
     * 数据库当前连接信息
     *
     * @var Medoo\Medoo
     */
    private static $dbCurrentConnect = null;

    /**
     * 数据库查询等的返回结果
     *
     * @var mixed
     */
    private static $sqlResut = [];

    /**
     * 数据库最后一个
     *
     * @var array
     */
    private static $errorInfo = [];

    /**
     * 写操作函数
     *
     * @var array
     */
    protected static $writeFuncitonList = [
        "insert",
        "update",
        "delete",
        "selectMaster", // 或使用/*master*/强制路由，对主从实时性要求较高的场景使用，分布式系统尽量用消息代替查主库等其他方案
        "getMaster",
    ];

    /**
     * 读操作函数
     *
     * @var array
     */
    protected static $readFunctionList = [
        "select",
        "get",
        "count",
    ];

    public static function __callStatic($method, $arguments)
    {
        if (!self::validation($method, $arguments)) {
            return false;
        }
        if (in_array($method, self::$writeFuncitonList)) {
            // 写操作
            $type = static::WRITE;
            self::$dbConnect[$type] = self::getConnection($type);
            if (strpos($method, 'Master') !== false) {
                $method = substr_replace($method, '', -6);
            }
            array_unshift($arguments, static::TABLE_NAME);
        } elseif (in_array($method, self::$readFunctionList)) {
            // 读操作
            $type = static::READ;
            self::$dbConnect[$type] = self::getConnection($type);
            array_unshift($arguments, static::TABLE_NAME);
        } elseif (strpos($method, 'query') !== false) {
            // Medoo原生SQL操作
            $type = static::READ;
            if (strpos($method, 'Master') !== false) {
                $type = static::WRITE;
            }
            $method = 'query';
            self::$dbConnect[$type] = self::getConnection($type);
        } else {
            throw new \Exception("use undefined Medoo function:" . $method . "-" . json_encode($arguments));
        }

        self::$dbCurrentConnect = self::$dbConnect[$type];
        self::$sqlResut = call_user_func_array([self::$dbConnect[$type], $method], $arguments);
        if (!self::catchError($method, $arguments)) {
            // 如果失败重试一次
            self::$dbCurrentConnect = self::$dbConnect[$type] = self::getConnection($type, true);
            self::$sqlResut = call_user_func_array([self::$dbConnect[$type], $method], $arguments);
            if (!self::catchError($method, $arguments)) {
                return false;
            }
        }
        return self::formatResult($method, $arguments);
    }

    /**
     * 获取数据库连接
     *
     * @param [string] $type 读或写
     * @param boolean $flush 是否强制重新连接，重试等场景使用
     * @return void
     */
    public static function getConnection($type, $flush = false)
    {
        if (!isset(self::$dbConnect[$type]) || $flush) {
            self::$dbConnect[$type] = new Medoo(self::getDbConf($type));
        }

        return self::$dbConnect[$type];
    }

    /**
     * 获取数据库配置
     *
     * @param string $type 读或写
     * @return array
     */
    public static function getDbConf($type)
    {
        self::$iniConf = config('mysql', static::CONFIG_INDEX);
        if (!self::$iniConf) {
            throw new \Exception('mysql yaconf ini config not exist');
        }

        list($server, $port) = self::getDbServerPort($type);
        $options = self::getDbOptions();

        self::$dbConf[$type] = [
            'database_type' => static::DATABASE_TYPE ? static::DATABASE_TYPE : self::$iniConf['database_type'],
            'database_name' => self::$iniConf['database_name'],
            'server'        => $server,
            'port'          => $port,
            'username'      => self::$iniConf['username'],
            'password'      => self::$iniConf['password'],
            'charset'       => self::$iniConf['charset'],
            'option'        => array_replace([
                \PDO::ATTR_STRINGIFY_FETCHES    => false,
                \PDO::ATTR_EMULATE_PREPARES     => false,
                \PDO::ATTR_CASE                 => \PDO::CASE_NATURAL,
                \PDO::ATTR_TIMEOUT              => intval(self::$iniConf['connect_timeout'] ?? 10)
            ], $options)
        ];
        return self::$dbConf[$type];
    }

    /**
     * 获取数据库server和port
     *
     * @param [string] $type 读或写类型
     * @return void
     */
    public static function getDbServerPort($type)
    {
        $server = '';
        $port = 0;
        $host = self::$iniConf[$type]['host'];
        // 处理分为的逻辑
        if ($type === static::WRITE) {
            list($server, $port) = explode(':', $host);
        } elseif ($type === static::READ) {
            if (strpos($host, ',') === false) {
                list($server, $port) = explode(':', $host);
            } else {
                $list = explode(',', $host);
                $rand = array_rand($list);
                list($server, $port) = explode(':', $list[$rand]);
            }
        }
        return [$server, $port];
    }

    /**
     * 获取ini中的option配置，必须用\PDO::ATTR_STRINGIFY_FETCHES对应的int数值
     *
     * @return array
     */
    public static function getDbOptions()
    {
        $options = [];
        if (isset(self::$iniConf['options'])) {
            foreach (self::$iniConf['options'] as $item) {
                $itemArr = explode('=>', $item);
                $options[trim($itemArr[0])] = $itemArr[1];
            }
        }
        return $options;
    }

    /**
     * 执行SQL前，先做基础的验证
     */
    private static function validation($method, &$arguments)
    {
        switch (strtolower($method)) {
            case 'update':
                // 更新操作前，如果没有where条件
                if (!isset($arguments[1]) || empty($arguments[1])) {
                    self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
                    FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
                    return false;
                }
                break;

            case 'delete':
                // 删除操作前，如果没有where条件
                if (!isset($arguments[0]) || empty($arguments[0])) {
                    self::$errorInfo = ['validation', 0, "sql exception write without where:" . $method . "-" . json_encode($arguments)];
                    FileLog::error("sql exception write without where:" . $method . "-" . json_encode($arguments));
                    return false;
                }
                // 如果必须要物理删除数据，只允计按主键或唯一键单条删除
                if (!isset($arguments[0][static::PRIMARY_KEY])) {
                    self::$errorInfo = ['validation', 0, "sql exception write error primary key:" . $method . "-" . json_encode($arguments)];
                    FileLog::error("sql exception write error primary key:" . $method . "-" . json_encode($arguments));
                    return false;
                }
                break;

            case 'select':
            case 'selectmaster':
            case 'get':
            case 'getmaster':
                // 不允许用join操作
                if (count($arguments) > 2) {
                    self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
                    FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
                    return false;
                }
                // 读取没有limit的强制加limit
                if (!isset($arguments[1]['LIMIT'])) {
                    $arguments[1]['LIMIT'] = 5000;
                    return true;
                }
                break;

            case 'count':
                // 不允许用join操作
                if (count($arguments) > 1) {
                    self::$errorInfo = ['validation', 0, "sql exception use join:" . $method . "-" . json_encode($arguments)];
                    FileLog::error("sql exception use join:" . $method . "-" . json_encode($arguments));
                    return false;
                }
                break;

            default:
                break;
        }

        return true;
    }

    /**
     * 捕获错误
     *
     * @return bool
     */
    private static function catchError($method, $arguments)
    {
        self::$errorInfo = call_user_func_array([self::$dbCurrentConnect, 'error'], []);
        // debug 模式返回值为NULL
        if (is_null(self::$errorInfo)) {
            return true;
        }

        list($sqlState, $errorCode, $errorMessage) = self::$errorInfo;
        // PDO::errorInfo() the SQLSTATE error code for '00000' (success) and '01000' (success with warning).
        if ($sqlState == '00000') {
            return true;
        }

        FileLog::error("mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . print_r([$sqlState, $errorCode, $errorMessage], 1));

        //throw Exception
        // $msg = "mysql error catch:" . $method . "-" . json_encode($arguments) . '-' . json_encode([$sqlState, $errorCode, $errorMessage]);
        // throw new \Exception($msg);
        return false;
    }

    /**
     * 格式化返回结果
     *
     * @return mixed
     */
    private static function formatResult($method, $arguments)
    {
        // debug 模式errorInfo返回值为NULL
        if (is_null(self::$errorInfo)) {
            return self::$sqlResut;
        }

        // get未查询到结果返回NULL,select返回空数组[],统一转成空数组[]
        if (is_null(self::$sqlResut)) {
            return [];
        }

        switch (strtolower($method)) {
            case 'insert':
                // 多条数据插入，返回影响的行数
                if (isset($arguments[1][1])) {
                    // PDOStatement::rowCount() 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
                    return self::$sqlResut->rowCount();
                }
                // 单条插入返回最后插入行的ID
                return call_user_func_array([self::$dbCurrentConnect, 'id'], []);
                break;

            case 'update':
            case 'delete':
                // 返回上一个由对应的 PDOStatement 对象执行DELETE、 INSERT、或 UPDATE 语句受影响的行数。
                return self::$sqlResut->rowCount();
                break;

            case 'select':
                // 返回数组
                return self::$sqlResut;
                break;

            default:
                return self::$sqlResut;
                break;
        }
    }

    /**
     * 获得最后一个执行的错误
     *
     * @return array
     */
    public static function error()
    {
        return self::$errorInfo;
    }

    /**
     * 开启Medoo调试模式,只输出执行的SQL语句，但不执行，在最开始调用
     *
     * @return array
     */
    public static function debug()
    {
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        self::$dbConnect[$type]->debug();
        $type = static::READ;
        self::$dbConnect[$type] = self::getConnection($type);
        self::$dbConnect[$type]->debug();

        return [static::WRITE => self::$dbConnect[static::WRITE], static::READ => self::$dbConnect[static::READ]];
    }

    /**
     * 返回所有执行的查询
     *
     * @return array
     */
    public static function log()
    {
        $ret = [];
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        $ret[$type] = self::$dbConnect[$type]->log();
        $type = static::READ;
        self::$dbConnect[$type] = self::getConnection($type);
        $ret[$type] = self::$dbConnect[$type]->log();

        return $ret;
    }

    /**
     * 返回最后一条执行的SQL语句
     * 
     * @return array
     */
    public static function last()
    {
        return self::$dbCurrentConnect->last();
    }

    /**
     * 事务操作
     *
     * @return bool
     */
    public static function beginTransaction()
    {
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        if(!self::$dbConnect[$type]->pdo->inTransaction()) {
            return self::$dbConnect[$type]->pdo->beginTransaction();
        }
        return false;
    }

    /**
     * 事务提交
     *
     * @return bool
     */
    public static function commit()
    {
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        if(self::$dbConnect[$type]->pdo->inTransaction()) {
            return self::$dbConnect[$type]->pdo->commit();
        }
        return false;
    }

    /**
     * 事务回滚
     *
     * @return bool
     */
    public static function rollback()
    {
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        if(self::$dbConnect[$type]->pdo->inTransaction()) {
            return self::$dbConnect[$type]->pdo->rollback();
        }
        return false;
    }

    /**
     * 事务操作，Medoo提供的
     *
     * @return mixed
     */
    public static function action(callable $actions)
    {
        $type = static::WRITE;
        self::$dbConnect[$type] = self::getConnection($type);
        return self::$dbConnect[$type]->action($actions);
    }
}
