"""数据处理模块使用的无副作用算法。

本模块不访问数据库、文件系统或网络，便于 API、后台任务和测试共同复用。
所有偏移量均为 Python 字符串偏移量。
"""

from __future__ import annotations

import csv
import hashlib
import io
import json
import math
import re
import unicodedata
import xml.etree.ElementTree as ET
import zipfile
from collections import Counter
from collections.abc import Iterable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime, time
from pathlib import Path, PurePosixPath
from typing import Any, Literal
from urllib.parse import unquote, urlsplit

from docx import Document
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import Table
from docx.text.paragraph import Paragraph
from openpyxl import load_workbook
from openpyxl.utils.cell import range_boundaries
from pptx import Presentation
from pypdf import PdfReader

from app.modules.data_process.constants import MAX_QA_PAIRS_PER_ITEM

TextFormat = Literal[
    "json",
    "jsonl",
    "csv",
    "markdown",
    "txt",
    "pdf",
    "docx",
    "xlsx",
    "pptx",
]
DatasetSplit = Literal["train", "validation", "test"]
StructuredPreprocessOption = Literal[
    "clean_invalid",
    "detect_structure",
    "deduplicate",
    "normalize_format",
    "filter_anomaly",
    "desensitize",
]

SUPPORTED_TEXT_FORMATS: tuple[TextFormat, ...] = (
    "json",
    "jsonl",
    "csv",
    "markdown",
    "txt",
    "pdf",
    "docx",
    "xlsx",
    "pptx",
)

_FORMAT_ALIASES: dict[str, TextFormat] = {
    "json": "json",
    "jsonl": "jsonl",
    "ndjson": "jsonl",
    "csv": "csv",
    "tsv": "csv",
    "md": "markdown",
    "markdown": "markdown",
    "txt": "txt",
    "text": "txt",
    "pdf": "pdf",
    "docx": "docx",
    "xlsx": "xlsx",
    "pptx": "pptx",
}
_LEGACY_OFFICE_FORMATS: dict[str, str] = {
    "doc": "docx",
    "xls": "xlsx",
    "ppt": "pptx",
}
_OFFICE_OPEN_XML_FORMATS = {"docx", "xlsx", "pptx"}
_MAX_ARCHIVE_ENTRIES = 10_000
_MAX_ARCHIVE_UNCOMPRESSED_BYTES = 512 * 1024 * 1024
_MAX_ARCHIVE_ENTRY_BYTES = 128 * 1024 * 1024
_MAX_ARCHIVE_COMPRESSION_RATIO = 200
_MAX_EXTRACTED_TEXT_CHARS = 20_000_000
_MAX_PDF_PAGES = 2_000
_MAX_PRESENTATION_SLIDES = 2_000
_MAX_WORKBOOK_SHEETS = 100
_MAX_WORKBOOK_ROWS = 100_000
_MAX_WORKBOOK_SCANNED_ROWS = 200_000
_MAX_WORKBOOK_COLUMNS = 256
_MAX_WORKBOOK_CELLS = 2_000_000
_MAX_WORKBOOK_HEADER_ROWS = 8
_MAX_WORKBOOK_HEADER_SCAN_ROWS = 64
_MAX_WORKBOOK_MERGED_RANGES = 100_000
_MAX_STRUCTURED_FIELDS = 1_024
_MAX_STRUCTURED_DEPTH = 16
_MAX_ANOMALY_TEXT_CHARS = 1_000_000
_STRUCTURED_OPTIONS = {
    "clean_invalid",
    "detect_structure",
    "deduplicate",
    "normalize_format",
    "filter_anomaly",
    "desensitize",
}
_IDENTITY_FIELD_PATTERN = re.compile(r"(?:^|[._])(?:id|uuid|key|code)$|(?:^|[._]).+_id$")
_MOJIBAKE_MARKERS = ("\ufffd", "锟斤拷", "烫烫烫", "屯屯屯", "Ã", "Â", "â€")
_NAME_FIELD_NAMES = {
    "name",
    "full_name",
    "fullname",
    "real_name",
    "contact_name",
    "customer_name",
    "recipient_name",
    "姓名",
    "真实姓名",
    "联系人",
    "联系人姓名",
    "收件人",
}

_EMAIL_PATTERN = re.compile(
    r"(?<![\w.+-])[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
    r"@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?"
    r"(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+(?![\w.-])"
)
_PHONE_PATTERN = re.compile(r"(?<!\d)(?:(?:\+|00)?86[-\s]?)?1[3-9]\d{9}(?!\d)")
_ID_CARD_PATTERN = re.compile(r"(?<!\d)(?:\d{17}[\dXx]|\d{15})(?!\d)")
_CHINESE_NAME_CONTEXT_PATTERN = re.compile(
    r"(?P<label>姓名|真实姓名|联系人(?:姓名)?|收件人)"
    r"(?P<separator>\s*(?:[:：=]|为)\s*|\s+)"
    r"(?P<name>[\u3400-\u4dbf\u4e00-\u9fff·]{2,8})"
)
_ENGLISH_NAME_CONTEXT_PATTERN = re.compile(
    r"(?im)(?P<label>full\s+name|contact\s+name|name)"
    r"(?P<separator>\s*[:=]\s*)"
    r"(?P<name>[A-Za-z][A-Za-z'’-]*(?:[ \t]+[A-Za-z][A-Za-z'’-]*){0,3})"
)
_TOKEN_PATTERN = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]|[A-Za-z0-9_]+|[^\s]")


@dataclass(frozen=True, slots=True)
class ParsedText:
    """文本、文档或工作簿的统一解析结果。"""

    format: TextFormat
    text: str
    records: tuple[dict[str, Any], ...]


@dataclass(frozen=True, slots=True)
class PdfPageText:
    """PDF 物理页在统一提取文本中的字符范围。"""

    page_number: int
    text: str
    source_start: int
    source_end: int


@dataclass(frozen=True, slots=True)
class DocumentNoiseSpan:
    """PDF 中可安全从展示内容移除的文本范围。"""

    start: int
    end: int
    kind: Literal["page_number", "repeated_margin", "table_of_contents"]


@dataclass(frozen=True, slots=True)
class QualityScore:
    """标准 instruction/input/output 记录的可解释质量分。"""

    overall: float
    completeness: float
    length: float
    readability: float
    relevance: float
    duplicate: float
    is_valid: bool
    flags: tuple[str, ...]
    fingerprint: str


@dataclass(frozen=True, slots=True)
class DocumentHeading:
    """文档标题的位置和层级。"""

    level: int
    title: str
    line_number: int
    start: int
    end: int


@dataclass(frozen=True, slots=True)
class DocumentStructure:
    """无需外部模型即可复现的文档结构摘要。"""

    line_count: int
    paragraph_count: int
    headings: tuple[DocumentHeading, ...]
    code_block_count: int
    table_block_count: int
    list_block_count: int


def decode_utf8(raw: bytes | bytearray | memoryview | str) -> str:
    """严格解码 UTF-8 文本，并移除可选 BOM。

    不使用 ``errors='replace'``，避免上传内容损坏后仍被静默接收。
    """

    if isinstance(raw, str):
        return raw.removeprefix("\ufeff")
    if not isinstance(raw, (bytes, bytearray, memoryview)):
        raise TypeError("raw must be bytes-like or str")
    try:
        return bytes(raw).decode("utf-8-sig")
    except UnicodeDecodeError as exc:
        raise ValueError(f"content is not valid UTF-8 at byte {exc.start}") from exc


def parse_utf8_text(raw: bytes | bytearray | memoryview | str) -> str:
    """``decode_utf8`` 的语义化别名，供上传服务直接调用。"""

    return decode_utf8(raw)


def normalize_text(text: str) -> str:
    """规范 Unicode、换行和行尾空白，同时保留段落结构。"""

    if not isinstance(text, str):
        raise TypeError("text must be str")
    normalized = unicodedata.normalize("NFKC", text.removeprefix("\ufeff"))
    normalized = normalized.replace("\r\n", "\n").replace("\r", "\n")
    normalized = "".join(
        char
        for char in normalized
        if char in {"\n", "\t"} or not unicodedata.category(char).startswith("C")
    )
    lines = [re.sub(r"[\t \f\v]+$", "", line) for line in normalized.split("\n")]
    return "\n".join(lines).strip()


def _normalize_format(value: str | None) -> TextFormat | None:
    if value is None:
        return None
    normalized = value.strip().lower().removeprefix(".")
    if normalized in _LEGACY_OFFICE_FORMATS:
        replacement = _LEGACY_OFFICE_FORMATS[normalized]
        raise ValueError(
            f"legacy .{normalized} format is not supported; "
            f"convert the file to .{replacement} and upload again"
        )
    try:
        return _FORMAT_ALIASES[normalized]
    except KeyError as exc:
        raise ValueError(f"unsupported text format: {value}") from exc


def detect_text_format(
    *,
    filename: str | None = None,
    text: str = "",
    file_format: str | None = None,
) -> TextFormat:
    """按显式格式、扩展名和内容特征依次识别文本格式。"""

    explicit = _normalize_format(file_format)
    if explicit:
        return explicit

    if filename:
        suffix = Path(filename).suffix.lower().removeprefix(".")
        if suffix in _LEGACY_OFFICE_FORMATS:
            replacement = _LEGACY_OFFICE_FORMATS[suffix]
            raise ValueError(
                f"legacy .{suffix} format is not supported; "
                f"convert the file to .{replacement} and upload again"
            )
        detected = _FORMAT_ALIASES.get(suffix)
        if detected:
            return detected

    stripped = text.strip()
    if stripped:
        if stripped[0] in "[{":
            try:
                json.loads(stripped)
            except json.JSONDecodeError:
                pass
            else:
                return "json"

        nonempty_lines = [line for line in stripped.splitlines() if line.strip()]
        if len(nonempty_lines) > 1:
            try:
                for line in nonempty_lines:
                    json.loads(line)
            except json.JSONDecodeError:
                pass
            else:
                return "jsonl"

        if re.search(r"(?m)^(?:#{1,6}\s+|```|~~~)", stripped) or re.search(
            r"(?m)^\s*\|.+\|\s*$", stripped
        ):
            return "markdown"

        sample = stripped[:8192]
        try:
            dialect = csv.Sniffer().sniff(sample, delimiters=",\t;")
            rows = list(csv.reader(io.StringIO(sample), dialect))
            if len(rows) >= 2 and len(rows[0]) >= 2:
                return "csv"
        except csv.Error:
            pass

    return "txt"


def _binary_bytes(
    raw: bytes | bytearray | memoryview | str,
    file_format: TextFormat,
) -> bytes:
    if isinstance(raw, str):
        raise ValueError(f"{file_format.upper()} content must be uploaded as binary data")
    if not isinstance(raw, (bytes, bytearray, memoryview)):
        raise TypeError("raw must be bytes-like or str")
    return bytes(raw)


