"""Info Extractor Web 后端.

提供 Flask API 服务供 Vue 前端调用：
- 文档上传与文本提取（PDF / Word）
- 关键信息提取（流式返回思考过程 + 标注结果）
- 关键信息定义管理
"""

from __future__ import annotations

import json
import logging
import os
import queue
import re
import sys
import tempfile
import threading
import time
import uuid
from pathlib import Path
from typing import Any, Iterator

from flask import Flask, Response, jsonify, request, send_from_directory
from flask_cors import CORS

from dms import init_dms

# 文档解析依赖
try:
    import fitz  # PyMuPDF
except ImportError:
    fitz = None

try:
    from docx import Document
except ImportError:
    Document = None

from src.extractor import InfoExtractor

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)

app = Flask(__name__)
CORS(app)

# DMS业务使用独立的 /api/v1 Blueprint。初始化仅注册DMS基础设施，
# 不改变既有 /api/* AI接口的响应、认证或错误处理行为。
init_dms(app)

# ===== 前端静态托管（一体化分发用）=====
def _resolve_frontend_dist() -> Path | None:
    """定位前端 dist 目录：优先环境变量，其次 PyInstaller 资源，最后开发路径"""
    env_path = os.environ.get("DMS_FRONTEND_DIST", "").strip()
    if env_path:
        p = Path(env_path)
        return p if p.exists() else None
    if hasattr(sys, "_MEIPASS"):
        p = Path(sys._MEIPASS) / "frontend_dist"
        return p if p.exists() else None
    dev = Path(__file__).resolve().parent.parent / "frontend" / "dist"
    return dev if dev.exists() else None


FRONTEND_DIST = _resolve_frontend_dist()

if FRONTEND_DIST is not None:
    @app.route("/", defaults={"path": ""})
    @app.route("/<path:path>")
    def serve_spa(path: str):
        # /api/* 已由上方蓝图和显式路由优先匹配；漏网的 api 请求返回 404
        if path.startswith("api/"):
            return jsonify({"error": "Not found"}), 404
        candidate = FRONTEND_DIST / path
        if path and candidate.is_file():
            return send_from_directory(FRONTEND_DIST, path)
        # 其余路径统一返回 index.html，交给前端路由
        return send_from_directory(FRONTEND_DIST, "index.html")

    logger.info("Frontend dist mounted at: %s", FRONTEND_DIST)
else:
    logger.warning("Frontend dist not found; running in API-only mode")
# ===== 前端静态托管 END =====

# 从环境变量读取后端配置
# 例如：INFO_EXTRACTOR_API_KEY=your-api-key INFO_EXTRACTOR_MODEL_NAME=glm-5.2 INFO_EXTRACTOR_BASE_URL=https://... python app.py
API_KEY = os.environ.get("INFO_EXTRACTOR_API_KEY", "")
MODEL_NAME = os.environ.get("INFO_EXTRACTOR_MODEL_NAME", "glm-5.2")
BASE_URL = os.environ.get("INFO_EXTRACTOR_BASE_URL", "https://open.bigmodel.cn/api/coding/paas/v4")

logger.info("Backend config: MODEL_NAME=%s, BASE_URL=%s, API_KEY_PREFIX=%s", MODEL_NAME, BASE_URL, API_KEY[:6] if API_KEY else "")

# 上传文件临时存储目录
UPLOAD_DIR = Path(tempfile.gettempdir()) / "info_extractor_uploads"
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

# 关键信息定义列表持久化文件
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DEFINITIONS_FILE = DATA_DIR / "definitions.json"

