"""
MySQL数据库客户端模块

提供MySQL数据库操作的封装，支持：
- 连接池管理
- 线程安全操作
- 批量插入/替换/删除
- 分页查询
- 自动重连机制
- 参数化查询（防SQL注入）

依赖：
    pip install pymysql DBUtils mysql-connector-python
"""
import logging
import threading
from typing import Optional, List, Dict, Any, Type, Tuple, Union

import pymysql
import mysql.connector
import mysql.connector.errorcode
from dbutils.pooled_db import PooledDB

from xsdk.xutil import Xutil
from xsdk.bean_util import BaseBean, BeanUtil
from xsdk.config_reader import ConfigReader

logger = logging.getLogger(__name__)

# 参数类型定义
Params = Optional[Union[Tuple, List, Dict]]


class DBError(Exception):
    """
    数据库操作异常类

    Attributes:
        code: 错误码
        message: 错误消息
    """

    def __init__(self, code: int = -100, message: str = "DB_ERROR"):
        super().__init__(message)
        self.code = code
        self.message = message

    def __str__(self) -> str:
        return str({"code": self.code, "message": self.message})


class DBResult:
    """
    数据库操作结果类

    Attributes:
        code: 状态码（0成功，-1失败）
        message: 状态消息
        data: 查询结果数据
        row_count: 影响行数
        page_info: 分页信息
    """

    CODE_SUCCESS = 0
    CODE_FAILED = -1
    MESSAGE_SUCCESS = "SUCCESS"
    MESSAGE_FAILED = "FAILED"

    def __init__(
        self,
        code: int = CODE_SUCCESS,
        message: str = MESSAGE_SUCCESS,
        data: Optional[List[Dict]] = None,
        row_count: int = 0
    ):
        self.code = code
        self.message = message
        self.data = data if data is not None else []
        self.row_count = row_count
        self.page_info: Optional["PageUtil"] = None

    def __str__(self) -> str:
        return str(self.__dict__)

    def is_failed(self) -> bool:
        """检查操作是否失败"""
        return self.code == DBResult.CODE_FAILED

    def is_success(self) -> bool:
        """检查操作是否成功"""
        return self.code == DBResult.CODE_SUCCESS

    @staticmethod
    def fail(msg: Optional[str] = None) -> "DBResult":
        """创建失败结果"""
        if Xutil.is_empty(msg):
            msg = DBResult.MESSAGE_FAILED
        return DBResult(DBResult.CODE_FAILED, msg)

    @staticmethod
    def success() -> "DBResult":
        """创建成功结果"""
        return DBResult(DBResult.CODE_SUCCESS, DBResult.MESSAGE_SUCCESS)


class PageUtil:
    """
    分页工具类

    Attributes:
        total_pages: 总页数
        total_records: 总记录数
        page_number: 当前页码（从1开始）
        page_size: 每页记录数
        has_prev: 是否有上一页
        has_next: 是否有下一页
        prev_page: 上一页页码
        next_page: 下一页页码
    """

    def __init__(self, total_records: int, page_number: int = 1, page_size: int = 20):
        self.total_pages = 0
        self.total_records = total_records
        self.page_number = page_number
        self.page_size = page_size
        self.has_prev = False
        self.has_next = False
        self.prev_page = 0
        self.next_page = 0
        self._reset()

    def __str__(self) -> str:
        return str(self.__dict__)

    def _reset(self) -> None:
        """重新计算分页信息"""
        # 参数校验
        if self.total_records is None or int(self.total_records) < 0:
            self.total_records = 0
        if self.page_number is None or int(self.page_number) < 1:
            self.page_number = 1
        if self.page_size is None or int(self.page_size) <= 0:
            self.page_size = 20

        self.total_records = int(self.total_records)
        self.page_number = int(self.page_number)
        self.page_size = int(self.page_size)

        # 计算总页数
        if self.total_records % self.page_size == 0:
            self.total_pages = self.total_records // self.page_size
        else:
            self.total_pages = self.total_records // self.page_size + 1

        # 计算上一页
        if self.page_number > 1:
            self.has_prev = True
            self.prev_page = self.page_number - 1
        else:
            self.has_prev = False
            self.prev_page = self.page_number

        # 计算下一页
        if self.page_number < self.total_pages:
            self.has_next = True
            self.next_page = self.page_number + 1
        else:
            self.has_next = False
            self.next_page = self.page_number

    @staticmethod
    def get_total(db_instance: "MySQLClient", sql: str, params: Params = None) -> int:
        """
        获取SQL查询的总记录数

        Args:
            db_instance: MySQLClient实例
            sql: 原始查询SQL
            params: SQL参数（用于参数化查询）

        Returns:
            总记录数
        """
        sql_total = f"SELECT COUNT(*) AS total FROM ({sql}) t"
        db_ret = db_instance.query(sql_total, params=params)

        total = 0
        if db_ret and len(db_ret.data) == 1:
            total = int(db_ret.data[0]["total"])
        return total