def _validate_office_archive(raw: bytes, file_format: TextFormat) -> None:
    """在交给 Office 解析库前限制 ZIP 包规模并拒绝活动 XML。"""

    required_members = {
        "docx": {"[Content_Types].xml", "word/document.xml"},
        "xlsx": {"[Content_Types].xml", "xl/workbook.xml"},
        "pptx": {"[Content_Types].xml", "ppt/presentation.xml"},
    }
    try:
        with zipfile.ZipFile(io.BytesIO(raw)) as archive:
            members = archive.infolist()
            if len(members) > _MAX_ARCHIVE_ENTRIES:
                raise ValueError(
                    f"{file_format.upper()} archive contains too many entries "
                    f"(limit {_MAX_ARCHIVE_ENTRIES})"
                )

            names: set[str] = set()
            total_size = 0
            for member in members:
                path = PurePosixPath(member.filename.replace("\\", "/"))
                if path.is_absolute() or ".." in path.parts:
                    raise ValueError(
                        f"{file_format.upper()} archive contains an unsafe member path"
                    )
                if member.filename in names:
                    raise ValueError(
                        f"{file_format.upper()} archive contains duplicate member names"
                    )
                names.add(member.filename)
                if member.flag_bits & 0x1:
                    raise ValueError(f"encrypted {file_format.upper()} files are not supported")
                if member.is_dir():
                    continue
                if member.file_size > _MAX_ARCHIVE_ENTRY_BYTES:
                    raise ValueError(
                        f"{file_format.upper()} archive entry exceeds "
                        f"{_MAX_ARCHIVE_ENTRY_BYTES} bytes"
                    )
                total_size += member.file_size
                if total_size > _MAX_ARCHIVE_UNCOMPRESSED_BYTES:
                    raise ValueError(
                        f"{file_format.upper()} archive expands beyond "
                        f"{_MAX_ARCHIVE_UNCOMPRESSED_BYTES} bytes"
                    )
                if member.file_size >= 1024 * 1024:
                    if member.compress_size <= 0:
                        raise ValueError(
                            f"{file_format.upper()} archive has an unsafe compression ratio"
                        )
                    ratio = member.file_size / member.compress_size
                    if ratio > _MAX_ARCHIVE_COMPRESSION_RATIO:
                        raise ValueError(
                            f"{file_format.upper()} archive has an unsafe compression ratio"
                        )

            missing = required_members[file_format] - names
            if missing:
                raise ValueError(
                    f"invalid {file_format.upper()} package: missing "
                    f"{', '.join(sorted(missing))}"
                )

            for member in members:
                if member.is_dir() or not member.filename.lower().endswith((".xml", ".rels")):
                    continue
                with archive.open(member) as stream:
                    prefix = stream.read(min(member.file_size, 1024 * 1024)).upper()
                if b"<!DOCTYPE" in prefix or b"<!ENTITY" in prefix:
                    raise ValueError(
                        f"{file_format.upper()} archive contains unsupported active XML"
                    )
    except zipfile.BadZipFile as exc:
        raise ValueError(f"invalid {file_format.upper()} file: not an Office ZIP package") from exc


def _append_bounded_text(parts: list[str], value: Any, total: int) -> int:
    text = normalize_text(str(value or ""))
    if not text:
        return total
    total += len(text)
    if total > _MAX_EXTRACTED_TEXT_CHARS:
        raise ValueError(
            f"extracted document text exceeds {_MAX_EXTRACTED_TEXT_CHARS} characters"
        )
    parts.append(text)
    return total


def extract_pdf_page_texts(raw: bytes) -> tuple[PdfPageText, ...]:
    """提取 PDF 各页文本，并保留与切片字符偏移一致的页范围。"""

    if b"%PDF-" not in raw[:1024]:
        raise ValueError("invalid PDF file: missing PDF header")
    try:
        reader = PdfReader(io.BytesIO(raw), strict=True)
    except Exception as exc:
        raise ValueError(f"invalid PDF file: {exc}") from exc
    if reader.is_encrypted and not reader.decrypt(""):
        raise ValueError("password-protected PDF files are not supported")
    if len(reader.pages) > _MAX_PDF_PAGES:
        raise ValueError(f"PDF contains too many pages (limit {_MAX_PDF_PAGES})")

    pages: list[PdfPageText] = []
    total = 0
    has_text = False
    for page_number, page in enumerate(reader.pages, start=1):
        try:
            text = normalize_text(page.extract_text() or "")
        except Exception as exc:
            raise ValueError(
                f"failed to extract text from PDF page {page_number}: {exc}"
            ) from exc
        if not text:
            pages.append(
                PdfPageText(
                    page_number=page_number,
                    text="",
                    source_start=total,
                    source_end=total,
                )
            )
            continue
        if has_text:
            total += 2
        start = total
        total += len(text)
        if total > _MAX_EXTRACTED_TEXT_CHARS:
            raise ValueError(
                f"extracted document text exceeds {_MAX_EXTRACTED_TEXT_CHARS} characters"
            )
        pages.append(
            PdfPageText(
                page_number=page_number,
                text=text,
                source_start=start,
                source_end=total,
            )
        )
        has_text = True
    if not has_text:
        raise ValueError("PDF contains no extractable text; scanned PDF requires OCR")
    return tuple(pages)


@dataclass(frozen=True, slots=True)
class _PdfLine:
    text: str
    start: int
    end: int


_PDF_PAGE_NUMBER_LINE_PATTERN = re.compile(
    r"^(?:页次\s*)?(?:第\s*)?(?P<page>\d+)\s*页\s*"
    r"(?:(?:[/／]\s*)?共\s*(?P<total>\d+)\s*页)?$"
)
_PDF_FRACTION_PAGE_LINE_PATTERN = re.compile(
    r"^[—–-]?\s*(?P<page>\d+)\s*[/／]\s*(?P<total>\d+)\s*[—–-]?$"
)
_PDF_CLASSIFICATION_LABEL_PATTERN = re.compile(
    r"^(?:(?:秘密等级|密级)\s*)?(?:商密|秘密|机密|绝密)"
    r"\s*(?:[【\[(（][^】\])）]{1,8}[】\])）])?$"
)
_TOC_TITLE_PATTERN = re.compile(r"^(?:目\s*录|contents)$", re.IGNORECASE)
_TOC_LEADER_ENTRY_PATTERN = re.compile(
    r"(?:[.．…·•]\s*){3,}\s*\d{1,4}\s*$"
)
_TOC_NUMBERED_ENTRY_PATTERN = re.compile(
    r"^(?:第[\u3400-\u4dbf\u4e00-\u9fff]{1,12}章|附表\s*\d+|\d+(?:\.\d+)+)"
    r"\s+.+\s+\d{1,4}\s*$",
    re.IGNORECASE,
)
_MARGIN_TEMPLATE_KEYWORDS = (
    "页",
    "页次",
    "版本",
    "文件编码",
    "秘密等级",
    "密级",
    "商密",
    "confidential",
)


def _pdf_page_lines(page: PdfPageText) -> tuple[_PdfLine, ...]:
    lines: list[_PdfLine] = []
    local_offset = 0
    for raw_line in page.text.splitlines(keepends=True):
        content = raw_line.rstrip("\r\n")
        leading = len(content) - len(content.lstrip())
        trailing = len(content.rstrip())
        text = content.strip()
        if text:
            lines.append(
                _PdfLine(
                    text=text,
                    start=page.source_start + local_offset + leading,
                    end=page.source_start + local_offset + trailing,
                )
            )
        local_offset += len(raw_line)
    return tuple(lines)


def _is_standalone_page_number(
    text: str,
    *,
    physical_page: int,
    page_count: int,
) -> bool:
    normalized = unicodedata.normalize("NFKC", text).strip()
    match = _PDF_PAGE_NUMBER_LINE_PATTERN.fullmatch(normalized)
    if match is None:
        match = _PDF_FRACTION_PAGE_LINE_PATTERN.fullmatch(normalized)
    if match is None or int(match.group("page")) != physical_page:
        return False
    total = match.groupdict().get("total")
    return total is None or int(total) == page_count


def _margin_signature(text: str) -> str:
    normalized = unicodedata.normalize("NFKC", text).casefold()
    normalized = re.sub(r"\s+", " ", normalized).strip()
    if any(keyword in normalized for keyword in _MARGIN_TEMPLATE_KEYWORDS):
        normalized = re.sub(r"\d+", "#", normalized)
    return normalized


def _has_margin_metadata_keyword(text: str) -> bool:
    normalized = unicodedata.normalize("NFKC", text).casefold()
    return any(keyword in normalized for keyword in _MARGIN_TEMPLATE_KEYWORDS)


def _has_meaningful_margin_signature(signature: str) -> bool:
    return len(re.sub(r"[#\W_]+", "", signature, flags=re.UNICODE)) >= 2


def _is_toc_leader_entry(text: str) -> bool:
    return bool(_TOC_LEADER_ENTRY_PATTERN.search(text))


def _is_toc_numbered_entry(text: str) -> bool:
    return bool(_TOC_NUMBERED_ENTRY_PATTERN.fullmatch(text))


def detect_pdf_document_noise(
    pages: Sequence[PdfPageText],
) -> tuple[DocumentNoiseSpan, ...]:
    """识别 PDF 中的独立页码、重复页边内容和高置信目录。

    规则只查看每页顶部 5 行和底部 3 行来推断页眉页脚；目录必须有
    明显的点引导线密度，避免仅因正文中出现“目录”或章节标题而误删。
    """

    page_lines = tuple(_pdf_page_lines(page) for page in pages)
    detected: dict[tuple[int, int], DocumentNoiseSpan] = {}

    def mark(
        line: _PdfLine,
        kind: Literal["page_number", "repeated_margin", "table_of_contents"],
    ) -> None:
        detected.setdefault(
            (line.start, line.end),
            DocumentNoiseSpan(
                start=line.start,
                end=line.end,
                kind=kind,
            ),
        )

    for page, lines in zip(pages, page_lines, strict=True):
        for line in lines:
            if _is_standalone_page_number(
                line.text,
                physical_page=page.page_number,
                page_count=len(pages),
            ):
                mark(line, "page_number")
        outer_margin_lines = (*lines[:2], *lines[-2:])
        for line in outer_margin_lines:
            if _PDF_CLASSIFICATION_LABEL_PATTERN.fullmatch(line.text):
                mark(line, "repeated_margin")

    # 只在三页及以上文档中推断通用页眉页脚，避免短文档误删。
    if len(pages) >= 3:
        signature_pages: dict[str, set[int]] = {}
        candidate_lines: list[tuple[int, _PdfLine, str]] = []
        for page_index, lines in enumerate(page_lines):
            boundary_lines = (
                *((line, index < 2) for index, line in enumerate(lines[:5])),
                *((line, index < 2) for index, line in enumerate(reversed(lines[-3:]))),
            )
            seen_ranges: set[tuple[int, int]] = set()
            for line, is_outer_margin in boundary_lines:
                line_range = (line.start, line.end)
                if (
                    line_range in seen_ranges
                    or line_range in detected
                    or len(line.text) > 160
                ):
                    continue
                seen_ranges.add(line_range)
                if not is_outer_margin and not _has_margin_metadata_keyword(line.text):
                    continue
                signature = _margin_signature(line.text)
                if not _has_meaningful_margin_signature(signature):
                    continue
                signature_pages.setdefault(signature, set()).add(page_index)
                candidate_lines.append((page_index, line, signature))
        minimum_pages = max(3, math.ceil(len(pages) * 0.3))
        repeated_signatures = {
            signature
            for signature, matching_pages in signature_pages.items()
            if len(matching_pages) >= minimum_pages
        }
        for _, line, signature in candidate_lines:
            if signature in repeated_signatures:
                mark(line, "repeated_margin")

    # 先依据强证据判定目录页，再补充删除少量不带点引导线的编号目录项。
    toc_active = False
    for lines in page_lines:
        content_lines = [
            line for line in lines if (line.start, line.end) not in detected
        ]
        leader_entries = [line for line in content_lines if _is_toc_leader_entry(line.text)]
        titles = [line for line in content_lines if _TOC_TITLE_PATTERN.fullmatch(line.text)]
        starts_toc = bool(titles and len(leader_entries) >= 2)
        is_toc_dense = bool(
            len(leader_entries) >= 3
            and len(leader_entries) / max(1, len(content_lines)) >= 0.5
        )
        if not (starts_toc or (toc_active and is_toc_dense)):
            toc_active = False
            continue
        toc_active = True
        for line in content_lines:
            if (
                line in titles
                or _is_toc_leader_entry(line.text)
                or _is_toc_numbered_entry(line.text)
            ):
                mark(line, "table_of_contents")

    return tuple(sorted(detected.values(), key=lambda span: (span.start, span.end)))


