"""CLAUDE.md 标签隔离读写 — 跨会话上下文持久化 (#260)

磁盘级兜底：即使所有 hook 失败，上次写入的 CLAUDE.md 仍在磁盘，
Claude Code 启动时自动读取。

写入格式：
    <index1-context>
    ... index1 自动生成的内容 ...
    </index1-context>

标签隔离：标签外的内容（用户手写、claude-mem 标签等）不受影响。

三层防御：
- L1: 路径/可写性/内容类型校验 → 非法值拒绝执行
- L2: 原子写入(tmp + rename) + 写入后字节数校验
- L3: 日志记录写入/失败事件
"""

from __future__ import annotations

import logging
import os
import re
import shutil
from pathlib import Path

logger = logging.getLogger(__name__)

# 标签匹配正则 — 匹配 <tag>...</tag> 包括标签本身
_TAG_PATTERN_TEMPLATE = r"<{tag}>\n?[\s\S]*?</{tag}>\n?"


def write_tagged_content(
    file_path: Path | str,
    tag_name: str,
    content: str,
) -> bool:
    """写入带标签隔离的内容到文件

    如果文件中已有 <tag_name>...</tag_name>，替换标签内内容。
    如果文件中没有该标签，追加到文件末尾。
    如果文件不存在，创建文件并写入。

    Args:
        file_path: 目标文件路径
        tag_name: 标签名（如 "index1-context"）
        content: 标签内的内容（不含标签本身）

    Returns:
        True 写入成功，False 失败（已记录日志）
    """
    file_path = Path(file_path)

    # ── L1 事前校验 ──
    if not isinstance(tag_name, str) or not re.match(r"^[a-zA-Z0-9_-]+$", tag_name):
        logger.error("标签名非法: %r", tag_name)
        return False

    if not isinstance(content, str):
        logger.error("内容类型非法: %s", type(content).__name__)
        return False

    # 内容上限 10KB（防止意外写入巨量数据）
    if len(content) > 10240:
        logger.warning("内容过大 (%d chars)，截断至 10KB", len(content))
        content = content[:10240]

    try:
        file_path.parent.mkdir(parents=True, exist_ok=True)
    except OSError as e:
        logger.error("创建目录失败: %s — %s", file_path.parent, e)
        return False

    if file_path.exists() and not os.access(file_path, os.W_OK):
        logger.error("文件不可写: %s", file_path)
        return False

    # 磁盘空间粗检（<1MB 拒绝写入）
    try:
        if shutil.disk_usage(file_path.parent).free < 1_000_000:
            logger.error("磁盘空间不足 (<1MB): %s", file_path.parent)
            return False
    except OSError:
        pass  # disk_usage 不可用时跳过检查

    # ── L2 事中操作 ──
    tagged_block = f"<{tag_name}>\n{content}\n</{tag_name}>\n"
    pattern = re.compile(_TAG_PATTERN_TEMPLATE.format(tag=re.escape(tag_name)))

    try:
        if file_path.exists():
            original = file_path.read_text(encoding="utf-8")
            if pattern.search(original):
                # 替换已有标签
                new_content = pattern.sub(tagged_block, original)
            else:
                # 追加到文件末尾
                new_content = original.rstrip("\n") + "\n\n" + tagged_block
        else:
            new_content = tagged_block
    except (OSError, UnicodeDecodeError) as e:
        logger.error("读取原文件失败: %s — %s", file_path, e)
        return False

    # 原子写入: tmp → rename
    tmp_path = file_path.with_suffix(file_path.suffix + ".index1.tmp")
    try:
        tmp_path.write_text(new_content, encoding="utf-8")

        # L2: 写入后字节数校验
        written = tmp_path.stat().st_size
        if written < 2:
            logger.error("写入后文件异常小: %d bytes", written)
            tmp_path.unlink(missing_ok=True)
            return False

        tmp_path.rename(file_path)
    except OSError as e:
        logger.error("原子写入失败: %s — %s", file_path, e)
        try:
            tmp_path.unlink(missing_ok=True)
        except OSError:
            pass
        return False

    # ── L3 事后日志 ──
    logger.debug(
        "CLAUDE.md 标签更新: %s tag=%s (%d bytes)",
        file_path, tag_name, len(new_content),
    )
    return True


def read_tagged_content(
    file_path: Path | str,
    tag_name: str,
) -> str | None:
    """读取文件中指定标签的内容

    Args:
        file_path: 文件路径
        tag_name: 标签名

    Returns:
        标签内的文本内容（不含标签本身），不存在返回 None
    """
    file_path = Path(file_path)

    # L1: 基本校验
    if not isinstance(tag_name, str) or not re.match(r"^[a-zA-Z0-9_-]+$", tag_name):
        return None

    if not file_path.exists():
        return None

    try:
        # L1: 文件大小校验（>1MB 拒绝读取）
        if file_path.stat().st_size > 1_048_576:
            logger.warning("文件过大，跳过标签读取: %s", file_path)
            return None

        text = file_path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError) as e:
        logger.debug("读取文件失败: %s — %s", file_path, e)
        return None

    # 提取标签内容
    pattern = re.compile(
        rf"<{re.escape(tag_name)}>\n?([\s\S]*?)</{re.escape(tag_name)}>",
    )
    match = pattern.search(text)
    if not match:
        return None

    content = match.group(1).strip()
    return content if content else None
