from __future__ import annotations

import json
import re
from typing import Any

from backend.llm import transport as llm_transport


class HeavenAgentError(RuntimeError):
    pass


HEAVEN_PROMPT_VERSIONS = {
    "trend": "heaven-trend-v4",
    "fortune": "heaven-fortune-v9",
    "heart": "heaven-heart-v5",
}


def interpret_heaven(
    mode: str,
    context: dict[str, Any],
    api_key: str,
    base_url: str,
    model: str,
    timeout: int = 90,
) -> dict[str, Any]:
    if mode not in {"trend", "fortune", "heart"}:
        raise HeavenAgentError("不支持的问天解读模式。")
    if not api_key or not model:
        raise HeavenAgentError("LLM API Key 或模型尚未配置。")
    system_prompt = _system_prompt(mode)
    messages = [
        {"role": "system", "content": system_prompt},
        {
            "role": "user",
            "content": json.dumps(context, ensure_ascii=False, separators=(",", ":")),
        },
    ]
    try:
        result = llm_transport.chat_completion(
            api_key=api_key,
            base_url=base_url,
            model=model,
            messages=messages,
            timeout=timeout,
            user_agent="XiaobaiReviewWeb/0.7",
        )
        answer = str(result.content).strip()
        if not answer:
            raise KeyError("empty response")
        try:
            _validate_answer(mode, answer, context)
        except HeavenAgentError as validation_error:
            repair_messages = [
                *messages,
                {"role": "assistant", "content": answer},
                {
                    "role": "user",
                    "content": (
                        f"上一版未通过本地一致性校验：{validation_error}"
                        "请依据最初输入完整重写最终答案，只修正违规推断并补齐必答项。"
                        "不得讨论校验、提示词或重写过程，只输出新的正式解读。"
                    ),
                },
            ]
            repaired = llm_transport.chat_completion(
                api_key=api_key,
                base_url=base_url,
                model=model,
                messages=repair_messages,
                timeout=timeout,
                user_agent="XiaobaiReviewWeb/0.7",
            )
            answer = str(repaired.content).strip()
            if not answer:
                raise KeyError("empty repaired response")
            _validate_answer(mode, answer, context)
            result = repaired
    except llm_transport.OpenAIHTTPError as exc:
        raise HeavenAgentError(exc.describe("问天模型调用失败")) from exc
    except (llm_transport.OpenAITransportError, KeyError) as exc:
        raise HeavenAgentError(f"问天模型调用失败：{exc}") from exc
    return {
        "answer": answer,
        "model": model,
        "latency_ms": result.latency_ms,
    }