def remove_document_noise(
    text: str,
    spans: Sequence[DocumentNoiseSpan],
    *,
    source_offset: int = 0,
) -> str:
    """按原文绝对偏移移除噪声，不改动调用方保留的原文及偏移。"""

    text_end = source_offset + len(text)
    intersections = sorted(
        (
            max(0, span.start - source_offset),
            min(len(text), span.end - source_offset),
        )
        for span in spans
        if span.start < text_end and span.end > source_offset
    )
    if not intersections:
        return text
    parts: list[str] = []
    cursor = 0
    for start, end in intersections:
        if end <= cursor:
            continue
        if start > cursor:
            parts.append(text[cursor:start])
        cursor = end
    parts.append(text[cursor:])
    cleaned = normalize_text("".join(parts))
    return re.sub(r"\n{3,}", "\n\n", cleaned)


def _extract_pdf_text(raw: bytes) -> str:
    return "\n\n".join(page.text for page in extract_pdf_page_texts(raw) if page.text)


def _extract_docx_text(raw: bytes) -> str:
    _validate_office_archive(raw, "docx")
    try:
        document = Document(io.BytesIO(raw))
    except Exception as exc:
        raise ValueError(f"invalid DOCX file: {exc}") from exc

    parts: list[str] = []
    total = 0
    for child in document.element.body.iterchildren():
        if isinstance(child, CT_P):
            total = _append_bounded_text(parts, Paragraph(child, document).text, total)
            continue
        if isinstance(child, CT_Tbl):
            table = Table(child, document)
            for row in table.rows:
                cells = [normalize_text(cell.text) for cell in row.cells]
                total = _append_bounded_text(parts, "\t".join(cells), total)
    return normalize_text("\n\n".join(parts))


def _presentation_shape_text(shape: Any) -> list[str]:
    if getattr(shape, "has_table", False):
        return [
            "\t".join(normalize_text(cell.text) for cell in row.cells)
            for row in shape.table.rows
        ]
    if getattr(shape, "has_text_frame", False):
        return [shape.text]
    child_shapes = getattr(shape, "shapes", None)
    if child_shapes is not None:
        values: list[str] = []
        for child in child_shapes:
            values.extend(_presentation_shape_text(child))
        return values
    return []


def _extract_pptx_text(raw: bytes) -> str:
    _validate_office_archive(raw, "pptx")
    try:
        presentation = Presentation(io.BytesIO(raw))
    except Exception as exc:
        raise ValueError(f"invalid PPTX file: {exc}") from exc
    if len(presentation.slides) > _MAX_PRESENTATION_SLIDES:
        raise ValueError(
            f"PPTX contains too many slides (limit {_MAX_PRESENTATION_SLIDES})"
        )

    parts: list[str] = []
    total = 0
    for slide in presentation.slides:
        slide_parts: list[str] = []
        for shape in slide.shapes:
            slide_parts.extend(_presentation_shape_text(shape))
        total = _append_bounded_text(parts, "\n".join(slide_parts), total)
    return normalize_text("\n\n".join(parts))


def _normalize_spreadsheet_value(value: Any) -> Any:
    if value is None:
        return ""
    if isinstance(value, str):
        return normalize_text(value)
    if isinstance(value, (datetime, date, time)):
        return value.isoformat()
    if isinstance(value, (bool, int, float)):
        return value
    return normalize_text(str(value))


def _xml_local_name(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]


def _resolve_xlsx_relationship_target(
    archive: zipfile.ZipFile,
    target: str,
    *,
    source_part: str = "xl/workbook.xml",
) -> str:
    """按 OPC URI 规则解析内部关系目标，并保证结果仍位于 ZIP 根内。"""

    raw_target = target.strip()
    if (
        raw_target != target
        or not raw_target
        or "\\" in raw_target
        or any(unicodedata.category(char).startswith("C") for char in raw_target)
        or re.search(r"%(?![0-9A-Fa-f]{2})", raw_target)
    ):
        raise ValueError("XLSX workbook contains an unsafe worksheet path")
    try:
        parsed = urlsplit(raw_target)
    except ValueError as exc:
        raise ValueError("XLSX workbook contains an unsafe worksheet path") from exc
    if parsed.scheme or parsed.netloc or parsed.query or parsed.fragment:
        raise ValueError("XLSX workbook contains an unsafe worksheet path")
    if re.search(r"%(?:2[fF]|5[cC]|0{2})", parsed.path):
        raise ValueError("XLSX workbook contains an unsafe worksheet path")
    try:
        decoded_path = unquote(parsed.path, encoding="utf-8", errors="strict")
    except UnicodeDecodeError as exc:
        raise ValueError("XLSX workbook contains an unsafe worksheet path") from exc
    if not decoded_path or "\\" in decoded_path or "\x00" in decoded_path or "%" in decoded_path:
        raise ValueError("XLSX workbook contains an unsafe worksheet path")

    parts = [] if decoded_path.startswith("/") else list(PurePosixPath(source_part).parent.parts)
    for part in decoded_path.lstrip("/").split("/"):
        if part in {"", "."}:
            continue
        if part == "..":
            if not parts:
                raise ValueError("XLSX workbook contains an unsafe worksheet path")
            parts.pop()
            continue
        if unicodedata.category(part[0]).startswith("C") or any(
            unicodedata.category(char).startswith("C") for char in part
        ):
            raise ValueError("XLSX workbook contains an unsafe worksheet path")
        parts.append(part)
    if not parts:
        raise ValueError("XLSX workbook contains an unsafe worksheet path")

    member_name = "/".join(parts)
    try:
        member = archive.getinfo(member_name)
    except KeyError as exc:
        raise ValueError(
            f"XLSX worksheet relationship target does not exist: {member_name}"
        ) from exc
    if member.is_dir():
        raise ValueError("XLSX worksheet relationship target must be a file")
    return member_name


def _rewrite_xlsx_workbook_relationships(
    raw: bytes,
    replacements: Mapping[str, str],
) -> bytes:
    """把已验证的 worksheet Target 改为解析库稳定支持的包内绝对路径。"""

    relationships_member = "xl/_rels/workbook.xml.rels"
    output = io.BytesIO()
    with zipfile.ZipFile(io.BytesIO(raw)) as source, zipfile.ZipFile(output, "w") as target:
        target.comment = source.comment
        for member in source.infolist():
            if member.filename == relationships_member:
                root = ET.fromstring(source.read(member))
                pending = dict(replacements)
                for element in root:
                    if _xml_local_name(element.tag) != "Relationship":
                        continue
                    relationship_id = element.attrib.get("Id")
                    if relationship_id in pending:
                        element.set("Target", pending.pop(relationship_id))
                if pending:
                    raise ValueError(
                        "XLSX workbook relationship changed during normalization"
                    )
                content = ET.tostring(root, encoding="utf-8", xml_declaration=True)
                target.writestr(member, content)
                continue
            if member.is_dir():
                target.writestr(member, b"")
                continue
            with source.open(member) as source_stream, target.open(
                member,
                "w",
                force_zip64=True,
            ) as target_stream:
                while chunk := source_stream.read(1024 * 1024):
                    target_stream.write(chunk)
    return output.getvalue()


def _xlsx_sheet_merge_ranges(
    raw: bytes,
) -> tuple[
    dict[str, tuple[tuple[int, int, int, int], ...]],
    dict[str, str],
]:
    """流式读取 XLSX 合并单元格，不把工作表 XML 整体载入内存。"""

    relationship_namespace = (
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
    )
    try:
        with zipfile.ZipFile(io.BytesIO(raw)) as archive:
            relationships: dict[str, tuple[str, str, str]] = {}
            with archive.open("xl/_rels/workbook.xml.rels") as stream:
                for _, element in ET.iterparse(stream, events=("end",)):
                    if _xml_local_name(element.tag) != "Relationship":
                        element.clear()
                        continue
                    relationship_id = element.attrib.get("Id")
                    target = element.attrib.get("Target")
                    target_mode = element.attrib.get("TargetMode", "Internal")
                    relationship_type = element.attrib.get("Type", "")
                    if relationship_id:
                        if relationship_id in relationships:
                            raise ValueError(
                                "XLSX workbook contains duplicate relationship identifiers"
                            )
                        relationships[relationship_id] = (
                            target or "",
                            target_mode,
                            relationship_type,
                        )
                    element.clear()

            sheet_paths: dict[str, str] = {}
            normalized_targets: dict[str, str] = {}
            with archive.open("xl/workbook.xml") as stream:
                for _, element in ET.iterparse(stream, events=("end",)):
                    if _xml_local_name(element.tag) != "sheet":
                        element.clear()
                        continue
                    title = element.attrib.get("name")
                    relationship_id = element.attrib.get(
                        f"{{{relationship_namespace}}}id"
                    )
                    if title and relationship_id:
                        relationship = relationships.get(relationship_id)
                        if relationship is None:
                            raise ValueError(
                                f"XLSX worksheet {title!r} references a missing relationship"
                            )
                        target, target_mode, relationship_type = relationship
                        if target_mode.strip().lower() != "internal":
                            raise ValueError(
                                f"XLSX worksheet {title!r} uses an external relationship"
                            )
                        if not relationship_type.endswith("/worksheet"):
                            raise ValueError(
                                f"XLSX worksheet {title!r} has an invalid relationship type"
                            )
                        member_name = _resolve_xlsx_relationship_target(
                            archive,
                            target,
                        )
                        sheet_paths[title] = member_name
                        canonical_target = f"/{member_name}"
                        if target != canonical_target:
                            normalized_targets[relationship_id] = canonical_target
                    element.clear()

            result: dict[str, tuple[tuple[int, int, int, int], ...]] = {}
            total_ranges = 0
            for title, member_name in sheet_paths.items():
                ranges: list[tuple[int, int, int, int]] = []
                with archive.open(member_name) as stream:
                    for _, element in ET.iterparse(stream, events=("end",)):
                        if _xml_local_name(element.tag) != "mergeCell":
                            element.clear()
                            continue
                        reference = element.attrib.get("ref")
                        if reference:
                            try:
                                boundaries = range_boundaries(reference)
                            except ValueError as exc:
                                raise ValueError(
                                    f"XLSX worksheet {title!r} has an invalid merged range"
                                ) from exc
                            ranges.append(boundaries)
                            total_ranges += 1
                            if total_ranges > _MAX_WORKBOOK_MERGED_RANGES:
                                raise ValueError(
                                    "XLSX workbook contains too many merged ranges "
                                    f"(limit {_MAX_WORKBOOK_MERGED_RANGES})"
                                )
                        element.clear()
                result[title] = tuple(ranges)
            return result, normalized_targets
    except (KeyError, ET.ParseError, zipfile.BadZipFile) as exc:
        raise ValueError(f"invalid XLSX workbook structure: {exc}") from exc