DEFAULT_DEFINITION: dict[str, Any] = {
    "metadata": {
        "node_label": "PDJH",
        "node_name": "派对计划",
        "node_type": "concept",
    },
    "subnodes": {
        "JBXX": {
            "metadata": {
                "node_label": "JBXX",
                "node_name": "基本信息",
                "node_type": "concept",
            },
            "subnodes": {
                "PDMC": {
                    "metadata": {
                        "node_label": "PDMC",
                        "node_name": "派对名称",
                        "node_type": "property",
                        "value_type": "string",
                        "is_list": False,
                    }
                },
                "PDKSSJ": {
                    "metadata": {
                        "node_label": "PDKSSJ",
                        "node_name": "派对开始时间",
                        "node_type": "property",
                        "value_type": "date_time",
                        "is_list": False,
                    }
                },
                "PDZRS": {
                    "metadata": {
                        "node_label": "PDZRS",
                        "node_name": "派对总人数",
                        "node_type": "property",
                        "value_type": "integer",
                        "is_list": False,
                    }
                },
                "PDZYS": {
                    "metadata": {
                        "node_label": "PDZYS",
                        "node_name": "派对总预算",
                        "node_type": "property",
                        "value_type": "float",
                        "is_list": False,
                    }
                },
                "SFBA": {
                    "metadata": {
                        "node_label": "SFBA",
                        "node_name": "是否备案",
                        "node_type": "property",
                        "value_type": "boolean",
                        "is_list": False,
                    }
                },
            },
        },
        "RYXX": {
            "metadata": {
                "node_label": "RYXX",
                "node_name": "人员信息",
                "node_type": "concept",
            },
            "subnodes": {
                "ZCH": {
                    "metadata": {
                        "node_label": "ZCH",
                        "node_name": "总策划",
                        "node_type": "property",
                        "value_type": "class",
                        "is_list": False,
                    },
                    "subnodes": {
                        "XM": {
                            "metadata": {
                                "node_label": "XM",
                                "node_name": "姓名",
                                "node_type": "property",
                                "value_type": "string",
                                "is_list": False,
                            }
                        },
                        "XB": {
                            "metadata": {
                                "node_label": "XB",
                                "node_name": "性别",
                                "node_type": "property",
                                "value_type": "string",
                                "is_list": False,
                            }
                        },
                    },
                },
                "RYQD": {
                    "metadata": {
                        "node_label": "RYQD",
                        "node_name": "人员清单",
                        "node_type": "property",
                        "value_type": "class",
                        "is_list": True,
                    },
                    "subnodes": {
                        "XM": {
                            "metadata": {
                                "node_label": "XM",
                                "node_name": "姓名",
                                "node_type": "property",
                                "value_type": "string",
                                "is_list": False,
                            }
                        },
                        "XB": {
                            "metadata": {
                                "node_label": "XB",
                                "node_name": "性别",
                                "node_type": "property",
                                "value_type": "string",
                                "is_list": False,
                            }
                        },
                    },
                },
            },
        },
        "HDXX": {
            "metadata": {
                "node_label": "HDXX",
                "node_name": "活动信息",
                "node_type": "concept",
            },
            "subnodes": {
                "PBYXQD": {
                    "metadata": {
                        "node_label": "PBYXQD",
                        "node_name": "破冰游戏清单",
                        "node_type": "property",
                        "value_type": "string",
                        "is_list": True,
                    }
                }
            },
        },
    },
}


def _load_definitions() -> list[dict[str, Any]]:
    """从 JSON 文件加载关键信息定义列表."""
    if not DEFINITIONS_FILE.exists():
        return []
    try:
        with open(DEFINITIONS_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, list):
            return data
        if isinstance(data, dict):
            # 兼容旧版单条定义
            return [_wrap_definition(data)]
    except Exception as exc:
        logger.warning("加载定义文件失败: %s", exc)
    return []


def _save_definitions(definitions: list[dict[str, Any]]) -> None:
    """保存关键信息定义列表到 JSON 文件."""
    with open(DEFINITIONS_FILE, "w", encoding="utf-8") as f:
        json.dump(definitions, f, ensure_ascii=False, indent=2)


def _wrap_definition(definition: dict[str, Any], name: str = "", description: str = "") -> dict[str, Any]:
    """将原始定义包装为列表项."""
    now = time.strftime("%Y-%m-%d %H:%M:%S")
    metadata = definition.get("metadata", {})
    if not name:
        name = metadata.get("node_name", "未命名定义")
    return {
        "id": str(uuid.uuid4()),
        "name": name,
        "description": description,
        "definition": definition,
        "created_at": now,
        "updated_at": now,
    }


# 全局定义列表
_definitions: list[dict[str, Any]] = _load_definitions()
if not _definitions:
    _definitions = [_wrap_definition(DEFAULT_DEFINITION, name="派对计划", description="示例关键信息定义")]
    _save_definitions(_definitions)


def _extract_text_from_pdf(file_path: str) -> str:
    """从 PDF 中提取文本."""
    if fitz is None:
        raise RuntimeError("PyMuPDF 未安装，无法解析 PDF")

    text_parts: list[str] = []
    with fitz.open(file_path) as doc:
        for page in doc:
            text_parts.append(page.get_text())
    return "\n\n".join(text_parts)


def _extract_text_from_docx(file_path: str) -> str:
    """从 Word 文档中提取文本."""
    if Document is None:
        raise RuntimeError("python-docx 未安装，无法解析 Word 文档")

    doc = Document(file_path)
    text_parts: list[str] = []
    for para in doc.paragraphs:
        if para.text.strip():
            text_parts.append(para.text)
    return "\n\n".join(text_parts)


