"""
⚠️ DEPRECATED — 请勿修改、复制或引用此文件 ⚠️

本文件已被 infra.pipelines.anyfile2md 统一管线取代。
mindos_sse.py 已完成迁移（2026-04-20），不再调用此模块。

新的统一管线位于：
  mindOSv2/hermes-overlay/infra/pipelines/anyfile2md.py
  mindOSv2/hermes-overlay/infra/atoms/  （6 个原子操作）

原始文件保留仅供参考，下次部署时将删除。

──────────────────────────────────────────────────────
以下为原始 docstring（仅供考古）：

doc_parser.py — AnyFile2MD 通用文件解析模块（已废弃）

架构约束（SPEC_anyfile2md_doc_parser.md v1.1）：
  ❌ 不依赖 LLMAgno（V2 遗留）——前代隔离铁律
  ❌ 不调用 LLM（VL 降级除外），不写 DB，不持久化任何状态
  ✅ VL 调用直接走 LiteLLM Gateway（OpenAI SDK）
  ✅ 封闭函数（铁律 1）：只做格式分发 + 文本提取 + 返回 Markdown

公开接口：
  parse_to_markdown(read_url, filename, ...) → str
  is_supported_doc(filename) → bool
  SUPPORTED_DOC_EXTS → set

依赖：
  python-docx    （≈V2 mammoth —— DOCX 文本提取）
  pdfplumber     （≈V2 pdf-parse —— PDF 文字提取）
  PyMuPDF (fitz) （≈V2 pdf-to-img —— PDF 逐页光栅化，VL 降级用）
  openai         （VL 调用，hermes 已有）
  httpx          （OSS 下载/上传，hermes 已有）
"""

import logging
import os
import tempfile
import time
from pathlib import Path
from typing import Optional

import httpx

logger = logging.getLogger(__name__)

# ─── 格式分类（移植自 V2 normalize-manager.cjs L14-30） ─────

AUDIO_EXTS = {
    ".mp3", ".mp4", ".m4a", ".wav", ".webm",
    ".ogg", ".aac", ".flac", ".opus", ".amr",
}
IMAGE_EXTS = {
    ".png", ".jpg", ".jpeg", ".bmp", ".gif",
    ".webp", ".tiff", ".tif",
}
DOCX_EXTS = {".docx", ".doc"}
PDF_EXTS = {".pdf"}
TEXT_EXTS = {".txt", ".md", ".markdown", ".rst", ".log"}

# 所有支持的非音频格式（前端 accept 属性用）
SUPPORTED_DOC_EXTS = IMAGE_EXTS | DOCX_EXTS | PDF_EXTS | TEXT_EXTS


def is_supported_doc(filename: str) -> bool:
    """判断文件是否为支持的文档/图片格式。"""
    ext = Path(filename).suffix.lower()
    return ext in SUPPORTED_DOC_EXTS


# ─── VL 提示词（从 LLMAgno/server.py 复制并固化） ────────────

VL_EXTRACT_PROMPT = """请仔细阅读这张图片中的所有内容，并以结构化的纯文本形式输出：

1. 完整转录图片中的所有文字内容，保留原始排版结构（标题、段落、列表等）
2. 如果图片包含表格，用 Markdown 表格格式输出
3. 如果图片包含图表（柱状图、折线图、饼图等），用文字描述图表的数据和趋势
4. 如果图片包含流程图或示意图，用文字描述其结构和逻辑关系
5. 忽略页眉页脚、页码、水印等装饰性元素

直接输出内容，不要加任何前缀说明或总结。"""


# ─── 主函数 ─────────────────────────────────────────────────

async def parse_to_markdown(
    read_url: str,
    filename: str,
    *,
    max_pages: int = 30,
    vl_model: str = "qwen-vl",
) -> dict:
    """
    下载 OSS 文件 → 按扩展名分流解析 → 返回解析结果。

    封闭函数（铁律 1）：不写 DB，不持久化状态。
    VL 调用直接走 LiteLLM Gateway（前代隔离铁律）。

    Args:
        read_url:  OSS 可读 URL（由 generate_oss_presign 返回）
        filename:  原始文件名（用于判断扩展名）
        max_pages: PDF 最大处理页数（安全守卫，V2 验证值=30）
        vl_model:  VL 模型别名（LiteLLM 标准别名）

    Returns:
        {
            "text": str,        # Markdown 格式的文本内容
            "provider": str,    # 使用的解析方式
            "vl_pages": int,    # VL 识别的页数（用于积分计算）
        }

    Raises:
        ValueError: 不支持的文件格式
        RuntimeError: 解析失败
    """
    ext = Path(filename).suffix.lower()

    if ext in AUDIO_EXTS:
        raise ValueError(f"音频文件请走 /api/audio/transcribe 管线: {ext}")

    if ext not in SUPPORTED_DOC_EXTS:
        raise ValueError(f"不支持的文件格式: {ext}")

    # 1. 下载到临时文件
    tmp_path = await _download_to_temp(read_url, filename)

    try:
        if ext in TEXT_EXTS:
            text = _read_text_file(tmp_path)
            return {"text": text, "provider": "text_read", "vl_pages": 0}

        if ext in IMAGE_EXTS:
            text = await _vl_extract_image(read_url, filename, vl_model)
            return {"text": text, "provider": "vl_image", "vl_pages": 1}

        if ext in DOCX_EXTS:
            text = _extract_docx(tmp_path)
            return {"text": text, "provider": "python_docx", "vl_pages": 0}

        if ext in PDF_EXTS:
            # PDF 双策略降级（移植自 V2 normalize-manager L260-280）
            text = _extract_pdf_text(tmp_path)
            if len(text.strip()) >= 50:
                return {"text": text, "provider": "pdfplumber", "vl_pages": 0}

            logger.info(
                "[DocParser] PDF 文字过少 (%d字)，降级到 VL 逐页识别",
                len(text.strip()),
            )
            text, page_count = await _vl_extract_pdf_pages(
                tmp_path, filename, max_pages, vl_model,
            )
            return {"text": text, "provider": "vl_pdf_pages", "vl_pages": page_count}

        # 兜底：尝试直读
        text = _read_text_file(tmp_path)
        return {"text": text, "provider": "text_fallback", "vl_pages": 0}

    finally:
        _cleanup_temp(tmp_path)