class MySQLConfig:
    """
    MySQL连接配置类

    Attributes:
        host: 数据库主机
        port: 数据库端口
        user: 用户名
        password: 密码
        timeout: 连接超时时间
        charset: 字符集
    """

    def __init__(
        self,
        host: str = "127.0.0.1",
        port: int = 3306,
        user: str = "root",
        password: str = "",
        timeout: int = 5000,
        charset: str = "utf8"
    ):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.timeout = timeout
        self.charset = charset
        self.use_unicode = True
        self.use_pure = False

    @staticmethod
    def get_config(conf_file: str, conf_section: str) -> "MySQLConfig":
        """
        从配置文件读取MySQL配置

        Args:
            conf_file: 配置文件路径
            conf_section: 配置节名称

        Returns:
            MySQLConfig实例
        """
        config_reader = ConfigReader.get_instance(conf_file)
        host = config_reader.get(conf_section, "host")
        port = config_reader.get_int(conf_section, "port")
        user = config_reader.get(conf_section, "user")
        password = config_reader.get(conf_section, "password")
        timeout = config_reader.get_int(conf_section, "timeout")
        charset = config_reader.get(conf_section, "charset") or "utf8"

        return MySQLConfig(host, port, user, password, timeout, charset)

    @staticmethod
    def parse_dict(data_dict: Dict[str, Any]) -> "MySQLConfig":
        """
        从字典解析MySQL配置

        Args:
            data_dict: 配置字典

        Returns:
            MySQLConfig实例
        """
        db_config = MySQLConfig()
        db_config.__dict__ = data_dict
        return db_config


