"""범용 텍스트 생성기 — 각 bullet_list 컴포넌트의 narrative_input 근거로 불릿 생성.

ETF 리뷰/전망 고정 구조가 아니라, IR 의 임의 narrative 컴포넌트(type=bullet_list 이고
narrative_input 필드를 가진)에 대해 컴포넌트별로 LLM 으로 불릿을 만들어 주입한다.
수치 환각은 가드로 검증한다.
"""
from __future__ import annotations

import re
from typing import Any

from app import prompts as _p
from app.services import llm as llm_service

GEN_SYSTEM = _p.get("generation", "system")
GEN_USER_TMPL = _p.get("generation", "user_template")


def _collect_numbers(obj: Any, out: set[str]) -> None:
    if isinstance(obj, dict):
        for v in obj.values():
            _collect_numbers(v, out)
    elif isinstance(obj, list):
        for v in obj:
            _collect_numbers(v, out)
    elif isinstance(obj, (int, float)):
        out.add(f"{float(obj):.2f}")
    elif isinstance(obj, str):
        for m in re.findall(r"-?\d+\.?\d*", obj):
            try:
                out.add(f"{float(m):.2f}")
            except ValueError:
                pass


def _hallucinated_metrics(text_obj: Any, allowed: set[str]) -> set[str]:
    found: set[str] = set()
    _collect_numbers(text_obj, found)
    return {n for n in found - allowed if not n.endswith(".00")}


def _evidence_from_component(comp: dict[str, Any]) -> dict[str, Any]:
    evidence: dict[str, Any] = {}
    for f in comp.get("dataSpec", {}).get("fields", []) or []:
        if f.get("role") != "narrative_input":
            continue
        rv = f.get("resolvedValue")
        if rv is None:
            continue
        if isinstance(rv, dict):
            evidence.update({k: v for k, v in rv.items() if v is not None})
        else:
            evidence[f.get("label") or f.get("key")] = rv
    return evidence


def _generate_bullets(section_title: str, evidence: dict[str, Any]) -> tuple[list[str], list[str]]:
    import json

    user = GEN_USER_TMPL.format(
        section_title=section_title or "내용",
        evidence=json.dumps(evidence, ensure_ascii=False),
    )
    raw = _call(user)
    warnings: list[str] = []
    try:
        out = llm_service.parse_json(raw)
        bullets = [str(b) for b in (out.get("bullets") or [])]
    except Exception as e:  # noqa: BLE001
        return [], [f"'{section_title}' 생성 JSON 파싱 실패: {e}"]
    allowed: set[str] = set()
    _collect_numbers(evidence, allowed)
    suspicious = _hallucinated_metrics(bullets, allowed)
    if suspicious:
        warnings.append(f"'{section_title}' 생성 텍스트에 근거 외 수치 포함(검토 필요): {sorted(suspicious)}")
    return bullets, warnings


def _call(user_text: str) -> str:
    from langchain_core.messages import HumanMessage, SystemMessage

    llm = llm_service.get_llm()
    resp = llm.invoke([SystemMessage(content=GEN_SYSTEM), HumanMessage(content=user_text)])
    return resp.content if isinstance(resp.content, str) else str(resp.content)


def generate_narratives(ir: dict[str, Any]) -> list[str]:
    """resolved IR 의 각 narrative bullet_list 컴포넌트에 bullets 를 생성·주입. warnings 반환."""
    warnings: list[str] = []
    for comp in ir.get("report", {}).get("components", []):
        if comp.get("type") != "bullet_list":
            continue
        evidence = _evidence_from_component(comp)
        if not evidence:
            continue
        bullets, w = _generate_bullets(comp.get("title", ""), evidence)
        comp["generatedBullets"] = bullets
        warnings.extend(w)
    return warnings
