"""LLM 投资事件抽取的数据模型 (M4)。

EventExtraction 是 LLM 严格输出 schema(JSON mode 解析后用 Pydantic 校验)。
ExtractedEvent 把 EventExtraction 与原文章元数据合并,作为 M4 最终落盘格式。
"""

from __future__ import annotations

import re
from datetime import datetime
from enum import StrEnum
from typing import Self

from pydantic import BaseModel, Field, field_validator, model_validator


class Sentiment(StrEnum):
    """事件情绪倾向。"""

    POSITIVE = "positive"  # 利好
    NEUTRAL = "neutral"    # 中性
    NEGATIVE = "negative"  # 利空


# 事件类型枚举(Prompt 中也会展示给 LLM)
EVENT_TYPES: tuple[str, ...] = (
    "业绩预告",
    "业绩快报",
    "财报披露",
    "合作签约",
    "投资并购",
    "重大合同",
    "产品发布",
    "技术突破",
    "监管处罚",
    "诉讼仲裁",
    "股东减持",
    "股东增持",
    "回购",
    "分红",
    "高管变动",
    "资产重组",
    "停牌复牌",
    "ST警示",
    "退市风险",
    "宏观政策",
    "行业政策",
    "国际局势",
    "其他",
)

# A 股股票代码:6 位数字(000xxx/300xxx/600xxx 等),也可带 .SH/.SZ/.BJ 后缀
_STOCK_CODE_RE = re.compile(r"^\d{6}(\.(SH|SZ|BJ))?$")

# 重要程度合理区间(LLM 偶尔会给 0/6/10,这里夹紧)
MIN_IMPORTANCE = 1
MAX_IMPORTANCE = 5


class EventExtraction(BaseModel):
    """LLM 输出的 JSON 直接映射到此模型。"""

    stock_codes: list[str] = Field(
        default_factory=list,
        description="A 股 6 位代码,允许带 .SH/.SZ/.BJ 后缀;无相关股票时为空",
    )
    company_names: list[str] = Field(
        default_factory=list, description="涉及公司中文简称,无关时为空"
    )
    industries: list[str] = Field(
        default_factory=list, description="所属行业(申万二级粒度优先);无关时为空"
    )
    sentiment: Sentiment = Field(..., description="positive/neutral/negative")
    importance: int = Field(
        ..., ge=MIN_IMPORTANCE, le=MAX_IMPORTANCE, description="1-5 重要程度"
    )
    event_type: str = Field(..., description="事件类型,见 EVENT_TYPES")
    summary: str = Field(
        default="",
        max_length=200,
        description="一句话事件摘要(≤ 100 字),便于人工浏览",
    )

    @field_validator("stock_codes")
    @classmethod
    def _strip_and_validate_stock_codes(cls, v: list[str]) -> list[str]:
        """剔除空字符串、统一大写、过滤明显非法格式。"""
        cleaned: list[str] = []
        for code in v:
            s = (code or "").strip().upper().replace(" ", "")
            if not s:
                continue
            if _STOCK_CODE_RE.match(s):
                cleaned.append(s)
        # 去重保持顺序
        seen: set[str] = set()
        out: list[str] = []
        for c in cleaned:
            if c not in seen:
                seen.add(c)
                out.append(c)
        return out

    @field_validator("company_names", "industries")
    @classmethod
    def _strip_text_lists(cls, v: list[str]) -> list[str]:
        cleaned = [(s or "").strip() for s in v]
        cleaned = [s for s in cleaned if s]
        seen: set[str] = set()
        out: list[str] = []
        for c in cleaned:
            if c not in seen:
                seen.add(c)
                out.append(c)
        return out

    @field_validator("event_type")
    @classmethod
    def _normalize_event_type(cls, v: str) -> str:
        s = (v or "").strip()
        if not s:
            return "其他"
        return s

    @model_validator(mode="after")
    def _post_check(self) -> Self:
        """中性情绪时 importance 应较低(1-3),纠正常见误判。"""
        # 不强制纠正,留作后续校准。占位,便于以后扩展。
        return self


class ExtractedEvent(BaseModel):
    """落盘格式:文章元数据 + LLM 抽取结果 + 调用元信息。"""

    # ---- 来源标识 ----
    source_id: str
    url: str
    url_hash: str
    title: str
    publish_time: datetime | None = None

    # ---- 抽取结果 ----
    event: EventExtraction

    # ---- 调用元信息 ----
    provider: str = Field(..., description="deepseek / qwen 等")
    model: str
    extracted_at: datetime = Field(default_factory=datetime.now)
    attempts: int = Field(default=1, ge=1, description="LLM 实际调用次数(含重试)")
    prompt_tokens: int | None = None
    completion_tokens: int | None = None

    def short_summary(self) -> str:
        ev = self.event
        codes = ",".join(ev.stock_codes) or "-"
        return (
            f"[{self.source_id}] {self.title[:30]} "
            f"-> {ev.sentiment.value}/{ev.importance}/{ev.event_type} "
            f"({codes})"
        )


class LLMCallError(Exception):
    """LLM 调用失败(网络 / 解析 / 校验)。"""

    def __init__(self, reason: str, *, attempts: int = 0) -> None:
        super().__init__(reason)
        self.reason = reason
        self.attempts = attempts
