# -*- coding: utf-8 -*-
"""AtomicFileWriter + EncodingDetector — 原子文件写入与编码检测。

借鉴 zhikuncode-main：
- tool/impl/AtomicFileWriter.java（~256 行）— CAS 原子写入
- tool/impl/EncodingDetector.java（~138 行）— 编码检测

设计要点：
- AtomicFileWriter：临时文件 → fsync → os.replace（CAS 原子替换）→ 失败自动清理
- 大文件（>10MB）跳过原子写入避免临时空间翻倍
- EncodingDetector：BOM → 二进制检测 → UTF-8 验证 → ISO-8859-1 fallback，纯标准库实现
- 返回 None 表示二进制文件
"""
import os
import threading
from typing import Optional, Tuple


class AtomicFileWriter:
    """CAS 原子文件写入器。

    借鉴 zhikuncode AtomicFileWriter，简化版（去除 SHA 校验和版本追踪，
    这些可在 Phase 4 S FileHistoryService 中独立实现）。

    写入流程：
        1. 在同目录创建临时文件 `<filename>.<pid>.<tid>.tmp`
        2. 写入内容并 fsync 持久化
        3. os.replace(tmp, target) 原子替换（CAS 语义）
        4. 失败时清理临时文件

    大文件（>10MB）跳过原子写入避免临时空间翻倍，直接走普通 open().write()。
    追加模式（append=True）不走原子替换，直接追加。
    """

    LARGE_FILE_THRESHOLD = 10 * 1024 * 1024  # 10MB

    @staticmethod
    def write(
        path: str,
        content: str,
        encoding: str = "utf-8",
        append: bool = False,
    ) -> None:
        """原子写入文件。

        Args:
            path: 目标文件路径
            content: 要写入的内容
            encoding: 文件编码，默认 utf-8
            append: 是否追加模式。True 时不走原子替换，直接追加。

        Raises:
            OSError: 文件系统错误（权限/磁盘满/路径无效等）
        """
        # 追加模式直接走非原子写入（追加语义无原子性概念）
        if append:
            AtomicFileWriter._direct_write(path, content, encoding, append=True)
            return

        # 大文件跳过原子写入
        content_bytes = content.encode(encoding) if isinstance(content, str) else content
        if len(content_bytes) > AtomicFileWriter.LARGE_FILE_THRESHOLD:
            AtomicFileWriter._direct_write(path, content, encoding, append=False)
            return

        AtomicFileWriter._atomic_write(path, content, encoding)

    @staticmethod
    def _atomic_write(path: str, content: str, encoding: str) -> None:
        """执行原子写入：临时文件 → fsync → os.replace。"""
        # 确保父目录存在
        parent = os.path.dirname(os.path.abspath(path))
        os.makedirs(parent, exist_ok=True)

        # 临时文件名：与目标同目录，含 pid + tid 防止冲突
        filename = os.path.basename(path)
        tmp_path = "{}/.{}.{}.{}.tmp".format(
            parent, filename, os.getpid(), threading.get_ident()
        )

        try:
            # 1. 写入临时文件
            with open(tmp_path, "w", encoding=encoding, newline="") as f:
                f.write(content)
                f.flush()
                # 2. fsync 持久化（防止系统崩溃丢失数据）
                try:
                    os.fsync(f.fileno())
                except OSError:
                    # 某些文件系统（如网络挂载）不支持 fsync，忽略错误
                    pass

            # 3. 原子替换目标文件
            os.replace(tmp_path, path)

        except Exception:
            # 失败时清理临时文件
            if os.path.exists(tmp_path):
                try:
                    os.unlink(tmp_path)
                except OSError:
                    pass
            raise

    @staticmethod
    def _direct_write(
        path: str,
        content: str,
        encoding: str,
        append: bool,
    ) -> None:
        """直接写入（非原子，用于大文件或追加模式）。"""
        parent = os.path.dirname(os.path.abspath(path))
        os.makedirs(parent, exist_ok=True)
        mode = "a" if append else "w"
        with open(path, mode, encoding=encoding) as f:
            f.write(content)