def _xlsx_header_end_row(
    first_row: int,
    rows: Mapping[int, Sequence[Any]],
    merged_ranges: Sequence[tuple[int, int, int, int]],
) -> int:
    """在固定深度内闭包表头合并关系，忽略越界或跨空行的可疑级联。"""

    header_end = first_row
    maximum_end = first_row + _MAX_WORKBOOK_HEADER_ROWS - 1
    has_header_hierarchy = any(
        max_column > min_column and min_row == first_row
        for min_column, min_row, max_column, _ in merged_ranges
    )
    changed = True
    while changed:
        changed = False
        for min_column, min_row, max_column, max_row in merged_ranges:
            if min_row < first_row or min_row > header_end or max_row < first_row:
                continue
            if max_column == min_column and not has_header_hierarchy:
                continue
            candidate = max_row + 1 if max_column > min_column else max_row
            if candidate <= header_end or candidate > maximum_end:
                continue
            if (
                max_column > min_column
                and len(_xlsx_nonempty_values(rows.get(min_row, ()))) < 2
                and len(_xlsx_nonempty_values(rows.get(candidate, ()))) < 2
            ):
                continue
            if any(
                not rows.get(row_number)
                for row_number in range(header_end + 1, candidate + 1)
            ):
                continue
            header_end = candidate
            changed = True
    return header_end


def _xlsx_headers(
    title: str,
    rows: Mapping[int, Sequence[Any]],
    first_row: int,
    header_end: int,
    merged_ranges: Sequence[tuple[int, int, int, int]],
) -> list[str]:
    width = max((len(row) for row in rows.values()), default=0)
    for min_column, min_row, max_column, max_row in merged_ranges:
        if min_row <= header_end and max_row >= first_row:
            width = max(width, max_column)
    if width > _MAX_WORKBOOK_COLUMNS:
        raise ValueError(
            f"XLSX worksheet {title!r} exceeds {_MAX_WORKBOOK_COLUMNS} columns"
        )

    matrix = [
        list(rows.get(row_number, ())) + [None] * (width - len(rows.get(row_number, ())))
        for row_number in range(first_row, header_end + 1)
    ]
    for min_column, min_row, max_column, max_row in merged_ranges:
        if min_row > header_end or max_row < first_row:
            continue
        source_row = max(first_row, min_row) - first_row
        source_column = min_column - 1
        source = matrix[source_row][source_column]
        for row_number in range(max(first_row, min_row), min(header_end, max_row) + 1):
            for column_number in range(min_column, max_column + 1):
                matrix[row_number - first_row][column_number - 1] = source

    headers: list[str] = []
    for column in range(width):
        components: list[str] = []
        for row in matrix:
            component = normalize_text(str(row[column] or ""))
            if component and (not components or component != components[-1]):
                components.append(component)
        header = ".".join(components)
        if not header:
            raise ValueError(f"XLSX worksheet {title!r} contains an empty header")
        headers.append(header)
    if len(set(headers)) != len(headers):
        raise ValueError(f"XLSX worksheet {title!r} contains duplicate headers")
    return headers


_XLSX_REPORT_METADATA_PATTERN = re.compile(
    r"^(?:报表|报告|标题|说明|备注|制表|统计|日期|时间|期间|"
    r"report|title|note|remark|date|time|period)\b",
    flags=re.IGNORECASE,
)


def _xlsx_nonempty_values(row: Sequence[Any]) -> list[Any]:
    return [value for value in row if value not in {None, ""}]


def _infer_xlsx_header_region(
    title: str,
    rows: Mapping[int, Sequence[Any]],
    merged_ranges: Sequence[tuple[int, int, int, int]],
) -> tuple[int, int, list[str]]:
    """从有界前缀中推断表头，区分报表说明、多级表头和正文合并。"""

    ordered_rows = sorted(rows)
    if not ordered_rows:
        return 0, 0, []
    first_nonempty_row = ordered_rows[0]
    horizontal_merge_rows = {
        min_row
        for min_column, min_row, max_column, _ in merged_ranges
        if max_column > min_column
    }
    candidates: list[tuple[float, int, int, list[str]]] = []
    for first_row in ordered_rows:
        raw_values = _xlsx_nonempty_values(rows[first_row])
        if not raw_values:
            continue
        if len(raw_values) < 2 and first_row in horizontal_merge_rows:
            continue

        header_end = _xlsx_header_end_row(first_row, rows, merged_ranges)
        header_rows = {
            row_number: rows[row_number]
            for row_number in range(first_row, header_end + 1)
            if row_number in rows
        }
        try:
            headers = _xlsx_headers(
                title,
                header_rows,
                first_row,
                header_end,
                merged_ranges,
            )
        except ValueError:
            if header_end == first_row:
                continue
            header_end = first_row
            try:
                headers = _xlsx_headers(
                    title,
                    {first_row: rows[first_row]},
                    first_row,
                    first_row,
                    merged_ranges,
                )
            except ValueError:
                continue

        data_rows = [rows[row_number] for row_number in ordered_rows if row_number > header_end]
        if any(len(row) > len(headers) for row in data_rows):
            continue

        text_ratio = sum(isinstance(value, str) for value in raw_values) / len(raw_values)
        score = text_ratio * 6 + min(len(raw_values), 4)
        if data_rows:
            first_data_values = _xlsx_nonempty_values(data_rows[0])
            score += 4 * min(len(first_data_values), len(headers)) / len(headers)
            if first_data_values:
                score += (
                    2
                    * sum(
                        not isinstance(value, str)
                        for value in first_data_values
                    )
                    / len(first_data_values)
                )
        first_label = normalize_text(str(raw_values[0]))
        if len(raw_values) <= 2 and _XLSX_REPORT_METADATA_PATTERN.match(first_label):
            score -= 8
        candidates.append((score, first_row, header_end, headers))

    if not candidates:
        first_row = first_nonempty_row
        headers = _xlsx_headers(
            title,
            {first_row: rows[first_row]},
            first_row,
            first_row,
            (),
        )
        return first_row, first_row, headers
    _, first_row, header_end, headers = max(
        candidates,
        key=lambda candidate: (candidate[0], -candidate[1]),
    )
    return first_row, header_end, headers


def _extract_xlsx_records(raw: bytes) -> list[dict[str, Any]]:
    _validate_office_archive(raw, "xlsx")
    merged_by_sheet, normalized_targets = _xlsx_sheet_merge_ranges(raw)
    workbook_raw = (
        _rewrite_xlsx_workbook_relationships(raw, normalized_targets)
        if normalized_targets
        else raw
    )
    try:
        workbook = load_workbook(
            io.BytesIO(workbook_raw),
            read_only=True,
            data_only=True,
            keep_links=False,
        )
    except Exception as exc:
        raise ValueError(f"invalid XLSX file: {exc}") from exc

    records: list[dict[str, Any]] = []
    total_cells = 0
    try:
        if len(workbook.worksheets) > _MAX_WORKBOOK_SHEETS:
            raise ValueError(
                f"XLSX contains too many worksheets (limit {_MAX_WORKBOOK_SHEETS})"
            )
        for worksheet in workbook.worksheets:
            reset_dimensions = getattr(worksheet, "reset_dimensions", None)
            if callable(reset_dimensions):
                reset_dimensions()
            merged_ranges = merged_by_sheet.get(worksheet.title, ())
            sheet_rows = 0
            scanned_rows = 0
            row_iterator = enumerate(
                worksheet.iter_rows(values_only=True),
                start=1,
            )
            buffered_rows: dict[int, Sequence[Any]] = {}

            def normalized_row_values(row: Sequence[Any]) -> list[Any]:
                values = list(row)
                while values and values[-1] in {None, ""}:
                    values.pop()
                if len(values) > _MAX_WORKBOOK_COLUMNS:
                    raise ValueError(
                        f"XLSX worksheet {worksheet.title!r} exceeds "
                        f"{_MAX_WORKBOOK_COLUMNS} columns"
                    )
                return values

            for row_number, row in row_iterator:
                scanned_rows += 1
                if scanned_rows > _MAX_WORKBOOK_SCANNED_ROWS:
                    raise ValueError(
                        f"XLSX worksheet {worksheet.title!r} exceeds "
                        f"{_MAX_WORKBOOK_SCANNED_ROWS} scanned rows"
                    )
                values = normalized_row_values(row)
                if not values or all(value in {None, ""} for value in values):
                    continue
                buffered_rows[row_number] = tuple(values)
                if len(buffered_rows) >= _MAX_WORKBOOK_HEADER_SCAN_ROWS:
                    break

            if not buffered_rows:
                continue
            _, header_end_row, headers = _infer_xlsx_header_region(
                worksheet.title,
                buffered_rows,
                merged_ranges,
            )

            def append_record(values: Sequence[Any]) -> None:
                nonlocal total_cells, sheet_rows
                row_values = list(values)
                if len(row_values) > len(headers):
                    raise ValueError(
                        f"XLSX worksheet {worksheet.title!r} has a row wider than its header"
                    )
                row_values.extend([None] * (len(headers) - len(row_values)))
                record = {
                    header: _normalize_spreadsheet_value(value)
                    for header, value in zip(headers, row_values, strict=True)
                }
                if not any(value not in {"", None} for value in record.values()):
                    return
                sheet_rows += 1
                total_cells += len(headers)
                if sheet_rows > _MAX_WORKBOOK_ROWS:
                    raise ValueError(
                        f"XLSX worksheet {worksheet.title!r} exceeds "
                        f"{_MAX_WORKBOOK_ROWS} data rows"
                    )
                if total_cells > _MAX_WORKBOOK_CELLS:
                    raise ValueError(
                        f"XLSX workbook exceeds {_MAX_WORKBOOK_CELLS} populated cells"
                    )
                records.append(record)

            for row_number, values in buffered_rows.items():
                if row_number > header_end_row:
                    append_record(values)

            for _, row in row_iterator:
                scanned_rows += 1
                if scanned_rows > _MAX_WORKBOOK_SCANNED_ROWS:
                    raise ValueError(
                        f"XLSX worksheet {worksheet.title!r} exceeds "
                        f"{_MAX_WORKBOOK_SCANNED_ROWS} scanned rows"
                    )
                values = normalized_row_values(row)
                if not values or all(value in {None, ""} for value in values):
                    continue
                append_record(values)
    finally:
        workbook.close()
    return records


