"""LLM 字段抽取：差量模式（仅对未知字段构造 prompt）。"""

from __future__ import annotations
import logging
from typing import Any
from fastapi_modules.fastapi_leaudit.govdoc_engine.models import Document
from fastapi_modules.fastapi_leaudit.govdoc_engine.llm.client import LlmClient, _format_exc

_log = logging.getLogger(__name__)


_PROMPT_HEAD = """从下面的公文中抽取以下指定字段，仅以 JSON 输出。

【公文内容（顺序段落）】
{text}

【需要抽取的字段】
{spec_block}

【输出格式】
仅 JSON：{{{example}}}
未识别的字段填 ""（list 类型填 []）。
"""


def _build_doc_text(doc: Document) -> str:
    return "\n".join(f"[{p.index}] {p.text}" for p in doc.paragraphs)


def _example_for(spec: dict[str, dict]) -> str:
    parts = []
    for name, meta in spec.items():
        t = meta.get("type", "string")
        if t == "list":
            parts.append(f'"{name}": []')
        else:
            parts.append(f'"{name}": ""')
    return ", ".join(parts)


class FieldExtractor:
    """LLM 差量字段抽取。

    extract_missing(doc, spec): spec 指定需要抽哪些字段；空 spec 不调 LLM。
    """

    def __init__(self, llm_client: LlmClient):
        self.client = llm_client

    def _build_messages_for_spec(
        self, doc: Document, spec: dict[str, dict]
    ) -> list[dict[str, str]]:
        spec_lines = [
            f"- {name}: {meta.get('description', name)}"
            f"（{meta.get('type', 'string')}）"
            for name, meta in spec.items()
        ]
        prompt = _PROMPT_HEAD.format(
            text=_build_doc_text(doc),
            spec_block="\n".join(spec_lines) or "（无）",
            example=_example_for(spec),
        )
        return [{"role": "user", "content": prompt}]

    def _shape_missing(
        self, spec: dict[str, dict], resp: dict
    ) -> dict[str, Any]:
        out: dict[str, Any] = {}
        for name, meta in spec.items():
            if meta.get("type") == "list":
                out[name] = resp.get(name) or []
            else:
                out[name] = resp.get(name) or ""
        return out

    def extract_missing(
        self, doc: Document | None, spec: dict[str, dict]
    ) -> dict[str, Any]:
        if not spec or doc is None:
            return {}
        label = "extract_missing__" + ",".join(spec.keys())
        try:
            resp = self.client.chat_json(
                self._build_messages_for_spec(doc, spec), label=label,
            )
        except Exception as e:
            _log.warning("Differential extraction failed: %s", _format_exc(e))
            resp = {}
        return self._shape_missing(spec, resp)

    async def extract_missing_async(
        self, doc: Document | None, spec: dict[str, dict]
    ) -> dict[str, Any]:
        if not spec or doc is None:
            return {}
        label = "extract_missing__" + ",".join(spec.keys())
        try:
            resp = await self.client.chat_json_async(
                self._build_messages_for_spec(doc, spec), label=label,
            )
        except Exception as e:
            _log.warning("Differential extraction failed: %s", _format_exc(e))
            resp = {}
        return self._shape_missing(spec, resp)