def _to_markdown(text: str) -> str:
    """简单地将正文文本转为 markdown 格式.

    当前只做基本分段和标题检测，未来可接入更强的文档解析模型。
    """
    lines = text.split("\n")
    result: list[str] = []
    for line in lines:
        stripped = line.strip()
        if not stripped:
            result.append("")
            continue
        # 简单启发式：短且没有标点的行可能是标题
        if len(stripped) < 30 and not re.search(r"[。，；：！？]", stripped):
            result.append(f"## {stripped}")
        else:
            result.append(stripped)
    return "\n\n".join(result)


def _clean_markdown_for_extraction(markdown: str) -> str:
    """将 markdown 还原为纯文本供 LLM 提取."""
    text = re.sub(r"^#+\s*", "", markdown, flags=re.MULTILINE)
    return text.strip()


@app.route("/api/health", methods=["GET"])
def health():
    return jsonify({"status": "ok"})


@app.route("/api/upload", methods=["POST"])
def upload_file():
    """上传 PDF 或 Word 文档，返回文档 ID 和提取的文本内容."""
    if "file" not in request.files:
        return jsonify({"error": "未提供文件"}), 400

    file = request.files["file"]
    if file.filename == "":
        return jsonify({"error": "文件名为空"}), 400

    ext = Path(file.filename).suffix.lower()
    if ext not in (".pdf", ".docx", ".doc"):
        return jsonify({"error": "仅支持 PDF 和 Word 文档"}), 400

    doc_id = str(uuid.uuid4())
    file_path = UPLOAD_DIR / f"{doc_id}{ext}"
    file.save(str(file_path))

    try:
        if ext == ".pdf":
            raw_text = _extract_text_from_pdf(str(file_path))
        else:
            raw_text = _extract_text_from_docx(str(file_path))
    except Exception as exc:
        logger.exception("文档解析失败")
        return jsonify({"error": f"文档解析失败: {exc}"}), 500

    markdown = _to_markdown(raw_text)

    return jsonify({
        "doc_id": doc_id,
        "filename": file.filename,
        "markdown": markdown,
        "raw_text": raw_text,
    })


@app.route("/api/definition", methods=["GET", "POST"])
def definition():
    """获取或保存关键信息定义列表."""
    global _definitions
    if request.method == "GET":
        return jsonify({"definitions": _definitions})

    data = request.get_json()
    if not isinstance(data, dict) or "definitions" not in data:
        return jsonify({"error": "非法的关键信息定义列表格式"}), 400
    definitions = data["definitions"]
    if not isinstance(definitions, list):
        return jsonify({"error": "definitions 必须是数组"}), 400
    now = time.strftime("%Y-%m-%d %H:%M:%S")
    for item in definitions:
        if not isinstance(item, dict) or "definition" not in item:
            return jsonify({"error": "列表项必须包含 definition 字段"}), 400
        item.setdefault("id", str(uuid.uuid4()))
        item.setdefault("name", "未命名定义")
        item.setdefault("description", "")
        item["updated_at"] = now
    _definitions = definitions
    _save_definitions(_definitions)
    return jsonify({"success": True})


@app.route("/api/definition/import", methods=["POST"])
def import_definition():
    """导入 JSON 格式定义，自动提取名称和描述."""
    data = request.get_json()
    if not isinstance(data, dict):
        return jsonify({"error": "请求体必须是 JSON 对象"}), 400

    definition = data.get("definition")
    if not isinstance(definition, dict) or "metadata" not in definition:
        return jsonify({"error": "非法的关键信息定义格式"}), 400

    name = data.get("name", "").strip()
    description = data.get("description", "").strip()
    item = _wrap_definition(definition, name=name, description=description)
    _definitions.append(item)
    _save_definitions(_definitions)
    return jsonify({"success": True, "definition": item})