def _normalize_value(value: Any) -> Any:
    if isinstance(value, str):
        return normalize_text(value)
    if isinstance(value, Mapping):
        return {normalize_text(str(key)): _normalize_value(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_normalize_value(item) for item in value]
    return value


def _record_from_value(value: Any) -> dict[str, Any]:
    if isinstance(value, Mapping):
        return dict(_normalize_value(value))
    return {"value": _normalize_value(value)}


def extract_structured_records(text: str, file_format: str) -> list[dict[str, Any]]:
    """从 JSON、JSONL 或 CSV 中提取规范化记录。

    JSON 顶层对象若包含 ``records/data/items/rows`` 数组，则提取该数组；
    其他顶层对象视为单条记录。标量会稳定包装为 ``{"value": ...}``。
    """

    normalized_format = _normalize_format(file_format)
    if normalized_format not in {"json", "jsonl", "csv"}:
        raise ValueError("structured record extraction only supports JSON, JSONL and CSV")

    normalized_text = normalize_text(text)
    if not normalized_text:
        return []

    if normalized_format == "json":
        try:
            payload = json.loads(normalized_text)
        except json.JSONDecodeError as exc:
            raise ValueError(f"invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}") from exc
        values: Sequence[Any]
        if isinstance(payload, list):
            values = payload
        elif isinstance(payload, Mapping):
            nested = next(
                (
                    payload[key]
                    for key in ("records", "data", "items", "rows")
                    if isinstance(payload.get(key), list)
                ),
                None,
            )
            values = nested if isinstance(nested, list) else [payload]
        else:
            values = [payload]
        return [_record_from_value(value) for value in values]

    if normalized_format == "jsonl":
        records: list[dict[str, Any]] = []
        for line_number, line in enumerate(normalized_text.splitlines(), start=1):
            if not line.strip():
                continue
            try:
                value = json.loads(line)
            except json.JSONDecodeError as exc:
                raise ValueError(
                    f"invalid JSONL at line {line_number}, "
                    f"column {exc.colno}: {exc.msg}"
                ) from exc
            records.append(_record_from_value(value))
        return records

    try:
        dialect = csv.Sniffer().sniff(normalized_text[:8192], delimiters=",\t;")
    except csv.Error:
        dialect = csv.excel
    reader = csv.DictReader(io.StringIO(normalized_text), dialect=dialect)
    if not reader.fieldnames:
        raise ValueError("CSV header is required")
    headers = [normalize_text(header or "") for header in reader.fieldnames]
    if any(not header for header in headers):
        raise ValueError("CSV header cannot be empty")
    if len(set(headers)) != len(headers):
        raise ValueError("CSV headers must be unique")
    reader.fieldnames = headers

    records = []
    for row in reader:
        if None in row:
            raise ValueError("CSV row has more fields than the header")
        normalized_row = {
            key: normalize_text(value or "")
            for key, value in row.items()
        }
        if any(value for value in normalized_row.values()):
            records.append(normalized_row)
    return records


def parse_text_content(
    raw: bytes | bytearray | memoryview | str,
    *,
    filename: str | None = None,
    file_format: str | None = None,
) -> ParsedText:
    """安全解析 UTF-8 文本、文本型 PDF 和现代 Office 文件。"""

    detected_format = detect_text_format(
        filename=filename,
        text="",
        file_format=file_format,
    )
    if detected_format in _OFFICE_OPEN_XML_FORMATS or detected_format == "pdf":
        binary = _binary_bytes(raw, detected_format)
        if detected_format == "pdf":
            text = _extract_pdf_text(binary)
            return ParsedText(format=detected_format, text=text, records=())
        if detected_format == "docx":
            text = _extract_docx_text(binary)
            return ParsedText(format=detected_format, text=text, records=())
        if detected_format == "pptx":
            text = _extract_pptx_text(binary)
            return ParsedText(format=detected_format, text=text, records=())

        records = _extract_xlsx_records(binary)
        text = "\n".join(
            json.dumps(record, ensure_ascii=False, separators=(",", ":"))
            for record in records
        )
        return ParsedText(
            format=detected_format,
            text=normalize_text(text),
            records=tuple(records),
        )

    text = normalize_text(decode_utf8(raw))
    detected_format = detect_text_format(filename=filename, text=text, file_format=file_format)
    records: list[dict[str, Any]] = []
    if detected_format in {"json", "jsonl", "csv"}:
        records = extract_structured_records(text, detected_format)
    return ParsedText(format=detected_format, text=text, records=tuple(records))


def desensitize_pii(text: str) -> tuple[str, dict[str, int]]:
    """掩码邮箱、手机号、身份证号及有明确上下文的姓名。"""

    if not isinstance(text, str):
        raise TypeError("text must be str")
    counts: dict[str, int] = {"email": 0, "phone": 0, "id_card": 0}

    def replace(pattern: re.Pattern[str], replacement: str, kind: str, value: str) -> str:
        def replacer(_: re.Match[str]) -> str:
            counts[kind] += 1
            return replacement

        return pattern.sub(replacer, value)

    masked = replace(_EMAIL_PATTERN, "[EMAIL]", "email", text)
    masked = replace(_ID_CARD_PATTERN, "[ID_CARD]", "id_card", masked)
    masked = replace(_PHONE_PATTERN, "[PHONE]", "phone", masked)

    def replace_context_name(match: re.Match[str]) -> str:
        counts["name"] = counts.get("name", 0) + 1
        return f"{match.group('label')}{match.group('separator')}[NAME]"

    masked = _CHINESE_NAME_CONTEXT_PATTERN.sub(replace_context_name, masked)
    masked = _ENGLISH_NAME_CONTEXT_PATTERN.sub(replace_context_name, masked)
    counts["total"] = sum(counts.values())
    return masked, counts


def _is_empty_value(value: Any) -> bool:
    if value is None:
        return True
    if isinstance(value, str):
        return not normalize_text(value)
    if isinstance(value, Mapping):
        return not value or all(_is_empty_value(item) for item in value.values())
    if isinstance(value, (list, tuple, set, frozenset)):
        return not value or all(_is_empty_value(item) for item in value)
    return False


def _canonical_value(value: Any) -> Any:
    if value is None or isinstance(value, (bool, int)):
        return value
    if isinstance(value, float):
        if math.isfinite(value):
            return value
        return str(value).lower()
    if isinstance(value, str):
        return normalize_text(value)
    if isinstance(value, (datetime, date, time)):
        return value.isoformat()
    if isinstance(value, Mapping):
        normalized: dict[str, Any] = {}
        for key, item in sorted(value.items(), key=lambda pair: str(pair[0])):
            normalized_key = normalize_text(str(key))
            if not normalized_key:
                raise ValueError("structured record contains an empty field name")
            if normalized_key in normalized:
                raise ValueError(
                    f"structured record fields collide after normalization: {normalized_key}"
                )
            normalized[normalized_key] = _canonical_value(item)
        return normalized
    if isinstance(value, (list, tuple)):
        return [_canonical_value(item) for item in value]
    if isinstance(value, (set, frozenset)):
        items = [_canonical_value(item) for item in value]
        return sorted(
            items,
            key=lambda item: json.dumps(
                item,
                ensure_ascii=False,
                sort_keys=True,
                separators=(",", ":"),
            ),
        )
    if isinstance(value, (bytes, bytearray, memoryview)):
        return bytes(value).hex()
    return normalize_text(str(value))


def canonical_record_json(record: Mapping[str, Any]) -> str:
    """生成与字段顺序无关、可用于比较和落库的 canonical JSON。"""

    if not isinstance(record, Mapping):
        raise TypeError("record must be a mapping")
    return json.dumps(
        _canonical_value(record),
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    )


def _normalize_field_name(value: Any, style: str) -> str:
    name = normalize_text(str(value))
    if style == "preserve":
        return name
    if style == "lower":
        return name.lower()
    if style != "snake_case":
        raise ValueError("field_name_style must be snake_case, lower or preserve")
    name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", name)
    name = re.sub(r"[^\w\u3400-\u4dbf\u4e00-\u9fff]+", "_", name, flags=re.UNICODE)
    return re.sub(r"_+", "_", name).strip("_").lower()


def normalize_structured_record(
    record: Mapping[str, Any],
    *,
    field_name_style: str = "snake_case",
) -> dict[str, Any]:
    """规范字段名、Unicode/空白、容器类型和不可 JSON 化的标量。"""

    if not isinstance(record, Mapping):
        raise TypeError("record must be a mapping")
    normalized: dict[str, Any] = {}
    for key, value in record.items():
        normalized_key = _normalize_field_name(key, field_name_style)
        if not normalized_key:
            raise ValueError("structured record contains an empty field name")
        if normalized_key in normalized:
            raise ValueError(
                f"structured record fields collide after normalization: {normalized_key}"
            )
        normalized[normalized_key] = _canonical_value(value)
    return dict(sorted(normalized.items()))


def _embedded_structure(value: Any) -> Any:
    if not isinstance(value, str):
        return value
    candidate = value.strip()
    if not candidate or candidate[0] not in "[{":
        return value
    try:
        parsed = json.loads(candidate)
    except json.JSONDecodeError:
        return value
    return parsed if isinstance(parsed, (Mapping, list)) else value


def flatten_structured_record(
    record: Mapping[str, Any],
    *,
    separator: str = ".",
) -> dict[str, Any]:
    """把嵌套对象展平；数组保留为 canonical JSON 兼容值。"""

    if not isinstance(record, Mapping):
        raise TypeError("record must be a mapping")
    if not separator:
        raise ValueError("separator cannot be empty")
    flattened: dict[str, Any] = {}

    def visit(value: Any, path: tuple[str, ...], depth: int) -> None:
        if depth > _MAX_STRUCTURED_DEPTH:
            raise ValueError(
                f"structured record nesting exceeds {_MAX_STRUCTURED_DEPTH} levels"
            )
        value = _embedded_structure(value)
        if isinstance(value, Mapping):
            if not value and path:
                key = separator.join(path)
                flattened[key] = {}
                return
            for child_key, child_value in value.items():
                normalized_key = normalize_text(str(child_key))
                if not normalized_key:
                    raise ValueError("structured record contains an empty field name")
                visit(child_value, (*path, normalized_key), depth + 1)
            return
        key = separator.join(path)
        if key in flattened:
            raise ValueError(f"structured record fields collide while flattening: {key}")
        flattened[key] = _canonical_value(value)
        if len(flattened) > _MAX_STRUCTURED_FIELDS:
            raise ValueError(
                f"structured record exceeds {_MAX_STRUCTURED_FIELDS} flattened fields"
            )

    for field, value in record.items():
        field_name = normalize_text(str(field))
        if not field_name:
            raise ValueError("structured record contains an empty field name")
        visit(value, (field_name,), 1)
    return flattened


def _clean_invalid_structured_records(
    records: Sequence[Mapping[str, Any]],
) -> list[dict[str, Any]]:
    if not records:
        return []
    fields: list[str] = []
    for record in records:
        for field in record:
            if field not in fields:
                fields.append(field)
    active_fields = [
        field
        for field in fields
        if any(not _is_empty_value(record.get(field)) for record in records)
    ]
    if not active_fields:
        return []
    identity_fields = [
        field
        for field in active_fields
        if _IDENTITY_FIELD_PATTERN.search(_normalize_field_name(field, "snake_case"))
    ]
    cleaned: list[dict[str, Any]] = []
    for record in records:
        values = {field: deepcopy(record.get(field)) for field in active_fields}
        # 空记录一定无效；存在身份字段时只把身份字段缺失视作“残缺行”，
        # 避免因为备注等可选列为空而误删有效业务数据。
        if all(_is_empty_value(value) for value in values.values()):
            continue
        if identity_fields and any(_is_empty_value(values[field]) for field in identity_fields):
            continue
        cleaned.append(values)
    return cleaned


def _percentile(values: Sequence[float], fraction: float) -> float:
    if not values:
        raise ValueError("cannot calculate a percentile of an empty sequence")
    ordered = sorted(values)
    position = (len(ordered) - 1) * fraction
    lower = math.floor(position)
    upper = math.ceil(position)
    if lower == upper:
        return ordered[lower]
    weight = position - lower
    return ordered[lower] * (1 - weight) + ordered[upper] * weight


def content_quality_flags(
    text: str,
    *,
    min_chars: int = 20,
    min_tokens: int = 5,
    max_chars: int = _MAX_ANOMALY_TEXT_CHARS,
) -> tuple[str, ...]:
    """返回非结构化内容的确定性低质量原因。"""

    if min_chars < 0 or min_tokens < 0 or max_chars <= 0:
        raise ValueError("content quality limits must be non-negative")
    normalized = normalize_text(text)
    if not normalized:
        return ("empty_content",)
    flags: list[str] = []
    if len(normalized) < min_chars or estimate_token_count(normalized) < min_tokens:
        flags.append("content_too_short")
    if len(normalized) > max_chars:
        flags.append("content_too_long")
    if any(marker in normalized for marker in _MOJIBAKE_MARKERS):
        flags.append("mojibake")
    nonspace = [char for char in normalized if not char.isspace()]
    if nonspace:
        readable_ratio = sum(
            char.isprintable()
            and unicodedata.category(char) not in {"Co", "Cs", "Cn"}
            for char in nonspace
        ) / len(nonspace)
        if readable_ratio < 0.85:
            flags.append("low_printable_ratio")
        if len(nonspace) >= 100:
            most_common = Counter(nonspace).most_common(1)[0][1]
            if most_common / len(nonspace) > 0.9:
                flags.append("repetitive_content")
    return tuple(dict.fromkeys(flags))


def is_low_quality_content(
    text: str,
    *,
    min_chars: int = 20,
    min_tokens: int = 5,
    max_chars: int = _MAX_ANOMALY_TEXT_CHARS,
) -> bool:
    """判断内容是否命中任一低质量规则。"""

    return bool(
        content_quality_flags(
            text,
            min_chars=min_chars,
            min_tokens=min_tokens,
            max_chars=max_chars,
        )
    )


def filter_anomalous_structured_records(
    records: Sequence[Mapping[str, Any]],
    *,
    iqr_multiplier: float = 1.5,
) -> list[dict[str, Any]]:
    """按字段级数值 IQR、乱码和极端文本长度过滤异常记录。"""

    if iqr_multiplier <= 0:
        raise ValueError("iqr_multiplier must be greater than 0")
    numeric_values: dict[str, list[float]] = {}
    text_lengths: dict[str, list[float]] = {}
    for record in records:
        for field, value in record.items():
            if (
                isinstance(value, (int, float))
                and not isinstance(value, bool)
                and not _IDENTITY_FIELD_PATTERN.search(
                    _normalize_field_name(field, "snake_case")
                )
            ):
                number = float(value)
                if math.isfinite(number):
                    numeric_values.setdefault(field, []).append(number)
            elif isinstance(value, str) and value:
                text_lengths.setdefault(field, []).append(float(len(value)))

    numeric_bounds: dict[str, tuple[float, float]] = {}
    for field, values in numeric_values.items():
        # 小样本不做统计异常判断，避免把合法长尾值误删。
        if len(values) < 8:
            continue
        first_quartile = _percentile(values, 0.25)
        third_quartile = _percentile(values, 0.75)
        spread = third_quartile - first_quartile
        numeric_bounds[field] = (
            first_quartile - iqr_multiplier * spread,
            third_quartile + iqr_multiplier * spread,
        )

    text_upper_bounds: dict[str, float] = {}
    for field, lengths in text_lengths.items():
        if len(lengths) < 8:
            continue
        first_quartile = _percentile(lengths, 0.25)
        third_quartile = _percentile(lengths, 0.75)
        spread = third_quartile - first_quartile
        text_upper_bounds[field] = max(512.0, third_quartile + 3 * spread)

    accepted: list[dict[str, Any]] = []
    for record in records:
        anomalous = False
        for field, value in record.items():
            if (
                isinstance(value, (int, float))
                and not isinstance(value, bool)
                and not _IDENTITY_FIELD_PATTERN.search(
                    _normalize_field_name(field, "snake_case")
                )
            ):
                number = float(value)
                if not math.isfinite(number):
                    anomalous = True
                    break
                bounds = numeric_bounds.get(field)
                if bounds and not bounds[0] <= number <= bounds[1]:
                    anomalous = True
                    break
            if isinstance(value, str):
                flags = content_quality_flags(value, min_chars=0, min_tokens=0)
                if {"content_too_long", "mojibake", "low_printable_ratio"} & set(flags):
                    anomalous = True
                    break
                upper_bound = text_upper_bounds.get(field)
                if upper_bound is not None and len(value) > upper_bound:
                    anomalous = True
                    break
        if not anomalous:
            accepted.append(deepcopy(dict(record)))
    return accepted


def _identity_tokens(record: Mapping[str, Any]) -> set[tuple[str, str]]:
    tokens: set[tuple[str, str]] = set()
    for field, value in record.items():
        normalized_field = _normalize_field_name(field, "snake_case")
        if not _IDENTITY_FIELD_PATTERN.search(normalized_field) or _is_empty_value(value):
            continue
        tokens.add(
            (
                normalized_field,
                json.dumps(
                    _canonical_value(value),
                    ensure_ascii=False,
                    sort_keys=True,
                    separators=(",", ":"),
                ),
            )
        )
    return tokens


def deduplicate_structured_records(
    records: Sequence[Mapping[str, Any]],
) -> list[dict[str, Any]]:
    """按整条 canonical JSON 和 id/uuid/key/code/*_id 字段稳定去重。"""

    exact_seen: set[str] = set()
    identity_seen: set[tuple[str, str]] = set()
    unique: list[dict[str, Any]] = []
    for record in records:
        fingerprint = hashlib.sha256(canonical_record_json(record).encode("utf-8")).hexdigest()
        identities = _identity_tokens(record)
        if fingerprint in exact_seen or identities & identity_seen:
            continue
        exact_seen.add(fingerprint)
        identity_seen.update(identities)
        unique.append(deepcopy(dict(record)))
    return unique


def _is_name_field(field: Any) -> bool:
    normalized = _normalize_field_name(field, "snake_case")
    if normalized in _NAME_FIELD_NAMES:
        return True
    suffixes = ("_name", "_full_name", "_姓名", "_真实姓名", "_联系人", "_联系人姓名")
    return normalized.endswith(suffixes)


def desensitize_structured_record(
    record: Mapping[str, Any],
) -> tuple[dict[str, Any], dict[str, int]]:
    """递归脱敏结构化姓名字段及任意文本中的手机号、邮箱、身份证号。"""

    if not isinstance(record, Mapping):
        raise TypeError("record must be a mapping")
    counts: dict[str, int] = {"email": 0, "phone": 0, "id_card": 0}

    def add_counts(values: Mapping[str, int]) -> None:
        for kind, count in values.items():
            if kind != "total" and count:
                counts[kind] = counts.get(kind, 0) + count

    def visit(value: Any, field: Any = "") -> Any:
        if isinstance(value, Mapping):
            return {key: visit(item, key) for key, item in value.items()}
        if isinstance(value, list):
            return [visit(item, field) for item in value]
        if isinstance(value, tuple):
            return [visit(item, field) for item in value]
        if _is_name_field(field) and not _is_empty_value(value):
            counts["name"] = counts.get("name", 0) + 1
            return "[NAME]"
        if isinstance(value, str):
            masked, found = desensitize_pii(value)
            add_counts(found)
            return masked
        return deepcopy(value)

    masked = {key: visit(value, key) for key, value in record.items()}
    counts["total"] = sum(counts.values())
    return masked, counts


def _structured_options(options: Iterable[str] | Mapping[str, Any]) -> set[str]:
    if isinstance(options, str):
        raise TypeError("options must be an iterable or mapping of option names")
    if isinstance(options, Mapping):
        enabled = {str(key) for key, value in options.items() if bool(value)}
    else:
        enabled = {str(option) for option in options}
    unknown = enabled - _STRUCTURED_OPTIONS
    if unknown:
        raise ValueError(f"unsupported structured preprocess options: {', '.join(sorted(unknown))}")
    return enabled


def preprocess_structured_records(
    records: Iterable[Mapping[str, Any]],
    options: Iterable[str] | Mapping[str, Any],
) -> list[dict[str, Any]]:
    """按界面选项执行确定性、无副作用的结构化数据预处理。"""

    enabled = _structured_options(options)
    current: list[dict[str, Any]] = []
    for record in records:
        if not isinstance(record, Mapping):
            if "clean_invalid" in enabled:
                continue
            raise TypeError("structured records must contain mappings")
        value = deepcopy(dict(record))
        if "detect_structure" in enabled:
            value = flatten_structured_record(value)
        if "normalize_format" in enabled:
            value = normalize_structured_record(value)
        current.append(value)

    if "clean_invalid" in enabled:
        current = _clean_invalid_structured_records(current)
    if "filter_anomaly" in enabled:
        current = filter_anomalous_structured_records(current)
    if "deduplicate" in enabled:
        current = deduplicate_structured_records(current)
    if "desensitize" in enabled:
        current = [desensitize_structured_record(record)[0] for record in current]
    return current


def estimate_token_count(text: str) -> int:
    """无分词器依赖的确定性 token 估算，用于预览与保护性限流。"""

    return len(_TOKEN_PATTERN.findall(text))


def _protected_markdown_ranges(
    text: str,
    *,
    preserve_code_blocks: bool,
    preserve_tables: bool,
    preserve_lists: bool,
) -> list[tuple[int, int]]:
    """找出不应从中间切开的 Markdown 代码块、表格和连续列表。"""

    lines: list[tuple[int, int, str]] = []
    cursor = 0
    for raw_line in text.splitlines(keepends=True):
        end = cursor + len(raw_line)
        lines.append((cursor, end, raw_line.rstrip("\r\n")))
        cursor = end
    if cursor < len(text) or not lines:
        lines.append((cursor, len(text), text[cursor:]))

    ranges: list[tuple[int, int]] = []
    code_line_indexes: set[int] = set()
    if preserve_code_blocks:
        open_block: tuple[int, str, int] | None = None
        for index, (start, end, content) in enumerate(lines):
            fence = re.match(r"^\s*(`{3,}|~{3,})", content)
            if not fence:
                continue
            marker = fence.group(1)[0]
            length = len(fence.group(1))
            if open_block is None:
                open_block = (index, marker, length)
                continue
            first_index, open_marker, open_length = open_block
            if marker == open_marker and length >= open_length:
                ranges.append((lines[first_index][0], end))
                code_line_indexes.update(range(first_index, index + 1))
                open_block = None
        if open_block is not None:
            first_index = open_block[0]
            ranges.append((lines[first_index][0], len(text)))
            code_line_indexes.update(range(first_index, len(lines)))

    if preserve_tables:
        index = 0
        while index + 1 < len(lines):
            if index in code_line_indexes:
                index += 1
                continue
            header = lines[index][2].strip()
            separator = lines[index + 1][2].strip().strip("|")
            cells = [cell.strip() for cell in separator.split("|")]
            if (
                "|" not in header
                or len(cells) < 2
                or not all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells)
            ):
                index += 1
                continue
            end_index = index + 1
            while (
                end_index + 1 < len(lines)
                and end_index + 1 not in code_line_indexes
                and lines[end_index + 1][2].strip()
                and "|" in lines[end_index + 1][2]
            ):
                end_index += 1
            ranges.append((lines[index][0], lines[end_index][1]))
            index = end_index + 1

    if preserve_lists:
        list_pattern = re.compile(r"^\s*(?:[-+*]|\d+[.)])\s+\S")
        continuation_pattern = re.compile(r"^\s{2,}\S")
        index = 0
        while index < len(lines):
            if index in code_line_indexes or not list_pattern.match(lines[index][2]):
                index += 1
                continue
            end_index = index
            item_count = 1
            while end_index + 1 < len(lines) and end_index + 1 not in code_line_indexes:
                next_line = lines[end_index + 1][2]
                if list_pattern.match(next_line):
                    item_count += 1
                    end_index += 1
                elif continuation_pattern.match(next_line):
                    end_index += 1
                else:
                    break
            if item_count >= 2:
                ranges.append((lines[index][0], lines[end_index][1]))
            index = end_index + 1

    merged: list[tuple[int, int]] = []
    for start, end in sorted(ranges):
        if merged and start < merged[-1][1]:
            merged[-1] = (merged[-1][0], max(merged[-1][1], end))
        else:
            merged.append((start, end))
    return merged