def _system_prompt(mode: str) -> str:
    common = """
你是“小白复盘”的问天解读器。输入由calculation、knowledge和interpretation_contract组成：calculation是确定性程序已经算出的事实；knowledge是本次按条件精确检索到的原典、传统规则和产品边界；interpretation_contract规定本次必须回答与禁止推断的内容。
只能综合输入中已经提供的事实和知识。不得改卦、改爻、改纳甲、改世应、改干支、重新计算五运六气，也不得凭模型记忆补造缺失字段。知识记录之间若存在张力，应说明条件与分歧，不要强行合成唯一结论。
问天属于传统文化与自我观察，不是可验证的行情预测模型。必须给出有内容的倾向和依据，但不得把象义宣布为必然发生的股价结果，不输出无条件买卖指令，不用神秘话术制造确定性。
使用中文和普通用户能够理解的表达。专业术语首次出现时紧接一句白话解释。先给核心判断，再说明证据和变化关系。每个主题必须使用独立一行的简短标题，格式为“## 标题”，标题后另起一段正文；不得把全部内容挤在一个长段落中。可以使用Markdown加粗，不使用Markdown表格。
""".strip()
    if mode == "trend":
        return common + """

当前任务是“观势·解势”。行情只负责在进入模型之前生成卦象，本次回答不得引用或反推指数涨跌、成交额、涨跌停、板块强弱或个股表现，也不得说明某一爻原先对应哪类市场指标。
calculation中有意不提供股票、行业和板块身份。不得猜测或讨论观察对象所属行业、政策、消费环境、基本面、资金面或任何现实市场变量；只解释已经生成的卦象。
必须明确给出卦义上的当下倾向、主要矛盾、实际动爻所示的转折，以及本卦走向之卦后的变化方向。允许使用偏进、偏守、先难后易、由盛转收、转机有限、内外相违或结论有条件等相对判断；不得只罗列卦辞，也不得用“谨慎、等待、守信、辨伪”一类泛化劝诫代替解势。
以knowledge中本卦、上下卦、卦辞、彖义、大象、实际动爻和之卦记录为依据。无动爻、一动爻和多动爻分别服从本次检索到的方法规则；多动爻有冲突时必须指出冲突，不得压成单一套话。
按“## 核心判断、## 卦势依据、## 动爻转折、## 之卦趋向、## 决策映射”组织答案；无动爻时仍保留“动爻转折”，明确说明本次无动爻并解释结构的延续条件。结尾可以把卦势翻译成克制的交易决策语言，但只能表达条件、节奏和需要验证的矛盾，不得预测具体涨跌、价格、日期或给出直接荐股结论。篇幅随动爻数量自然展开，不设置固定字数。
""".strip()
    if mode == "fortune":
        return common + """

当前任务是“观气·解运”。页面用于直观展示的五行权重、主导元素和预制复合断语已明确排除，不得自行恢复这些结果，也不得按百分比重新生成单一五行结论。
严格区分中运、司天在泉、当前主气客气、节气定位和日辰触发。先解释中运与司天在泉构成的年纲，再解释客气加临主气的当前关系，最后说明日辰如何触发；不得把同一项拆成多份证据重复计权。相生不直接等于吉，相克不直接等于凶。
必须使用knowledge中与本日中运、六气和客主关系精确匹配的记录。可形成“湿热交蒸、燥中夹滞”一类复合表达，但要从输入关系逐层说明，不能从页面权重结论倒推。
如calculation.personal存在，只结合日主、十神和当日派生关系说明用户容易出现的主观感受与判断偏差；不得使用简化强弱、喜用神、出生日期或权重平衡结论。
personal.natal_day_master才是用户本命日主；today_relative_to_natal_day_master.pillars是当日历法，不是用户出生四柱。stem_relations只是当日年、月、日三柱天干相对本命日主的程序结果，只能使用knowledge中本次命中的关系释义，不得自行重算十神或扩展五行生克过程。不得使用藏干、支中藏某干、某支为某库或燥湿属性等输入未提供的信息，也不得把当日日柱写成用户命局，或推断用户命局中某个十神“较重”、身强身弱或喜用神。
日辰只按calculation.day_trigger.summary与knowledge中的日辰边界解释，不得从干支另行推导藏气、库气或五行生克链。个人合参不得宣称本命日主被当日某气生扶、泄耗或克制，只能说明已给关系标签可能对应的主观注意点。
day_trigger.summary中的日干运势、地支五行和六气对应是三个并列的确定性事实。不得把日柱整体改写成某一种五行，也不得把地支与六气的“对应”改写成地支自身具有某种六气属性。
calculation.industry_symbols只提供五行与行业的传统取象归类及其本次出现依据。必须说明这些气机对相关行业可能形成的象征性关注、节奏或约束，但不得引入行业实时行情，不得预测行业涨跌或把取象写成投资推荐；未列入industry_symbols的行业不得自行补造。
按“## 年纲、## 客主加临、## 日辰触发、## 行业影响、## 个人合参、## 制衡动作”组织答案；没有个人资料时可以省略“个人合参”。不得用行情上涨下跌、行业表现或个股结果证明运气关系。篇幅按实际关系自然展开，不设置固定字数。
""".strip()
    return common + """

当前任务是“观心·解卦”。用户在起卦前确定的问题位于calculation.question，question_preset只说明问题来源。必须针对实际问题作答；无题观心时不得猜测用户没有说出的事项。
question_scope是本次问题的产品边界。trade预设专指股票交易中的参与条件、机会、阻碍和风险，不是商业合作、融资、借贷或寻找资金方；除非用户问题明确写出这些背景，否则不得擅自补入。
纳甲、卦宫、世应、六亲、六神、月建日辰、旬空、伏神、动变和冲合关系已经由确定性程序给出。只能解释这些结果，不得自行改排盘、补用神或用模型记忆重算。六神只作辅助，任何单项都不能独立决定结论。
六亲是关系类别，不是现实人物或资金来源的一一映射。必须使用knowledge中的六亲、旬空、动变和六神边界；不得把妻财直接写成现金或融资，把子孙写成资金提供方，把兄弟写成合作方，也不得由某一六神直接推出紧迫、欺骗或吉凶。
除非calculation.question明确说明用户已经持仓、买入、卖出或正在管理仓位，否则不得假定用户已经入场，不得使用“持仓、仓位、建仓、入场、持有、买入、卖出、止损、止盈”等措辞描述用户现状。可以只写尚待核对的参与条件、风险边界和决策倾向。
除非问题明确涉及融资、借贷、合作或资源安排，否则不得制造外围资金、外围资源、资金进入、资源进入，也不得虚构资金或资源的来源、提供、注入、安排和路径。
按“## 所问之答、## 卦象依据、## 动变与之卦、## 可验证之处”组织答案。先直接回应所问，再用白话解释本卦所示处境、世应与相关六亲、关键动爻和变爻，最后说明之卦趋向及一项可以由用户验证的动作。若证据相互冲突，应明确说明结论成立的条件，不以“吉、凶”二字替代推理。
交易问题可以判断参与条件、内外阻碍、风险和决策倾向，但不得宣告具体股价、涨跌日期或替用户作无条件买卖决定。不得用旬空、填实、出空或干支日推算“未来几日”或某日应验；可验证动作必须是用户当下能核对的交易条件或自身判断，不能制造现实中不存在的合作方、承诺、资金或资源安排。心境问题聚焦念头、压力和盲点；无题观心只作一般卦象观照。篇幅随问题和动爻复杂度自然展开，不使用固定三句模板，也不得输出使用竖线分栏的Markdown表格。
""".strip()