class MySQLClient:
    """
    MySQL客户端封装类

    提供线程安全的数据库操作，支持连接池、自动重连和参数化查询。

    支持两种调用方式：
        # 方式1 - 传统字符串拼接（兼容旧代码）
        >>> result = client.query(f"SELECT * FROM users WHERE id = '{user_id}'")

        # 方式2 - 参数化查询（推荐，防SQL注入）
        >>> result = client.query("SELECT * FROM users WHERE id = %s", params=(user_id,))
    """

    def __init__(self, mysql_config: MySQLConfig, logger: logging.Logger = logger):
        self.logger = logger
        self.host = mysql_config.host
        self.port = mysql_config.port
        self.user = mysql_config.user
        self.password = mysql_config.password
        self.timeout = mysql_config.timeout
        self.charset = mysql_config.charset
        self.use_unicode = True
        self.use_pure = None
        self.pool: Optional[PooledDB] = None
        self.local_thread_data = threading.local()
        self._init()

    def __str__(self) -> str:
        return str(self.__dict__)

    def _init(self) -> None:
        """初始化连接池（仅创建池，不预取连接，避免主线程占用连接槽位）"""
        try:
            min_cached = 4
            max_cached = 8
            max_shared = 0
            max_connections = 64
            max_usage = 8
            set_session = None
            blocking = True
            reset = True

            self.pool = PooledDB(
                pymysql,
                min_cached,
                max_cached,
                max_shared,
                max_connections,
                blocking,
                max_usage,
                set_session,
                reset,
                host=self.host,
                port=self.port,
                user=self.user,
                passwd=self.password,
                charset=self.charset,
                cursorclass=pymysql.cursors.DictCursor
            )
        except Exception as error:
            raise DBError(message=str(error))

    def _ensure_connection(self) -> None:
        """
        确保当前线程持有可用的数据库连接和游标

        处理三种场景：
        1. 当前线程首次访问，无连接 → 从连接池获取新连接
        2. 已有连接，ping 存活检测通过 → 复用现有连接
        3. 已有连接，但 ping 失败（连接已断开）→ 关闭旧连接，从连接池获取新连接
        """
        thread_id = str(threading.current_thread().ident)
        self.local_thread_data.thread_id = thread_id

        try:
            conn = getattr(self.local_thread_data, "connection", None)

            if conn is None:
                # 场景1：当前线程无连接，从连接池获取
                self.local_thread_data.connection = self.pool.connection()
                self.local_thread_data.cursor = self.local_thread_data.connection.cursor()
                self.logger.info(
                    f"reconnect.001: new connection | thread={thread_id} | "
                    f"conn={hex(id(self.local_thread_data.connection))}",
                    stacklevel=3
                )
                return

            # 场景2/3：已有连接，检测存活
            try:
                conn.ping(reconnect=True)
                self.logger.info(
                    f"reconnect.002: ping ok | thread={thread_id} | "
                    f"conn={hex(id(conn))}",
                    stacklevel=3
                )
            except Exception:
                # 场景3：ping 失败，关闭旧连接后重新获取
                self.logger.warning(
                    f"reconnect.003: ping failed, replacing connection | thread={thread_id}",
                    stacklevel=3
                )
                try:
                    conn.close()
                except Exception:
                    pass
                self.local_thread_data.connection = self.pool.connection()

            # 每次都创建新 cursor（cursor 是轻量对象，确保状态干净）
            self.local_thread_data.cursor = self.local_thread_data.connection.cursor()

        except Exception as error:
            self.logger.error(f"Failed to get db connection | thread={thread_id}", stacklevel=3)
            self.logger.exception(error, stacklevel=3)
            raise error

    def reconnect(self) -> None:
        """重新建立数据库连接（兼容旧接口，内部委托给 _ensure_connection）"""
        self._ensure_connection()

    def get_connection(self):
        """获取数据库连接"""
        self.reconnect()
        return self.local_thread_data.connection

    @staticmethod
    def get_instance(
        conf_file: str,
        conf_section: str = "db",
        logger: logging.Logger = logger
    ) -> "MySQLClient":
        """
        从配置文件创建MySQLClient实例

        Args:
            conf_file: 配置文件路径
            conf_section: 配置节名称
            logger: 日志记录器

        Returns:
            MySQLClient实例
        """
        db_config = MySQLConfig.get_config(conf_file, conf_section)
        return MySQLClient(db_config, logger)

    @staticmethod
    def _is_connection_error(error: Exception) -> bool:
        """判断是否为连接断开类异常（可通过重连恢复）"""
        if isinstance(error, pymysql.err.OperationalError):
            # CR_SERVER_GONE_ERROR(2006), CR_SERVER_LOST(2013)
            err_code = error.args[0] if error.args else 0
            return err_code in (2006, 2013)
        if isinstance(error, pymysql.err.InterfaceError):
            return True
        err_msg = str(error).lower()
        return any(keyword in err_msg for keyword in (
            "server has gone away",
            "lost connection",
            "broken pipe",
            "connection reset",
            "not connected",
        ))

    def _force_new_connection(self) -> None:
        """强制丢弃当前连接，从连接池获取新连接和游标"""
        thread_id = getattr(self.local_thread_data, "thread_id", "unknown")
        old_conn = getattr(self.local_thread_data, "connection", None)
        if old_conn is not None:
            try:
                old_conn.close()
            except Exception:
                pass
        self.local_thread_data.connection = self.pool.connection()
        self.local_thread_data.cursor = self.local_thread_data.connection.cursor()
        self.logger.warning(
            f"reconnect.retry: forced new connection | thread={thread_id} | "
            f"conn={hex(id(self.local_thread_data.connection))}",
            stacklevel=4
        )

    def _execute(
        self,
        sql: str,
        params: Params = None,
        need_fetch: bool = False,
        auto_commit: bool = False
    ) -> DBResult:
        """
        执行SQL语句（内部方法）

        连接断开时自动重连重试一次。

        Args:
            sql: SQL语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应
            need_fetch: 是否需要获取结果
            auto_commit: 是否自动提交

        Returns:
            DBResult实例
        """
        self.local_thread_data.db_ret = DBResult()
        last_error = None

        for attempt in range(2):  # 最多重试 1 次
            try:
                if attempt == 0:
                    self._ensure_connection()
                else:
                    # 第二次尝试：强制获取新连接
                    self.logger.warning(
                        f"Retrying after connection error: {last_error}",
                        stacklevel=3
                    )
                    self._force_new_connection()

                if params is not None:
                    self.local_thread_data.cursor.execute(sql, params)
                else:
                    self.local_thread_data.cursor.execute(sql)
                if auto_commit:
                    self.local_thread_data.connection.commit()
                if need_fetch:
                    self.local_thread_data.db_ret = DBResult(data=self.local_thread_data.cursor.fetchall())
                self.local_thread_data.db_ret.row_count = self.local_thread_data.cursor.rowcount
                self.logger.info(f"Affected row count: {self.local_thread_data.db_ret.row_count}", stacklevel=3)
                return self.local_thread_data.db_ret

            except Exception as error:
                # 连接断开类错误且是首次尝试 → 重试
                if attempt == 0 and self._is_connection_error(error):
                    last_error = error
                    continue
                # 非连接错误或重试仍失败 → rollback 后抛出
                try:
                    self.local_thread_data.connection.rollback()
                except Exception:
                    pass
                raise DBError(message=str(error))

        # 理论上不会走到这里
        raise DBError(message=str(last_error))

    def _execute_many(
        self,
        sql: str,
        params_list: List[Tuple],
        auto_commit: bool = True
    ) -> DBResult:
        """
        批量执行参数化SQL（内部方法，用于 batch_insert / batch_replace）

        连接断开时自动重连重试一次。
        PyMySQL 的 executemany 会将 INSERT/REPLACE 自动优化为多行 VALUES 语法。

        Args:
            sql: SQL模板，如 INSERT INTO t(a,b) VALUES (%s,%s)
            params_list: 参数元组列表
            auto_commit: 是否自动提交

        Returns:
            DBResult实例
        """
        self.local_thread_data.db_ret = DBResult()
        last_error = None

        for attempt in range(2):
            try:
                if attempt == 0:
                    self._ensure_connection()
                else:
                    self.logger.warning(
                        f"Retrying executemany after connection error: {last_error}",
                        stacklevel=3
                    )
                    self._force_new_connection()

                self.local_thread_data.cursor.executemany(sql, params_list)
                if auto_commit:
                    self.local_thread_data.connection.commit()
                self.local_thread_data.db_ret.row_count = self.local_thread_data.cursor.rowcount
                self.logger.info(f"Affected row count: {self.local_thread_data.db_ret.row_count}", stacklevel=3)
                return self.local_thread_data.db_ret

            except Exception as error:
                if attempt == 0 and self._is_connection_error(error):
                    last_error = error
                    continue
                try:
                    self.local_thread_data.connection.rollback()
                except Exception:
                    pass
                raise DBError(message=str(error))

        raise DBError(message=str(last_error))

    def query(
        self,
        sql: str,
        params: Params = None,
        cls: Optional[Type[BaseBean]] = None,
        auto_commit: bool = False
    ) -> DBResult:
        """
        执行查询SQL

        Args:
            sql: SELECT语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应
            cls: 结果映射的Bean类（需继承BaseBean）
            auto_commit: 是否自动提交

        Returns:
            DBResult实例

        Raises:
            DBError: 非SELECT语句时抛出

        Example:
            # 传统方式（兼容）
            >>> db.query(f"SELECT * FROM users WHERE id = '{uid}'")

            # 参数化查询（推荐）
            >>> db.query("SELECT * FROM users WHERE id = %s", params=(uid,))
            >>> db.query("SELECT * FROM users WHERE name = %s AND age > %s", params=(name, 18), cls=User)
        """
        log_msg = Xutil.simple_str(sql)
        if params is not None:
            log_msg += f" | params: {str(params)[:512]}"
        self.logger.info(log_msg, stacklevel=3)

        # SQL 校验
        if not sql.strip().lower().startswith(("select", "desc", "show")):
            raise DBError(message=f"not select|desc|show sql! {sql}")

        # 中文编码处理（仅对无参数的拼接SQL生效）
        if params is None and Xutil.contains_chinese(sql) and self.charset.lower() == "latin1":
            sql = sql.encode("gbk", "ignore")

        db_ret = self._execute(sql, params=params, need_fetch=True, auto_commit=auto_commit)

        # 结果映射到Bean
        if cls and issubclass(cls, BaseBean):
            if db_ret and db_ret.data:
                bean_list = BeanUtil.parse2bean_list(db_ret.data, cls)
                db_ret.data = bean_list

        return db_ret

    def query_page(
        self,
        sql: str,
        page_number: int,
        page_size: int,
        params: Params = None,
        cls: Optional[Type[BaseBean]] = None
    ) -> Optional[DBResult]:
        """
        分页查询

        Args:
            sql: SELECT语句（可包含 %s 占位符）
            page_number: 页码（从1开始）
            page_size: 每页记录数
            params: SQL参数，与 %s 占位符对应
            cls: 结果映射的Bean类

        Returns:
            包含分页信息的DBResult，无数据时返回None

        Example:
            # 传统方式
            >>> db.query_page(f"SELECT * FROM users WHERE age > {min_age}", 1, 20)

            # 参数化查询
            >>> db.query_page("SELECT * FROM users WHERE age > %s", 1, 20, params=(min_age,))
        """
        sql = str(sql).strip()
        if sql.endswith(";"):
            sql = sql[:-1]

        total_records = PageUtil.get_total(self, sql, params=params)
        if total_records == 0:
            return None

        page_info = PageUtil(total_records, page_number, page_size)
        idx_limit = (page_info.page_number - 1) * page_info.page_size
        sql_page = f"{sql} LIMIT {idx_limit}, {page_info.page_size}"

        db_ret = self.query(sql_page, params=params, cls=cls)
        db_ret.page_info = page_info
        return db_ret

    def execute(self, sql: str, params: Params = None, auto_commit: bool = True) -> DBResult:
        """
        执行SQL语句

        Args:
            sql: SQL语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应
            auto_commit: 是否自动提交

        Returns:
            DBResult实例

        Example:
            # 传统方式
            >>> db.execute(f"DELETE FROM users WHERE id = '{uid}'")

            # 参数化查询
            >>> db.execute("DELETE FROM users WHERE id = %s", params=(uid,))
        """
        simple_sql = Xutil.simple_str(sql)
        if len(simple_sql) > 8192:
            simple_sql = f"{simple_sql[:8192]} ..."
        if params is not None:
            simple_sql += f" | params: {str(params)[:512]}"
        self.logger.info(simple_sql, stacklevel=3)

        # 中文编码处理（仅对无参数的拼接SQL生效）
        if params is None and Xutil.contains_chinese(sql) and self.charset.lower() == "latin1":
            sql = sql.encode("gbk", "ignore")

        return self._execute(sql, params=params, need_fetch=True, auto_commit=auto_commit)

    def update(self, sql: str, params: Params = None, auto_commit: bool = True) -> DBResult:
        """
        执行更新SQL

        Args:
            sql: 更新SQL语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应
            auto_commit: 是否自动提交

        Returns:
            DBResult实例

        Example:
            # 传统方式
            >>> db.update(f"UPDATE users SET name = '{name}' WHERE id = '{uid}'")

            # 参数化查询
            >>> db.update("UPDATE users SET name = %s WHERE id = %s", params=(name, uid))
        """
        simple_sql = Xutil.simple_str(sql)
        if len(simple_sql) > 8192:
            simple_sql = f"{simple_sql[:8192]} ..."
        if params is not None:
            simple_sql += f" | params: {str(params)[:512]}"
        self.logger.info(simple_sql, stacklevel=3)

        # SQL 校验
        _ALLOWED_UPDATE_PREFIXES = ("delete", "insert", "replace", "truncate", "update", "alter", "set", "create")
        sql_lower = sql.strip().lower()
        if not sql_lower.startswith(_ALLOWED_UPDATE_PREFIXES):
            raise DBError(message=f"not delete|insert|update|replace|truncate|alter|set|create sql! {sql}")

        # 中文编码处理（仅对无参数的拼接SQL生效）
        if params is None and Xutil.contains_chinese(sql) and self.charset.lower() == "latin1":
            sql = sql.encode("gbk", "ignore")

        return self._execute(sql, params=params, need_fetch=True, auto_commit=auto_commit)

    def update_object(
        self,
        data_dict: Dict[str, Any],
        db_name: str,
        tb_name: str,
        key_fields: List[str],
        auto_commit: bool = True
    ) -> None:
        """
        更新对象数据（内部使用参数化查询）

        Args:
            data_dict: 数据字典或对象
            db_name: 数据库名
            tb_name: 表名
            key_fields: 主键字段列表（只用于定位，不更新）
            auto_commit: 是否自动提交
        """
        if not key_fields:
            self.logger.info("No key fields specified", stacklevel=3)
            return
        if not data_dict:
            self.logger.info("No data to update", stacklevel=3)
            return

        if not isinstance(data_dict, dict):
            data_dict = data_dict.__dict__

        # 构建SET子句（参数化）
        set_parts = []
        set_values = []
        for key, value in data_dict.items():
            if key not in key_fields:
                set_parts.append(f"{key} = %s")
                set_values.append(value)

        # 构建WHERE子句（参数化）
        where_parts = ["1=1"]
        where_values = []
        for key, value in data_dict.items():
            if key in key_fields:
                where_parts.append(f"{key} = %s")
                where_values.append(value)

        sql = f"UPDATE {db_name}.{tb_name} SET {', '.join(set_parts)} WHERE {' AND '.join(where_parts)}"
        params = tuple(set_values + where_values)
        self.update(sql, params=params, auto_commit=auto_commit)

    def batch_insert(
        self,
        data_list: List[Any],
        db_name: str,
        tb_name: str,
        batch_size: int = 3000,
        auto_keys: List[str] = None
    ) -> None:
        """
        批量插入数据（内部使用参数化查询）

        Args:
            data_list: 字典列表或对象列表
            db_name: 数据库名
            tb_name: 表名
            batch_size: 每批插入的记录数
            auto_keys: 自增字段列表，默认["id", "fid"]
        """
        if auto_keys is None:
            auto_keys = ["id", "fid"]

        if not data_list:
            self.logger.info(f"[batch_insert] No data to insert: {db_name}.{tb_name}", stacklevel=3)
            return

        self.logger.info(f"[batch_insert] Data size: {len(data_list)}", stacklevel=3)

        # 获取字段列表
        first = data_list[0]
        if not isinstance(first, dict):
            first = first.__dict__

        field_list = [key for key in first.keys() if key.lower() not in auto_keys]
        field_str = ", ".join(field_list)
        placeholders = ", ".join(["%s"] * len(field_list))
        sql = f"INSERT INTO {db_name}.{tb_name}({field_str}) VALUES ({placeholders})"

        self.logger.info(f"[batch_insert] SQL template: {sql}", stacklevel=3)

        data_size = len(data_list)
        if batch_size <= 0:
            batch_size = 3000

        for batch_start in range(0, data_size, batch_size):
            batch_end = min(batch_start + batch_size, data_size)
            batch = data_list[batch_start:batch_end]

            params_list = []
            for data_dict in batch:
                if not isinstance(data_dict, dict):
                    data_dict = data_dict.__dict__
                values = tuple(data_dict[key] for key in field_list)
                params_list.append(values)

            self._execute_many(sql, params_list)

    def foreach_insert(
        self,
        data_list: List[Any],
        db_name: str,
        tb_name: str,
        ignore_error: bool = False,
        auto_keys: List[str] = None
    ) -> None:
        """
        逐条插入数据（内部使用参数化查询，支持异常忽略）

        Args:
            data_list: 字典列表或对象列表
            db_name: 数据库名
            tb_name: 表名
            ignore_error: 是否忽略插入错误
            auto_keys: 自增字段列表
        """
        if auto_keys is None:
            auto_keys = ["id", "fid"]

        if not data_list:
            self.logger.info(f"[foreach_insert] No data to insert: {db_name}.{tb_name}", stacklevel=3)
            return

        self.logger.info(f"[foreach_insert] Data size: {len(data_list)}", stacklevel=3)

        if len(data_list) >= 50000:
            self.logger.warning(
                f"[foreach_insert] Too much data for this method: {len(data_list)}",
                stacklevel=3
            )

        # 获取字段列表
        first = data_list[0]
        if not isinstance(first, dict):
            first = first.__dict__

        field_list = [key for key in first.keys() if key.lower() not in auto_keys]
        field_str = ", ".join(field_list)
        placeholders = ", ".join(["%s"] * len(field_list))
        sql = f"INSERT INTO {db_name}.{tb_name}({field_str}) VALUES ({placeholders})"

        for data_dict in data_list:
            if not isinstance(data_dict, dict):
                data_dict = data_dict.__dict__

            params = tuple(data_dict[key] for key in field_list)

            try:
                self.update(sql, params=params)
            except Exception as error:
                self.logger.error(f"[foreach_insert] Failed to insert: {error}", stacklevel=3)
                if not ignore_error:
                    raise error

    def batch_replace(
        self,
        data_list: List[Any],
        db_name: str,
        tb_name: str,
        batch_size: int = 3000,
        auto_keys: List[str] = None
    ) -> None:
        """
        批量替换数据（REPLACE INTO，内部使用参数化查询）

        Args:
            data_list: 字典列表或对象列表
            db_name: 数据库名
            tb_name: 表名
            batch_size: 每批记录数
            auto_keys: 自增字段列表
        """
        if auto_keys is None:
            auto_keys = ["id", "fid"]

        if not data_list:
            self.logger.info(f"[batch_replace] No data to replace: {db_name}.{tb_name}", stacklevel=3)
            return

        self.logger.info(f"[batch_replace] Data size: {len(data_list)}", stacklevel=3)

        # 获取字段列表
        first = data_list[0]
        if not isinstance(first, dict):
            first = first.__dict__

        field_list = [key for key in first.keys() if key.lower() not in auto_keys]
        field_str = ", ".join(field_list)
        placeholders = ", ".join(["%s"] * len(field_list))
        sql = f"REPLACE INTO {db_name}.{tb_name}({field_str}) VALUES ({placeholders})"

        self.logger.info(f"[batch_replace] SQL template: {sql}", stacklevel=3)

        data_size = len(data_list)
        if batch_size <= 0:
            batch_size = 3000

        for batch_start in range(0, data_size, batch_size):
            batch_end = min(batch_start + batch_size, data_size)
            batch = data_list[batch_start:batch_end]

            params_list = []
            for data_dict in batch:
                if not isinstance(data_dict, dict):
                    data_dict = data_dict.__dict__
                values = tuple(data_dict[key] for key in field_list)
                params_list.append(values)

            self._execute_many(sql, params_list)

    def foreach_replace(
        self,
        data_list: List[Any],
        db_name: str,
        tb_name: str,
        ignore_error: bool = False,
        auto_keys: List[str] = None
    ) -> None:
        """
        逐条替换数据（内部使用参数化查询，支持异常忽略）

        Args:
            data_list: 字典列表或对象列表
            db_name: 数据库名
            tb_name: 表名
            ignore_error: 是否忽略错误
            auto_keys: 自增字段列表
        """
        if auto_keys is None:
            auto_keys = ["id", "fid"]

        if not data_list:
            self.logger.info(f"[foreach_replace] No data to replace: {db_name}.{tb_name}", stacklevel=3)
            return

        self.logger.info(f"[foreach_replace] Data size: {len(data_list)}", stacklevel=3)

        if len(data_list) >= 50000:
            self.logger.warning(
                f"[foreach_replace] Too much data for this method: {len(data_list)}",
                stacklevel=3
            )

        # 获取字段列表
        first = data_list[0]
        if not isinstance(first, dict):
            first = first.__dict__

        field_list = [key for key in first.keys() if key.lower() not in auto_keys]
        field_str = ", ".join(field_list)
        placeholders = ", ".join(["%s"] * len(field_list))
        sql = f"REPLACE INTO {db_name}.{tb_name}({field_str}) VALUES ({placeholders})"

        for data_dict in data_list:
            if not isinstance(data_dict, dict):
                data_dict = data_dict.__dict__

            params = tuple(data_dict[key] for key in field_list)

            try:
                self.update(sql, params=params)
            except Exception as error:
                self.logger.error(f"[foreach_replace] Failed to replace: {error}", stacklevel=3)
                if not ignore_error:
                    raise error

    def batch_delete(self, sql: str, params: Params = None, batch_size: int = 30000) -> DBResult:
        """
        分批删除数据

        Args:
            sql: DELETE语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应
            batch_size: 每批删除的记录数

        Returns:
            DBResult实例

        Raises:
            DBError: 非DELETE语句时抛出

        Example:
            # 传统方式
            >>> db.batch_delete(f"DELETE FROM users WHERE age < {min_age}")

            # 参数化查询
            >>> db.batch_delete("DELETE FROM users WHERE age < %s", params=(min_age,))
        """
        sql = sql.replace("\n", " ").strip()

        # SQL 校验
        if not sql.lower().startswith("delete"):
            raise DBError(message=f"not delete sql! {sql}")

        if sql.endswith(";"):
            sql = sql[:-1]

        if "limit" not in sql.lower():
            sql += f" LIMIT {batch_size}"

        count = 0
        while count < 100000:
            count += 1
            log_msg = Xutil.simple_str(sql)[:8192]
            if params is not None:
                log_msg += f" | params: {str(params)[:512]}"
            self.logger.info(log_msg, stacklevel=3)
            db_ret = self._execute(sql, params=params, need_fetch=True, auto_commit=True)
            if db_ret.row_count <= 0:
                break

        return self.local_thread_data.db_ret

    def commit(self) -> DBResult:
        """提交事务"""
        try:
            self.local_thread_data.connection.commit()
        except mysql.connector.Error as error:
            raise DBError(message=str(error))
        return DBResult()

    def close(self) -> None:
        """关闭连接"""
        self.local_thread_data.cursor.close()
        self.local_thread_data.connection.close()

    def get_character_set(self) -> Dict[str, str]:
        """
        查询数据库字符编码设置

        Returns:
            字符编码配置字典
        """
        sql = "SHOW VARIABLES LIKE 'character_set%%'"
        ret = self.query(sql)

        if not ret.data:
            self.logger.info(f"DB result is empty: {ret.data}", stacklevel=3)
            return {}

        result = {}
        for item in ret.data:
            key = item["Variable_name"]
            value = item["Value"]
            result[key] = value

        return result

    def update_no_commit(self, sql: str, params: Params = None) -> DBResult:
        """
        执行更新SQL但不自动提交（用于事务）

        Args:
            sql: 更新SQL语句（可包含 %s 占位符）
            params: SQL参数，与 %s 占位符对应

        Returns:
            DBResult实例

        Example:
            # 事务操作
            >>> db.update_no_commit("UPDATE accounts SET balance = balance - %s WHERE id = %s", params=(100, 1))
            >>> db.update_no_commit("UPDATE accounts SET balance = balance + %s WHERE id = %s", params=(100, 2))
            >>> db.commit()
        """
        simple_sql = Xutil.simple_str(sql)
        if len(simple_sql) > 8192:
            simple_sql = f"{simple_sql[:8192]} ..."
        if params is not None:
            simple_sql += f" | params: {str(params)[:512]}"
        self.logger.info(simple_sql, stacklevel=3)

        # SQL 校验
        _ALLOWED_PREFIXES = ("delete", "insert", "replace", "update", "alter", "set")
        sql_lower = sql.strip().lower()
        if not sql_lower.startswith(_ALLOWED_PREFIXES):
            raise DBError(message=f"not delete|insert|replace|update|alter|set sql! {sql}")
        if sql_lower.startswith("delete") or sql_lower.startswith("set"):
            self.logger.info(Xutil.simple_str(sql), stacklevel=3)

        # 中文编码处理（仅对无参数的拼接SQL生效）
        if params is None and Xutil.contains_chinese(sql) and self.charset.lower() == "latin1":
            sql = sql.encode("gbk", "ignore")

        return self._execute(sql, params=params, need_fetch=False, auto_commit=False)


if __name__ == "__main__":
    pass