def protected_context_ranges(
    text: str,
    *,
    preserve_code_blocks: bool = True,
    preserve_tables: bool = True,
    preserve_lists: bool = True,
) -> tuple[tuple[int, int], ...]:
    """返回代码块、表格和列表的不可拆分区间。

    返回的偏移量基于规范化后的文本；调用方应在同一份 ``normalize_text``
    结果上使用这些区间。
    """

    normalized = normalize_text(text)
    return tuple(
        _protected_markdown_ranges(
            normalized,
            preserve_code_blocks=preserve_code_blocks,
            preserve_tables=preserve_tables,
            preserve_lists=preserve_lists,
        )
    )


def expand_to_context_boundaries(
    text: str,
    start: int,
    end: int,
    *,
    preserve_paragraph: bool = True,
    preserve_code_blocks: bool = True,
    preserve_tables: bool = True,
    preserve_lists: bool = True,
) -> tuple[int, int]:
    """将一个文本区间扩展到段落及受保护 Markdown 结构边界。"""

    normalized = normalize_text(text)
    if isinstance(start, bool) or isinstance(end, bool):
        raise TypeError("start and end must be integers")
    if not isinstance(start, int) or not isinstance(end, int):
        raise TypeError("start and end must be integers")
    if not 0 <= start <= end <= len(normalized):
        raise ValueError("start and end must define a valid normalized text range")

    expanded_start = start
    expanded_end = end
    if preserve_paragraph and normalized:
        paragraph_start = normalized.rfind("\n\n", 0, start)
        expanded_start = 0 if paragraph_start < 0 else paragraph_start + 2
        paragraph_end = normalized.find("\n\n", end)
        expanded_end = len(normalized) if paragraph_end < 0 else paragraph_end

    ranges = protected_context_ranges(
        normalized,
        preserve_code_blocks=preserve_code_blocks,
        preserve_tables=preserve_tables,
        preserve_lists=preserve_lists,
    )
    changed = True
    while changed:
        changed = False
        for range_start, range_end in ranges:
            overlaps = range_start < expanded_end and range_end > expanded_start
            contains_boundary = (
                range_start <= expanded_start < range_end
                or range_start < expanded_end <= range_end
            )
            if not overlaps and not contains_boundary:
                continue
            next_start = min(expanded_start, range_start)
            next_end = max(expanded_end, range_end)
            if (next_start, next_end) != (expanded_start, expanded_end):
                expanded_start, expanded_end = next_start, next_end
                changed = True
    return expanded_start, expanded_end