@app.route("/api/extract", methods=["POST"])
def extract():
    """流式提取关键信息.

    前端通过 SSE 接收：
    - type=thinking: 模型思考过程/流式输出片段
    - type=annotated: 最终标注后的文本
    - type=done: 完成
    - type=error: 错误
    """
    data = request.get_json()
    article = data.get("article", "")
    mode = data.get("mode", "two_phase")
    model_name = data.get("model_name", MODEL_NAME)
    base_url = data.get("base_url", BASE_URL)
    logger.info("Extract request: model_name=%s, base_url=%s", model_name, base_url)

    if not article:
        return jsonify({"error": "文章内容不能为空"}), 400

    definition_data = data.get("definition")
    definition_id = data.get("definition_id")
    if definition_data is None:
        if definition_id:
            for item in _definitions:
                if item.get("id") == definition_id:
                    definition_data = item.get("definition")
                    break
            if definition_data is None:
                return jsonify({"error": "未找到指定的关键信息定义"}), 400
        else:
            if not _definitions:
                return jsonify({"error": "未配置关键信息定义"}), 400
            definition_data = _definitions[0].get("definition")

    def generate() -> Iterator[str]:
        if not API_KEY:
            yield f"data: {json.dumps({'type': 'error', 'message': '后端未配置 API Key'}, ensure_ascii=False)}\n\n"
            return

        thinking_queue: queue.Queue[Any] = queue.Queue()
        extract_result: dict[str, Any] = {}
        extract_error: list[Exception] = []

        def run_extraction() -> None:
            try:
                extractor = InfoExtractor(
                    model_name=model_name,
                    api_key=API_KEY,
                    base_url=base_url or None,
                    temperature=0,
                    max_retries=1,
                )
                extractor.stream_callback = thinking_queue.put

                def on_step(label: str) -> None:
                    thinking_queue.put(("__step__", label))

                extractor.step_callback = on_step
                result = extractor.extract(
                    article=article,
                    definition=definition_data,
                    mode=mode,
                    verbose=True,
                    annotate=True,
                )
                extract_result["data"] = result
            except Exception as exc:
                logger.exception("提取失败")
                extract_error.append(exc)

        extraction_thread = threading.Thread(target=run_extraction)
        extraction_thread.start()

        # 实时从队列读取思考过程并推送
        while extraction_thread.is_alive() or not thinking_queue.empty():
            try:
                chunk = thinking_queue.get(timeout=0.1)
            except queue.Empty:
                continue
            if isinstance(chunk, tuple) and chunk and chunk[0] == "__step__":
                yield f"data: {json.dumps({'type': 'step', 'content': chunk[1]}, ensure_ascii=False)}\n\n"
            else:
                yield f"data: {json.dumps({'type': 'thinking', 'content': chunk}, ensure_ascii=False)}\n\n"

        extraction_thread.join()

        if extract_error:
            yield f"data: {json.dumps({'type': 'error', 'message': str(extract_error[0])}, ensure_ascii=False)}\n\n"
            return

        result = extract_result.get("data", {})
        annotated_text = result.get("annotated_text", "") if isinstance(result, dict) else ""
        yield f"data: {json.dumps({'type': 'annotated', 'content': annotated_text}, ensure_ascii=False)}\n\n"
        yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"

    response = Response(generate(), mimetype="text/event-stream")
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response


# 标注结果持久化目录
ANNOTATION_DIR = DATA_DIR / "annotations"
ANNOTATION_DIR.mkdir(parents=True, exist_ok=True)


def _annotation_path(doc_key: str) -> Path:
    """根据前端传入的 doc_key 生成本地存储路径.

    doc_key 由前端生成（如 doc_id + definition_id），仅允许字母数字与下划线连字符，
    避免路径穿越风险。
    """
    safe = re.sub(r"[^A-Za-z0-9_\-.]", "_", doc_key)
    if not safe:
        safe = "default"
    return ANNOTATION_DIR / f"{safe}.json"


@app.route("/api/annotation", methods=["GET", "POST"])
def annotation():
    """保存或读取标注后的文章内容.

    GET  ?doc_key=xxx  ->  {"content": "..."}
    POST {doc_key, content}  ->  {"success": true}
    """
    if request.method == "GET":
        doc_key = request.args.get("doc_key", "")
        path = _annotation_path(doc_key)
        if not path.exists():
            return jsonify({"content": ""})
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return jsonify({"content": data.get("content", "")})
        except Exception as exc:
            logger.warning("读取标注失败: %s", exc)
            return jsonify({"content": ""})

    data = request.get_json() or {}
    doc_key = data.get("doc_key", "")
    content = data.get("content", "")
    if not doc_key:
        return jsonify({"error": "缺少 doc_key"}), 400
    path = _annotation_path(doc_key)
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"content": content, "updated_at": time.strftime("%Y-%m-%d %H:%M:%S")}, f, ensure_ascii=False, indent=2)
    except Exception as exc:
        logger.exception("保存标注失败")
        return jsonify({"error": f"保存失败: {exc}"}), 500
    return jsonify({"success": True})


if __name__ == "__main__":
    app.run(
        host=os.environ.get("DMS_HOST", "0.0.0.0"),
        port=int(os.environ.get("DMS_PORT", "9345")),
        debug=False,
    )