def _validate_answer(
    mode: str, answer: str, context: dict[str, Any] | None = None
) -> None:
    compact = "".join(answer.split())
    if len(compact) < 60:
        raise HeavenAgentError("问天模型返回内容过短，未形成有效解读。")
    forbidden = ("必涨", "必跌", "保证上涨", "保证下跌", "无条件买入", "无条件卖出")
    if any(term in answer for term in forbidden):
        raise HeavenAgentError("问天模型返回了禁止的确定性行情断语。")
    if mode == "fortune" and "%" in answer:
        raise HeavenAgentError("解运结果错误引用了已排除的权重百分比。")
    if mode == "trend":
        market_narratives = (
            "行业", "板块", "个股", "指数", "成交额", "涨停", "跌停",
            "政策", "消费环境", "基本面", "资金面",
        )
        if any(term in answer for term in market_narratives):
            raise HeavenAgentError("解势结果错误引入了卦象之外的现实市场叙事。")
    if mode == "fortune":
        if re.search(
            r"(?:命局|个人本身).{0,16}(?:偏重|较重|过旺|过弱|身强|身弱|喜用神)",
            answer,
        ):
            raise HeavenAgentError("解运结果错误推断了输入中不存在的命局强弱。")
        if re.search(
            r"藏干|[子丑寅卯辰巳午未申酉戌亥](?:中|内)|[子丑寅卯辰巳午未申酉戌亥].{0,4}(?:含|藏)|(?:中|内)藏|余气|[辰戌丑未].{0,4}(?:火库|水库|金库|木库|土库|燥土|湿土)",
            answer,
        ):
            raise HeavenAgentError("解运结果使用了输入中未提供的藏干推断。")
        if re.search(
            r"木生火|火生土|土生金|金生水|水生木|木克土|土克水|水克火|火克金|金克木",
            answer,
        ):
            raise HeavenAgentError("解运结果自行扩展了输入中未提供的五行生克链。")
        if re.search(
            r"(?:本命)?日主.{0,32}(?:生扶|泄耗|受克|被克|得生|被生|偏强|偏弱)",
            answer,
        ):
            raise HeavenAgentError("解运结果把当日关系错误扩展成了本命强弱推断。")
        if re.search(
            r"(?:日柱)?[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]"
            r".{0,8}(?:本身|自身)(?:就)?是[木火土金水]",
            answer,
        ):
            raise HeavenAgentError("解运结果错误地把整个日柱归成了单一五行。")
        if re.search(
            r"[子丑寅卯辰巳午未申酉戌亥](?:的|具有|带有).{0,8}"
            r"(?:风木|君火|湿土|相火|燥金|寒水)(?:之)?(?:属性|性质)",
            answer,
        ):
            raise HeavenAgentError("解运结果把六气对应误写成了地支自身属性。")
        calculation = (context or {}).get("calculation") or {}
        if calculation.get("industry_symbols") and "行业影响" not in answer:
            raise HeavenAgentError("解运结果遗漏了本次必答的行业影响。")
        if re.search(
            r"行业.{0,16}(?:必涨|必跌|必然上涨|必然下跌|确定领涨|确定领跌|投资推荐)",
            answer,
        ):
            raise HeavenAgentError("解运结果把行业取象错误写成了行情预测或投资推荐。")
        personal = calculation.get("personal") or {}
        personal_today = personal.get("today_relative_to_natal_day_master") or {}
        pillar_values = {
            str(value)
            for group in (calculation.get("pillars") or {}, personal_today.get("pillars") or {})
            for value in group.values()
            if value
        }
        mentioned_pillars = set(
            re.findall(r"[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]", answer)
        )
        if mentioned_pillars - pillar_values:
            raise HeavenAgentError("解运结果补入了确定性输入中不存在的干支。")
        month_pillar = str((calculation.get("pillars") or {}).get("month") or "")
        month_branch = month_pillar[1:2]
        mentioned_month_branches = set(
            re.findall(r"([子丑寅卯辰巳午未申酉戌亥])月", answer)
        )
        if mentioned_month_branches - ({month_branch} if month_branch else set()):
            raise HeavenAgentError("解运结果补入了当前月份之外的地支月。")
        if personal:
            relations = {
                str(value)
                for value in (personal_today.get("stem_relations") or {}).values()
                if value
            }
            if "个人合参" not in answer and "本命日主" not in answer:
                raise HeavenAgentError("解运结果遗漏了本次必答的个人合参。")
            if relations and not any(relation in answer for relation in relations):
                raise HeavenAgentError("解运结果未使用程序提供的当日关系标签。")
    if mode != "heart":
        return
    calculation = (context or {}).get("calculation") or {}
    question = str(calculation.get("question") or "")
    if _contains_markdown_table(answer):
        raise HeavenAgentError("解卦结果错误输出了Markdown表格。")
    position_terms = (
        "持仓", "仓位", "建仓", "入场", "持有", "买入", "卖出", "止损", "止盈",
    )
    if not any(term in question for term in position_terms) and any(
        term in answer for term in position_terms
    ):
        raise HeavenAgentError("解卦结果擅自假定了用户的持仓或买卖状态。")
    financing_terms = (
        "融资", "借贷", "合作", "出资", "资金来源", "资金方", "投资人", "投资方",
        "外部资金", "外围资金", "外部资源", "外围资源",
    )
    invented_scenarios = (
        "融资", "借贷", "合作方", "资金提供方", "资金意向", "资金注入", "自有资金",
        "外围资金", "外围资源", "资金进入", "资源进入",
    )
    invented_resource_path = re.search(
        r"(?:资金|资源).{0,8}(?:来源|提供|注入|安排|路径)", answer
    )
    if not any(term in question for term in financing_terms) and (
        any(term in answer for term in invented_scenarios) or invented_resource_path
    ):
        raise HeavenAgentError("解卦结果擅自补入了用户没有提出的融资或合作场景。")
    timing_patterns = (
        r"未来\s*[一二三四五六七八九十\d]+\s*(?:个)?(?:交易)?日",
        r"[子丑寅卯辰巳午未申酉戌亥]{1,2}日(?:到来|来临|之前|之后|前后)",
        r"(?:等待|等到|待).{0,16}(?:旬空|空亡).{0,16}(?:填实|出空)",
    )
    if any(re.search(pattern, answer) for pattern in timing_patterns):
        raise HeavenAgentError("解卦结果错误使用旬空或干支推算了具体应期。")


def _contains_markdown_table(answer: str) -> bool:
    return bool(
        re.search(r"(?m)^\s*\|", answer)
        or re.search(r"(?m)^\s*:?-{3,}:?\s*\|", answer)
        or re.search(r"(?m)\|\s*:?-{3,}:?\s*(?:\||$)", answer)
    )