# ─── 各格式解析函数 ──────────────────────────────────────────

def _extract_docx(file_path: str) -> str:
    """DOCX → Markdown 文本。等价于 V2 mammoth.extractRawText()。"""
    import docx  # python-docx

    doc = docx.Document(file_path)
    paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
    return "\n\n".join(paragraphs)


def _extract_pdf_text(file_path: str) -> str:
    """PDF 文字提取。等价于 V2 pdf-parse。"""
    import pdfplumber

    texts = []
    with pdfplumber.open(file_path) as pdf:
        for page in pdf.pages:
            text = page.extract_text()
            if text:
                texts.append(text)
    return "\n\n".join(texts)


async def _vl_extract_image(
    image_url: str,
    filename: str,
    model: str = "qwen-vl",
) -> str:
    """单张图片 VL 识别。直接走 LiteLLM Gateway（前代隔离铁律）。"""
    import openai

    client = openai.AsyncOpenAI(
        base_url=os.getenv("LITELLM_BASE_URL", "http://127.0.0.1:4000/v1"),
        api_key=os.getenv("LITELLM_API_KEY", ""),
    )

    start = time.time()
    response = await client.chat.completions.create(
        model=model,
        messages=[{
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": image_url}},
                {"type": "text", "text": VL_EXTRACT_PROMPT},
            ],
        }],
        max_tokens=4096,
        temperature=0.1,
    )

    text = response.choices[0].message.content or ""
    elapsed = int((time.time() - start) * 1000)
    logger.info(
        "[DocParser] VL ✅ model=%s, text_len=%d, elapsed=%dms, file=%s",
        model, len(text), elapsed, filename,
    )
    return text


async def _vl_extract_pdf_pages(
    file_path: str,
    filename: str,
    max_pages: int,
    model: str,
) -> tuple[str, int]:
    """扫描型 PDF 逐页 VL 识别。移植自 V2 extractPdfViaVL。

    Returns:
        (markdown_text, page_count) — page_count 用于积分计算
    """
    import fitz  # PyMuPDF

    doc = fitz.open(file_path)
    total_pages = len(doc)
    pages = []
    processed_count = 0

    for i, page in enumerate(doc):
        if i >= max_pages:
            logger.warning(
                "[DocParser] PDF 页数超限 (%d/%d)，截断", max_pages, total_pages,
            )
            break

        # 光栅化 → PNG 字节
        pix = page.get_pixmap(dpi=200)
        img_bytes = pix.tobytes("png")

        # 每页独立 try-except（V2 验证的韧性策略）
        try:
            img_url = await _upload_temp_image(
                img_bytes, f"{filename}_p{i}.png",
            )
            text = await _vl_extract_image(img_url, f"{filename}_p{i}", model)
            if text.strip():
                pages.append(f"## 第 {i + 1} 页\n\n{text}")
                processed_count += 1
        except Exception as e:
            logger.warning(
                "[DocParser] PDF 第%d页 VL 失败（跳过）: %s", i + 1, e,
            )

    doc.close()
    return "\n\n".join(pages), processed_count


# ─── 辅助函数 ────────────────────────────────────────────────

def _read_text_file(file_path: str) -> str:
    """直读文本文件，含二进制安全检查（移植自 V2 normalize-manager L287-299）。"""
    with open(file_path, "rb") as f:
        raw = f.read()

    # null 字节检测：防止二进制文件误读
    if b"\x00" in raw[:1024]:
        raise ValueError("文件包含二进制内容，无法作为文本处理")

    return raw.decode("utf-8", errors="replace")


async def _download_to_temp(read_url: str, filename: str) -> str:
    """从 OSS URL 下载到临时文件，返回临时路径。"""
    suffix = Path(filename).suffix or ".bin"

    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.get(read_url)
        resp.raise_for_status()

    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
    tmp.write(resp.content)
    tmp.close()

    logger.info(
        "[DocParser] 下载完成: %s → %s (%d bytes)",
        filename, tmp.name, len(resp.content),
    )
    return tmp.name


async def _upload_temp_image(img_bytes: bytes, filename: str) -> str:
    """上传临时图片到 OSS，返回可读 URL（VL 逐页降级用）。

    复用 flash_asr.generate_oss_presign + httpx PUT。
    """
    from flash_asr import generate_oss_presign  # type: ignore

    ext = Path(filename).suffix or ".png"
    # 使用独立的 OSS 前缀，与音频/文档区分
    presign = generate_oss_presign(
        user_id="vl_temp", ext=ext,
        prefix="mindos-next/vl-temp",
    )

    async with httpx.AsyncClient(timeout=60) as client:
        resp = await client.put(
            presign["upload_url"],
            content=img_bytes,
            headers={"Content-Type": presign["content_type"]},
        )
        resp.raise_for_status()

    logger.debug("[DocParser] 临时图片已上传: %s", presign["oss_key"])
    return presign["read_url"]


def _cleanup_temp(tmp_path: str) -> None:
    """删除临时文件，fire-and-forget。"""
    try:
        os.unlink(tmp_path)
    except Exception:
        pass