class EncodingDetector:
    """文件编码检测器（纯标准库实现，无第三方依赖）。

    借鉴 zhikuncode EncodingDetector，检测策略：
        1. BOM 检测（UTF-8/UTF-16 LE/BE）
        2. 二进制检测（null 字节 >1% 或控制字符 >5%）
        3. UTF-8 有效性验证（覆盖 90%+ 代码文件）
        4. ISO-8859-1 fallback（全字节范围有效）

    返回 None 表示二进制文件。
    返回编码名称字符串（如 "utf-8"、"utf-8-sig"、"gbk" 等）。

    注意：与 zhikuncode 实现不同，本检测器不引入 chardet，
    对于无 BOM 的 GBK/Shift-JIS 等编码会回退到 iso-8859-1。
    若需更精确的启发式检测，可后续引入 charset_normalizer。
    """

    BOM_MAP: Tuple[Tuple[bytes, str], ...] = (
        (b"\xef\xbb\xbf", "utf-8-sig"),
        (b"\xff\xfe", "utf-16-le"),
        (b"\xfe\xff", "utf-16-be"),
    )
    DETECT_BUFFER_SIZE = 8192

    @staticmethod
    def detect(path: str) -> Optional[str]:
        """检测文件编码。

        Args:
            path: 文件路径

        Returns:
            编码名称字符串，或 None 表示二进制文件。

        Raises:
            OSError: 文件读取失败
        """
        with open(path, "rb") as f:
            sample = f.read(EncodingDetector.DETECT_BUFFER_SIZE)

        if not sample:
            return "utf-8"  # 空文件默认 UTF-8

        # 1. BOM 检测（最优先）
        bom_encoding = EncodingDetector._detect_bom(sample)
        if bom_encoding is not None:
            return bom_encoding

        # 2. 二进制检测
        if EncodingDetector._is_binary(sample):
            return None

        # 3. UTF-8 验证
        if EncodingDetector._is_valid_utf8(sample):
            return "utf-8"

        # 4. ISO-8859-1 fallback
        return "iso-8859-1"

    @staticmethod
    def _detect_bom(head: bytes) -> Optional[str]:
        """BOM 检测。返回编码名或 None。"""
        for bom, encoding in EncodingDetector.BOM_MAP:
            if head.startswith(bom):
                return encoding
        return None

    @staticmethod
    def _is_binary(sample: bytes) -> bool:
        """二进制检测：null 字节 >1% 或控制字符 >5% 视为二进制。

        Args:
            sample: 文件前 N 字节样本

        Returns:
            True 表示二进制文件
        """
        if not sample:
            return False

        null_count = 0
        control_count = 0
        for b in sample:
            if b == 0:
                null_count += 1
            elif b < 0x08 or (0x0D < b < 0x20 and b != 0x1B):
                # 控制字符（排除 \t \n \r 和 ESC）
                control_count += 1

        return (
            null_count > len(sample) * 0.01
            or control_count > len(sample) * 0.05
        )

    @staticmethod
    def _is_valid_utf8(data: bytes) -> bool:
        """UTF-8 有效性验证。

        在样本末尾找到最后一个完整 UTF-8 字符的边界，
        截断不完整的尾部字节后用严格 UTF-8 解码器验证。

        Args:
            data: 字节样本

        Returns:
            True 表示样本是合法 UTF-8
        """
        if not data:
            return True

        # 找到 UTF-8 安全边界
        valid_length = EncodingDetector._find_utf8_safe_boundary(data)
        if valid_length == 0:
            return True  # 全是续字节，按 ISO-8859-1 处理

        try:
            data[:valid_length].decode("utf-8", errors="strict")
            return True
        except UnicodeDecodeError:
            return False

    @staticmethod
    def _find_utf8_safe_boundary(data: bytes) -> int:
        """找到 UTF-8 安全边界。

        从末尾回退最多 3 字节，确保不在多字节序列中间截断。

        UTF-8 编码规则：
        - 1 字节: 0xxxxxxx
        - 多字节起始: 11xxxxxx
        - 续字节: 10xxxxxx
        """
        length = len(data)
        if length == 0:
            return 0

        # 从末尾向前检查最多 3 字节
        for i in range(1, min(4, length + 1)):
            b = data[length - i]
            if (b & 0x80) == 0:
                # ASCII 单字节字符，边界安全
                return length
            if (b & 0xC0) == 0xC0:
                # 多字节序列起始字节
                expected_len = EncodingDetector._get_utf8_sequence_length(b)
                if expected_len <= i:
                    # 序列完整
                    return length
                else:
                    # 序列不完整，截断到此起始字节之前
                    return length - i
            # 10xxxxxx 续字节，继续向前找起始字节
        # 回退 3 字节仍未找到起始字节
        return max(0, length - 3)

    @staticmethod
    def _get_utf8_sequence_length(start_byte: int) -> int:
        """根据起始字节获取 UTF-8 序列长度。"""
        if (start_byte & 0xE0) == 0xC0:
            return 2
        if (start_byte & 0xF0) == 0xE0:
            return 3
        if (start_byte & 0xF8) == 0xF0:
            return 4
        return 1  # 无效起始字节，按单字节处理