def detect_document_structure(text: str) -> DocumentStructure:
    """识别 Markdown、中文章节和数字编号标题及不可拆分块。"""

    normalized = normalize_text(text)
    if not normalized:
        return DocumentStructure(
            line_count=0,
            paragraph_count=0,
            headings=(),
            code_block_count=0,
            table_block_count=0,
            list_block_count=0,
        )

    code_ranges = protected_context_ranges(
        normalized,
        preserve_code_blocks=True,
        preserve_tables=False,
        preserve_lists=False,
    )
    table_candidates = protected_context_ranges(
        normalized,
        preserve_code_blocks=False,
        preserve_tables=True,
        preserve_lists=False,
    )
    list_candidates = protected_context_ranges(
        normalized,
        preserve_code_blocks=False,
        preserve_tables=False,
        preserve_lists=True,
    )
    table_ranges = tuple(
        item
        for item in table_candidates
        if not any(
            item[0] < code_end and item[1] > code_start
            for code_start, code_end in code_ranges
        )
    )
    list_ranges = tuple(
        item
        for item in list_candidates
        if not any(
            item[0] < code_end and item[1] > code_start
            for code_start, code_end in code_ranges
        )
    )
    markdown_heading = re.compile(r"^\s*(?P<marks>#{1,6})\s+(?P<title>.+?)\s*#*\s*$")
    chinese_heading = re.compile(
        r"^\s*(?P<title>第[一二三四五六七八九十百千万0-9]+[章节篇部分].*)$"
    )
    numbered_heading = re.compile(
        r"^\s*(?P<number>\d+(?:\.\d+)*)(?:[、.]|\s+)\s*(?P<title>\S.*)$"
    )

    headings: list[DocumentHeading] = []
    cursor = 0
    for line_number, raw_line in enumerate(normalized.splitlines(keepends=True), start=1):
        line = raw_line.rstrip("\n")
        line_end = cursor + len(line)
        if not any(range_start <= cursor < range_end for range_start, range_end in code_ranges):
            match = markdown_heading.match(line)
            if match:
                headings.append(
                    DocumentHeading(
                        level=len(match.group("marks")),
                        title=normalize_text(match.group("title")),
                        line_number=line_number,
                        start=cursor,
                        end=line_end,
                    )
                )
            else:
                match = chinese_heading.match(line)
                if match:
                    headings.append(
                        DocumentHeading(
                            level=1,
                            title=normalize_text(match.group("title")),
                            line_number=line_number,
                            start=cursor,
                            end=line_end,
                        )
                    )
                else:
                    match = numbered_heading.match(line)
                    if match:
                        headings.append(
                            DocumentHeading(
                                level=min(6, match.group("number").count(".") + 1),
                                title=normalize_text(match.group("title")),
                                line_number=line_number,
                                start=cursor,
                                end=line_end,
                            )
                        )
        cursor += len(raw_line)

    paragraphs = [part for part in re.split(r"\n\s*\n", normalized) if part.strip()]
    return DocumentStructure(
        line_count=len(normalized.splitlines()),
        paragraph_count=len(paragraphs),
        headings=tuple(headings),
        code_block_count=len(code_ranges),
        table_block_count=len(table_ranges),
        list_block_count=len(list_ranges),
    )


def merge_short_blocks(
    blocks: Iterable[str],
    *,
    min_token_count: int = 100,
    separator: str = "\n\n",
) -> list[str]:
    """按原顺序合并短内容块，并把末尾残块归入前一块。"""

    if isinstance(min_token_count, bool) or not isinstance(min_token_count, int):
        raise TypeError("min_token_count must be an integer")
    if min_token_count <= 0:
        raise ValueError("min_token_count must be greater than 0")
    if not isinstance(separator, str):
        raise TypeError("separator must be str")

    merged: list[str] = []
    pending: list[str] = []
    pending_tokens = 0
    for block in blocks:
        if not isinstance(block, str):
            raise TypeError("blocks must contain strings")
        normalized = normalize_text(block)
        if not normalized:
            continue
        token_count = estimate_token_count(normalized)
        if not pending and token_count >= min_token_count:
            merged.append(normalized)
            continue
        pending.append(normalized)
        pending_tokens += token_count
        if pending_tokens >= min_token_count:
            merged.append(separator.join(pending))
            pending = []
            pending_tokens = 0

    if pending:
        tail = separator.join(pending)
        if merged:
            merged[-1] = separator.join((merged[-1], tail))
        else:
            merged.append(tail)
    return merged


def _near_duplicate_features(text: str, shingle_size: int) -> tuple[str, ...]:
    if isinstance(shingle_size, bool) or not isinstance(shingle_size, int):
        raise TypeError("shingle_size must be an integer")
    if shingle_size <= 0:
        raise ValueError("shingle_size must be greater than 0")
    tokens = re.findall(
        r"[\u3400-\u4dbf\u4e00-\u9fff]|[A-Za-z0-9_]+",
        normalize_text(text).casefold(),
    )
    if not tokens:
        return ()
    if len(tokens) < shingle_size:
        return ("\x1f".join(tokens),)
    return tuple(
        "\x1f".join(tokens[index : index + shingle_size])
        for index in range(len(tokens) - shingle_size + 1)
    )


def near_duplicate_fingerprint(text: str, *, shingle_size: int = 3) -> str:
    """生成 64 位 SimHash 指纹，用于低成本近重复候选筛选。"""

    if isinstance(shingle_size, bool) or not isinstance(shingle_size, int):
        raise TypeError("shingle_size must be an integer")
    if shingle_size <= 0:
        raise ValueError("shingle_size must be greater than 0")
    features = Counter(_near_duplicate_features(text, shingle_size))
    if not features:
        return "0" * 16
    vector = [0] * 64
    for feature, weight in features.items():
        digest = int.from_bytes(hashlib.sha256(feature.encode("utf-8")).digest()[:8], "big")
        for bit in range(64):
            vector[bit] += weight if digest & (1 << bit) else -weight
    fingerprint = sum(1 << bit for bit, value in enumerate(vector) if value >= 0)
    return f"{fingerprint:016x}"


def fingerprints_are_near_duplicate(
    left: str,
    right: str,
    *,
    max_hamming_distance: int = 3,
) -> bool:
    """比较两个 64 位十六进制 SimHash 指纹。"""

    if isinstance(max_hamming_distance, bool) or not isinstance(max_hamming_distance, int):
        raise TypeError("max_hamming_distance must be an integer")
    if not 0 <= max_hamming_distance <= 64:
        raise ValueError("max_hamming_distance must be in [0, 64]")
    if not re.fullmatch(r"[0-9a-fA-F]{16}", left) or not re.fullmatch(
        r"[0-9a-fA-F]{16}", right
    ):
        raise ValueError("fingerprints must be 16-character hexadecimal strings")
    distance = (int(left, 16) ^ int(right, 16)).bit_count()
    return distance <= max_hamming_distance


def is_near_duplicate(
    left: str,
    right: str,
    *,
    shingle_size: int = 3,
    similarity_threshold: float = 0.9,
    max_hamming_distance: int = 3,
) -> bool:
    """结合词片 Jaccard 和 SimHash 判断两段内容是否近重复。"""

    if isinstance(similarity_threshold, bool) or not isinstance(
        similarity_threshold, (int, float)
    ):
        raise TypeError("similarity_threshold must be a number")
    if not 0 <= similarity_threshold <= 1:
        raise ValueError("similarity_threshold must be in [0, 1]")
    if isinstance(max_hamming_distance, bool) or not isinstance(max_hamming_distance, int):
        raise TypeError("max_hamming_distance must be an integer")
    if not 0 <= max_hamming_distance <= 64:
        raise ValueError("max_hamming_distance must be in [0, 64]")
    left_normalized = normalize_text(left)
    right_normalized = normalize_text(right)
    if not left_normalized or not right_normalized:
        return left_normalized == right_normalized
    if left_normalized.casefold() == right_normalized.casefold():
        return True
    left_features = set(_near_duplicate_features(left_normalized, shingle_size))
    right_features = set(_near_duplicate_features(right_normalized, shingle_size))
    union = left_features | right_features
    similarity = len(left_features & right_features) / len(union) if union else 1.0
    if similarity >= similarity_threshold:
        return True
    return fingerprints_are_near_duplicate(
        near_duplicate_fingerprint(left_normalized, shingle_size=shingle_size),
        near_duplicate_fingerprint(right_normalized, shingle_size=shingle_size),
        max_hamming_distance=max_hamming_distance,
    )


