"""通用配置模块 - 简化的 JSON 配置基类."""

from __future__ import annotations

import atexit
import contextlib
import json
import logging
import os
import platform
import tempfile
import threading
from decimal import Decimal
from functools import cached_property
from pathlib import Path
from typing import Callable, ClassVar

try:
    import fcntl
    from fcntl import LOCK_EX  # type: ignore[attr-defined]

    HAS_FCNTL = True
except ImportError:
    HAS_FCNTL = False

__all__ = ["ConfigMixin"]

logger = logging.getLogger(__name__)


class _DefaultConfigMixin:
    @property
    def _HOME_DIR(self) -> Path:
        """获取用户主目录."""
        return Path.home()

    @property
    def _EXTENSION(self) -> str:
        """获取可执行文件扩展名."""
        return ".exe" if self._IS_WINDOWS else ""

    @property
    def _IS_WINDOWS(self) -> bool:
        """判断是否为 Windows 系统."""
        return platform.system() == "Windows"


class ConfigMixin(_DefaultConfigMixin):
    """简化的 JSON 配置混入基类.

    核心功能:
    - 自动从 JSON 文件加载配置
    - 支持动态设置属性并保存
    - 简单的属性访问代理

    安全特性:
    - 线程安全的配置读写
    - 循环引用检测和清理
    - 保存失败时的备份恢复机制
    - 多进程安全的文件锁机制
    """

    ROOT_DIR: ClassVar[Path] = Path.home() / ".pytola"
    NAME: str = ""

    _file_attrs: dict[str, object]
    _attrs: dict[str, object]
    _lock: threading.RLock

    def __init__(self) -> None:
        """初始化配置混入类."""
        self._file_attrs: dict[str, object] = {}
        self._attrs: dict[str, object] = {}
        self._observers: list = []

        # 使用可重入锁允许同一线程多次获取
        self._lock = threading.RLock()

        self.load_from_json()

        if not self.ROOT_DIR.exists():
            logger.debug(f"创建设置目录:{self.ROOT_DIR}")
            self.ROOT_DIR.mkdir(parents=True, exist_ok=True)

        atexit.register(self.save_to_json)

    @property
    def attrs(self) -> dict[str, object]:
        """获取所有配置属性."""
        return {k: v for k, v in self._attrs.items() if not k.startswith("_") and not callable(v)}

    @property
    def attrs_with_private(self) -> dict[str, object]:
        """获取所有配置属性,包括私有属性."""
        return {k: v for k, v in self._attrs.items() if not callable(v)}

    def __getattribute__(self, name: str) -> object:
        """获取属性,优先从实例属性中查找."""
        # 私有属性和特殊属性直接访问
        if name.startswith("_") or (name.startswith("__") and name.endswith("__")):
            return object.__getattribute__(self, name)

        # 从 _attrs 中查找配置属性
        attrs = object.__getattribute__(self, "_attrs")
        if name in attrs:
            value = attrs[name]
            # 只在调试级别输出详细日志
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f"获取配置属性:{name} = {value!r}")
            return value

        return object.__getattribute__(self, name)

    def __setattr__(self, name: str, value: object) -> None:
        """设置属性."""
        if name.startswith("_"):
            object.__setattr__(self, name, value)
        else:
            with self._lock:
                old_value = self._attrs.get(name)
                self._attrs[name] = value

                # 通知观察者
                if old_value != value:
                    self._notify_observers(name, old_value, value)

    def add_observer(self, callback: Callable[[str, object, object], None]) -> None:
        """添加配置变更观察者."""
        with self._lock:
            if callback not in self._observers:
                self._observers.append(callback)
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug(
                        f"添加配置观察者:{callback.__name__ if hasattr(callback, '__name__') else 'anonymous'}",
                    )

    def remove_observer(self, callback: Callable[[str, object, object], None]) -> None:
        """移除配置变更观察者.

        Args:
            callback: 要移除的回调函数
        """
        with self._lock:
            if callback in self._observers:
                self._observers.remove(callback)
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug(
                        f"移除配置观察者:{callback.__name__ if hasattr(callback, '__name__') else 'anonymous'}",
                    )

    def _notify_observers(self, key: str, old_value: object, new_value: object) -> None:
        """通知所有观察者配置变更."""
        if not self._observers:
            return

        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f"通知 {len(self._observers)} 个观察者配置变更:{key}")

        try:
            for observer in self._observers[:]:  # 使用副本避免迭代时修改列表
                observer(key, old_value, new_value)
        except (OSError, RuntimeError, ValueError) as e:
            logger.error(f"观察者 {observer} 执行失败:{e}", exc_info=True)

    @property
    def _config_file(self) -> Path:
        """配置文件路径."""
        name = self.NAME or self._default_name
        return self.ROOT_DIR / f"{name}.json"

    @property
    def _backup_file(self) -> Path:
        """备份文件路径."""
        return self._config_file.with_suffix(".json.bak")

    @property
    def _default_name(self) -> str:
        """默认配置名称 (从类名转换)."""
        import re

        class_name = type(self).__name__
        name = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", class_name)
        name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
        return name.replace("_config", "")

    @cached_property
    def cls_attrs(self) -> dict[str, object]:
        """类属性字典."""
        return {
            attr: value
            for attr, value in self.__class__.__dict__.items()
            if not attr.startswith("_") and not callable(value)
        }

    def load_from_json(self) -> None:
        """从 JSON 文件加载配置.

        加载优先级:
        1. 文件中的配置值(最高优先级)
        2. 类属性默认值(最低优先级)
        """
        # 清空现有属性
        self._attrs.clear()
        self._file_attrs.clear()

        # 先加载类属性作为默认值
        self._attrs.update(self.cls_attrs)
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f"加载类属性默认值:{self.cls_attrs}")

        # 如果配置文件不存在,直接返回
        if not self._config_file.exists():
            logger.debug(f"配置文件不存在,使用默认值:{self._config_file}")
            return

        try:
            with self._config_file.open("r", encoding="utf-8") as f:
                loaded_data = json.load(f)

            # 处理 null 值
            if loaded_data is None:
                logger.warning(f"配置文件内容为 null,忽略文件配置: {self._config_file}")
                return

            # 验证数据格式
            if not isinstance(loaded_data, dict):
                logger.error(
                    f"配置文件格式错误,期望 dict 类型但得到 {type(loaded_data).__name__}: {self._config_file}",
                )
                return

            self._file_attrs = loaded_data
            # 文件配置覆盖类属性(重要:这是用户期望的行为)
            self._attrs.update(self._file_attrs)
            if logger.isEnabledFor(logging.DEBUG):
                logger.debug(f"从配置文件加载并覆盖属性:{self._file_attrs}")

        except json.JSONDecodeError:
            logger.exception(f"配置文件 JSON 解析失败:{self._config_file}")
            self._restore_from_backup()
        except OSError as e:
            logger.exception(f"读取配置文件失败:{self._config_file}", exc_info=e)

        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(f"最终加载的属性:{self._attrs}")

    def _restore_from_backup(self) -> None:
        """从备份文件恢复配置."""
        if self._backup_file.exists():
            logger.info(f"尝试从备份文件恢复:{self._backup_file}")
            try:
                with self._backup_file.open("r", encoding="utf-8") as f:
                    self._file_attrs = json.load(f) or {}
                logger.info("成功从备份文件恢复配置")
            except (json.JSONDecodeError, OSError) as e:
                logger.exception("从备份文件恢复失败", exc_info=e)

    def _atomic_write_config(self, data: dict) -> None:
        """原子写入配置文件,支持多进程并发安全.

        使用临时文件 + 重命名的方式确保写入的原子性:
        1. 在同一目录创建临时文件
        2. 写入数据到临时文件
        3. 使用原子重命名替换原文件

        在 Unix 系统上使用文件锁防止并发冲突.
        """
        config_dir = self._config_file.parent

        # 创建临时文件(在同一文件系统上以确保原子操作)
        fd, temp_path = tempfile.mkstemp(suffix=".tmp", dir=config_dir)
        temp_file = Path(temp_path)

        try:
            # 写入临时文件
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=4, ensure_ascii=False, default=self._default_encoder)
                f.flush()
                os.fsync(f.fileno())  # 确保数据刷入磁盘

            # 在 Unix 系统上对临时文件加锁
            if HAS_FCNTL:
                with contextlib.suppress(OSError):
                    fcntl.flock(fd, LOCK_EX)  # type: ignore[attr-defined]

            # 原子替换原配置文件
            # Windows 上 replace 可能失败如果文件被占用,需要重试
            max_retries = 3
            for attempt in range(max_retries):
                try:
                    temp_file.replace(self._config_file)
                    break
                except PermissionError as e:
                    if attempt < max_retries - 1:
                        logger.debug(f"替换配置文件失败,{0.1 * (attempt + 1)}秒后重试:{e}")
                        import time

                        time.sleep(0.1 * (attempt + 1))
                    else:
                        raise

        except Exception:
            # 清理临时文件
            with contextlib.suppress(OSError):
                temp_file.unlink()
            raise

    def _default_encoder(self, obj: object) -> object:
        """自定义 JSON 编码器,处理不可序列化的类型."""
        # 处理复数类型
        if isinstance(obj, complex):
            return {"__complex__": True, "real": obj.real, "imag": obj.imag}
        # 处理 Decimal 类型
        if isinstance(obj, Decimal):
            return float(obj)
        # 处理集合类型
        if isinstance(obj, (set, frozenset)):
            return list(obj)
        # 处理字节类型
        if isinstance(obj, bytes):
            return obj.decode("utf-8", errors="replace")
        # 处理其他不可序列化类型,返回字符串表示
        return str(obj)

    def _remove_circular_refs(
        self,
        obj: object,
        seen: set[int] | None = None,
        depth: int = 0,
        max_depth: int = 50,
    ) -> object:
        """递归移除循环引用,限制最大深度避免栈溢出."""
        if seen is None:
            seen = set()

        # 限制递归深度,防止栈溢出
        if depth > max_depth:
            return "<max depth exceeded>"

        # 如果是可变对象,检查循环引用,避免无限递归
        if isinstance(obj, dict):
            obj_id = id(obj)
            if obj_id in seen:
                return "<circular reference>"
            seen.add(obj_id)
            result = {k: self._remove_circular_refs(v, seen, depth + 1, max_depth) for k, v in obj.items()}
            seen.discard(obj_id)
            return result

        if isinstance(obj, (list, tuple)):
            obj_id = id(obj)
            if obj_id in seen:
                return "<circular reference>"
            seen.add(obj_id)
            result = [self._remove_circular_refs(item, seen, depth + 1, max_depth) for item in obj]
            seen.discard(obj_id)
            return result
        return obj

    def _is_basic_serializable(self, obj: object) -> bool:
        """检查对象是否是基本可序列化类型.

        Args:
            obj: 要检查的对象

        Returns
        -------
            如果是基本可序列化类型返回 True,否则返回 False
        """
        return isinstance(obj, (str, int, float, bool, type(None), Decimal, complex, bytes))

    def _validate_container(
        self,
        obj: dict | list | tuple,
        path: str,
        seen: set[int],
        max_depth: int,
        current_depth: int,
    ) -> bool:
        """验证容器类型(dict/list/tuple)的可序列化性.

        Args:
            obj: 要验证的容器对象
            path: 当前对象的路径
            seen: 已访问对象的 ID 集合
            max_depth: 最大嵌套深度
            current_depth: 当前嵌套深度

        Returns
        -------
            如果可序列化返回 True,否则返回 False
        """
        obj_id = id(obj)
        if obj_id in seen:
            # 检测到循环引用,直接返回 False,不再继续遍历
            return False
        seen.add(obj_id)

        if isinstance(obj, dict):
            for k, v in obj.items():
                if not isinstance(k, str):
                    with contextlib.suppress(ValueError, RuntimeError, OSError):
                        logger.warning(f"字典键不是字符串:{k!r} at '{path}'")
                    return False
                if not self._validate_serializable(v, f"{path}.{k}", seen.copy(), max_depth, current_depth + 1):
                    return False
        else:  # list or tuple
            for i, item in enumerate(obj):
                if not self._validate_serializable(item, f"{path}[{i}]", seen.copy(), max_depth, current_depth + 1):
                    return False

        return True

    def _validate_serializable(
        self,
        obj: object,
        path: str = "",
        seen: set[int] | None = None,
        max_depth: int = 50,
        current_depth: int = 0,
    ) -> bool:
        """验证对象是否可以 JSON 序列化.

        Args:
            obj: 要验证的对象
            path: 当前对象的路径(用于错误报告)
            seen: 已访问对象的 ID 集合,用于检测循环引用
            max_depth: 最大嵌套深度
            current_depth: 当前嵌套深度
        """
        if seen is None:
            seen = set()

        # 限制递归深度,防止栈溢出
        if current_depth > max_depth:
            with contextlib.suppress(ValueError, RuntimeError, OSError):
                logger.error(f"对象嵌套过深 ({current_depth}/{max_depth}) at '{path}'")
            return False

        # 检查基本类型
        if self._is_basic_serializable(obj):
            return True

        # 检查容器类型
        if isinstance(obj, (dict, list, tuple)):
            result = self._validate_container(obj, path, seen, max_depth, current_depth)
            if not result:
                # 只在验证失败时记录一次错误,避免重复日志
                with contextlib.suppress(ValueError, RuntimeError, OSError):
                    logger.error(f"配置包含不可序列化的数据或循环引用 at '{path}'")
            return result

        # 其他类型会在编码时转换为字符串
        return True

    def save_to_json(self) -> None:
        """保存配置到 JSON 文件,支持多进程并发安全."""
        with self._lock:
            try:
                self._config_file.parent.mkdir(parents=True, exist_ok=True)

                # 过滤掉可调用对象
                serializable_attrs = {key: value for key, value in self._attrs.items() if not callable(value)}

                # 先处理循环引用(在验证之前处理,避免重复报错)
                cleaned_attrs = self._remove_circular_refs(serializable_attrs)

                # 验证数据可序列化性(可选,仅用于警告用户)
                if not self._validate_serializable(cleaned_attrs):
                    with contextlib.suppress(ValueError, RuntimeError, OSError):
                        logger.warning("配置包含不可完全序列化的数据,但仍会尝试保存")

                # 使用原子写入方式:先写入临时文件,再替换原文件
                # 这样可以避免多进程同时写入时的文件锁定问题
                self._atomic_write_config(cleaned_attrs)  # type: ignore[arg-type]

                logger.debug(f"配置已保存到:{self._config_file}")

            except (OSError, json.JSONDecodeError) as e:
                logger.exception(f"保存配置文件失败:{self._config_file}", exc_info=e)
                # 尝试从备份恢复(如果有备份的话)
                if self._backup_file.exists():
                    self._restore_from_backup()