def record_fingerprint(record: Mapping[str, Any]) -> str:
    """计算与字典键顺序无关的稳定记录指纹。"""

    canonical = {
        "instruction": normalize_text(str(record.get("instruction") or "")),
        "input": normalize_text(str(record.get("input") or "")),
        "output": normalize_text(str(record.get("output") or "")),
    }
    raw = json.dumps(canonical, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


def _readability_score(text: str) -> float:
    if not text:
        return 0.0
    nonspace = [char for char in text if not char.isspace()]
    if not nonspace:
        return 0.0
    printable_ratio = sum(char.isprintable() for char in nonspace) / len(nonspace)
    useful_ratio = sum(
        char.isalnum() or "\u3400" <= char <= "\u9fff" or unicodedata.category(char).startswith("P")
        for char in nonspace
    ) / len(nonspace)
    return round(100 * (0.65 * printable_ratio + 0.35 * useful_ratio), 2)


def _internal_duplicate_score(text: str) -> float:
    units = [unit.strip().lower() for unit in re.split(r"[\n。！？!?；;]+", text) if unit.strip()]
    if len(units) <= 1:
        return 100.0
    return round(100 * len(set(units)) / len(units), 2)


def _source_relevance_score(record: Mapping[str, Any], source_content: str) -> float:
    """估算结果与来源文本的词元覆盖率。

    这是无外部模型依赖、可重复的首版评分。没有来源文本（例如人工新增结果）
    时不扣分；存在来源时，以结果中的有效词元被来源覆盖的比例计分。
    """

    source = normalize_text(source_content)
    if not source:
        return 100.0
    candidate = normalize_text(
        "\n".join(
            str(record.get(field) or "") for field in ("instruction", "input", "output")
        )
    )

    def semantic_tokens(text: str) -> set[str]:
        return {
            token.lower()
            for token in _TOKEN_PATTERN.findall(text)
            if token.isalnum() or "\u3400" <= token <= "\u9fff"
        }

    source_tokens = semantic_tokens(source)
    candidate_tokens = semantic_tokens(candidate)
    if not candidate_tokens:
        return 0.0
    if not source_tokens:
        return 0.0
    return round(100 * len(candidate_tokens & source_tokens) / len(candidate_tokens), 2)


def score_quality(
    record: Mapping[str, Any],
    *,
    min_output_length: int = 20,
    source_content: str = "",
    known_fingerprints: Iterable[str] = (),
    threshold: float = 60.0,
) -> QualityScore:
    """按完整性、长度、可读性、来源相关性和重复度计算质量分。"""

    if min_output_length <= 0:
        raise ValueError("min_output_length must be greater than 0")
    if not 0 <= threshold <= 100:
        raise ValueError("threshold must be in [0, 100]")

    instruction = normalize_text(str(record.get("instruction") or ""))
    input_text = normalize_text(str(record.get("input") or ""))
    output = normalize_text(str(record.get("output") or ""))
    flags: list[str] = []

    completeness = 100.0
    if not instruction:
        completeness -= 50
        flags.append("missing_instruction")
    if not output:
        completeness -= 50
        flags.append("missing_output")

    output_length = len(output)
    length_score = round(min(100.0, output_length / min_output_length * 100), 2)
    if output_length < min_output_length:
        flags.append("output_too_short")

    readability = _readability_score("\n".join((instruction, input_text, output)))
    if readability < 70:
        flags.append("low_readability")

    relevance = _source_relevance_score(record, source_content)
    if source_content and relevance < 30:
        flags.append("low_source_relevance")

    fingerprint = record_fingerprint(record)
    known = set(known_fingerprints)
    duplicate = 0.0 if fingerprint in known else _internal_duplicate_score(output)
    if duplicate == 0:
        flags.append("duplicate_record")
    elif duplicate < 70:
        flags.append("repetitive_output")

    overall = round(
        completeness * 0.35
        + length_score * 0.20
        + readability * 0.20
        + relevance * 0.15
        + duplicate * 0.10,
        2,
    )
    hard_valid = bool(instruction and output)
    return QualityScore(
        overall=overall,
        completeness=completeness,
        length=length_score,
        readability=readability,
        relevance=relevance,
        duplicate=duplicate,
        is_valid=hard_valid and overall >= threshold,
        flags=tuple(flags),
        fingerprint=fingerprint,
    )


def stable_split(
    value: str | int,
    split: Mapping[str, int] | None = None,
    *,
    seed: str = "",
) -> DatasetSplit:
    """按稳定哈希将记录划分到 train/validation/test。"""

    ratios = dict(split or {"train": 80, "validation": 10, "test": 10})
    required = {"train", "validation", "test"}
    if set(ratios) != required:
        raise ValueError("split must contain exactly train, validation and test")
    if any(isinstance(value, bool) or not isinstance(value, int) or value < 0 for value in ratios.values()):
        raise ValueError("split ratios must be non-negative integers")
    if sum(ratios.values()) != 100:
        raise ValueError("split ratios must sum to 100")

    digest = hashlib.sha256(f"{seed}:{value}".encode("utf-8")).digest()
    bucket = int.from_bytes(digest[:8], "big") % 10_000
    train_boundary = ratios["train"] * 100
    validation_boundary = train_boundary + ratios["validation"] * 100
    if bucket < train_boundary:
        return "train"
    if bucket < validation_boundary:
        return "validation"
    return "test"


def stable_split_assignments(
    values: Sequence[str | int],
    split: Mapping[str, int] | None = None,
    *,
    seed: str = "",
) -> list[DatasetSplit]:
    """按稳定顺序和精确配额批量划分数据集。

    单条哈希分桶只能在大样本下近似比例。这里先按哈希稳定排序，再用
    最大余数法计算各切分配额，确保小数据集也严格遵循配置比例。
    """

    ratios = dict(split or {"train": 80, "validation": 10, "test": 10})
    # 复用单条划分的参数校验，避免两套规则逐渐漂移。
    stable_split("validation", ratios, seed=seed)
    if not values:
        return []

    split_order: tuple[DatasetSplit, ...] = ("train", "validation", "test")
    exact = {name: len(values) * ratios[name] / 100 for name in split_order}
    quotas = {name: math.floor(exact[name]) for name in split_order}
    remaining = len(values) - sum(quotas.values())
    remainder_order = sorted(
        split_order,
        key=lambda name: (-(exact[name] - quotas[name]), split_order.index(name)),
    )
    for name in remainder_order[:remaining]:
        quotas[name] += 1

    ranked_indices = sorted(
        range(len(values)),
        key=lambda index: (
            hashlib.sha256(f"{seed}:{values[index]}".encode("utf-8")).digest(),
            index,
        ),
    )
    assignments: list[DatasetSplit] = ["train"] * len(values)
    cursor = 0
    for name in split_order:
        for index in ranked_indices[cursor : cursor + quotas[name]]:
            assignments[index] = name
        cursor += quotas[name]
    return assignments


def _preview_content(item: Mapping[str, Any]) -> str:
    for field in ("edited_content", "editedContent", "original_content", "originalContent", "content"):
        value = item.get(field)
        if value is not None:
            return normalize_text(str(value))
    return ""


def _standard_fields(content: str) -> tuple[str, str, str]:
    if not content:
        return "", "", ""

    try:
        payload = json.loads(content)
    except json.JSONDecodeError:
        payload = None
    if isinstance(payload, Mapping):
        instruction = next(
            (
                str(payload[key])
                for key in ("instruction", "question", "prompt")
                if payload.get(key) is not None
            ),
            "",
        )
        input_text = next(
            (str(payload[key]) for key in ("input", "context") if payload.get(key) is not None),
            "",
        )
        output = next(
            (str(payload[key]) for key in ("output", "answer", "response") if payload.get(key) is not None),
            "",
        )
        if instruction or output:
            return normalize_text(instruction), normalize_text(input_text), normalize_text(output)

    question_answer = re.match(
        r"^\s*(?:问|question)\s*[:：]\s*(.+?)(?:\n|\r\n?)\s*(?:答|answer)\s*[:：]\s*(.+)\s*$",
        content,
        flags=re.IGNORECASE | re.DOTALL,
    )
    if question_answer:
        return normalize_text(question_answer.group(1)), "", normalize_text(question_answer.group(2))

    lines = [line.strip() for line in content.splitlines() if line.strip()]
    first_line = re.sub(r"^(?:问|question)\s*[:：]\s*", "", lines[0], flags=re.IGNORECASE)
    output = normalize_text("\n".join(lines[1:])) if len(lines) > 1 else normalize_text(content)
    return normalize_text(first_line), "", output


def generate_standard_records(
    preview_items: Iterable[Mapping[str, Any]],
    *,
    qa_pairs_per_item: int = 1,
    semantic_enrichment: bool = False,
    split: Mapping[str, int] | None = None,
    split_seed: str = "",
) -> list[dict[str, Any]]:
    """把预览内容确定性转换为标准 instruction/input/output 记录。

    该函数只负责本地标准化，不冒充 LLM；服务层可将其作为无模型模式或
    LLM 响应解析后的统一落库步骤。
    """

    if not 1 <= qa_pairs_per_item <= MAX_QA_PAIRS_PER_ITEM:
        raise ValueError(
            f"qa_pairs_per_item must be in [1, {MAX_QA_PAIRS_PER_ITEM}]"
        )
    prefixes = (
        "请结合实际情况说明：",
        "请用通俗易懂的方式说明：",
        "请从实际应用角度说明：",
        "请简洁自然地说明：",
        "请详细解答：",
    )
    results: list[dict[str, Any]] = []
    for item_index, item in enumerate(preview_items):
        content = _preview_content(item)
        instruction, input_text, output = _standard_fields(content)
        preview_id = str(item.get("id") or f"preview-{item_index + 1}")
        for variant_index in range(qa_pairs_per_item):
            variant_instruction = instruction
            if variant_index:
                if semantic_enrichment:
                    prefix = prefixes[variant_index % len(prefixes)]
                    if variant_index >= len(prefixes):
                        prefix = (
                            f"{prefix.removesuffix('：')}"
                            f"（问法 {variant_index + 1}）："
                        )
                    variant_instruction = f"{prefix}{instruction}"
                else:
                    variant_instruction = f"{instruction}(问法 {variant_index + 1})"
            raw_id = f"{preview_id}:{variant_index + 1}"
            result_id = f"result_{hashlib.sha256(raw_id.encode('utf-8')).hexdigest()[:16]}"
            status = "valid" if variant_instruction and output else "invalid"
            results.append(
                {
                    "id": result_id,
                    "preview_item_id": preview_id,
                    "instruction": variant_instruction,
                    "input": input_text,
                    "output": output,
                    "original_instruction": variant_instruction,
                    "original_input": input_text,
                    "original_output": output,
                    "status": status,
                    "split": "train",
                }
            )
    assignments = stable_split_assignments(
        [str(result["id"]) for result in results],
        split,
        seed=split_seed,
    )
    for result, assignment in zip(results, assignments, strict=True):
        result["split"] = assignment
    return results


__all__ = [
    "DatasetSplit",
    "DocumentHeading",
    "DocumentNoiseSpan",
    "DocumentStructure",
    "ParsedText",
    "PdfPageText",
    "QualityScore",
    "SUPPORTED_TEXT_FORMATS",
    "StructuredPreprocessOption",
    "TextFormat",
    "canonical_record_json",
    "content_quality_flags",
    "decode_utf8",
    "desensitize_pii",
    "desensitize_structured_record",
    "detect_document_structure",
    "detect_pdf_document_noise",
    "detect_text_format",
    "deduplicate_structured_records",
    "estimate_token_count",
    "expand_to_context_boundaries",
    "extract_pdf_page_texts",
    "extract_structured_records",
    "filter_anomalous_structured_records",
    "fingerprints_are_near_duplicate",
    "flatten_structured_record",
    "generate_standard_records",
    "is_low_quality_content",
    "is_near_duplicate",
    "merge_short_blocks",
    "near_duplicate_fingerprint",
    "normalize_structured_record",
    "normalize_text",
    "parse_text_content",
    "parse_utf8_text",
    "preprocess_structured_records",
    "protected_context_ranges",
    "record_fingerprint",
    "remove_document_noise",
    "score_quality",
    "stable_split",
    "stable_split_assignments",
]
