"""快速新对话的首条消息自动命名。

这是平台 UI 元数据调用，不进入 agent loop、不改 working_dir。调用前后以
tasks.auto_title_pending + auto_title_version 为闸：人工 PATCH name 会清闸，清空
会递增版本，在途标题结果因此既不能覆盖用户命名，也不能跨会话轮次写回。
"""
from __future__ import annotations

import re
from typing import Any, Optional
from uuid import UUID

from sqlalchemy import select, update

from .agent_builder import ROOT, load_config
from .capabilities import ModelCapabilities
from .llm import LLM
from .storage import session_scope
from .storage.models import Task
from .storage.usage import record_chat_usage


_TITLE_PROMPT = """请给下面这条新对话消息生成一个简短、具体的中文对话标题。

规则：
1. 只输出标题本身，不要解释、引号、前缀或 Markdown。
2. 6～18 个汉字为宜，必要的英文缩写、材料牌号和数字可以保留。
3. 概括用户真正要做的事，不要使用“新对话”“咨询问题”“帮我处理”等空泛标题。
4. 不得包含斜杠或反斜杠。

用户消息：
{content}
"""

_PREFIX_RE = re.compile(r"^(?:标题|对话标题)\s*[:：]\s*", re.I)
_ATTACHMENT_LINE_RE = re.compile(r"^\[用户上传的(?:参考图|文件)\]\s+\S.*$")


def is_attachment_only_message(content: str) -> bool:
    """消息是否只包含前端注入的附件路径标记。"""
    lines = [line.strip() for line in (content or "").splitlines() if line.strip()]
    return bool(lines) and all(_ATTACHMENT_LINE_RE.fullmatch(line) for line in lines)


def clean_generated_title(raw: str, user_message: str) -> str:
    """把模型输出收敛为 validate_task_name 可接受的短标题，失败时按用户消息降级。"""
    first = next((line.strip() for line in (raw or "").splitlines() if line.strip()), "")
    title = _PREFIX_RE.sub("", first).strip(" \t\r\n\"'“”‘’`#*")
    title = re.sub(r"[/\\\x00]+", "·", title)
    title = re.sub(r"\s+", " ", title).strip(" .。,:：;；-—_")
    if not title or title == "新对话":
        fallback = next(
            (line.strip() for line in (user_message or "").splitlines() if line.strip()),
            "新对话",
        )
        title = re.sub(r"[/\\\x00]+", "·", fallback)
        title = re.sub(r"\s+", " ", title).strip(" .。,:：;；-—_")
    return (title or "新对话")[:24]


def generate_task_title(
    *,
    task_id: UUID,
    user_id: UUID,
    user_message: str,
    model_profile: str,
) -> Optional[str]:
    """若 task 仍待自动命名，调用一次模型并原子写标题；任何失败均不影响主 run。"""
    with session_scope() as s:
        state = s.execute(
            select(Task.auto_title_pending, Task.auto_title_version).where(
                Task.task_id == task_id,
                Task.user_id == user_id,
            )
        ).first()
    if state is None or not state.auto_title_pending:
        return None
    title_version = state.auto_title_version

    response: Any = None
    title: Optional[str] = None
    caps: Optional[ModelCapabilities] = None
    try:
        cfg = load_config()
        profile = model_profile or cfg["default_model"]
        caps = ModelCapabilities.load(profile, ROOT / cfg["models_dir"])
        response = LLM(caps).chat(
            messages=[{
                "role": "user",
                "content": _TITLE_PROMPT.format(content=user_message[:3000]),
            }],
            tools=None,
            max_retries=2,
        )
        choices = getattr(response, "choices", None) or []
        raw = ((choices[0].message.content if choices else "") or "").strip()
        title = clean_generated_title(raw, user_message)
    except Exception as e:
        print(
            f"[task_title] generate failed task={task_id}: "
            f"{type(e).__name__}: {e}",
            flush=True,
        )
        # 标题是一次性闸：若模型瞬时失败后仍只消费 pending，任务会永久停在
        # “新对话”。直接用首条消息的第一行作本地兜底，保证有效首发总能完成
        # 命名；不保留 pending 给后续消息重试，避免拿第二条消息误命名整段对话。
        title = clean_generated_title("", user_message)

    # 一次性消费 pending。WHERE pending=true 是与人工 PATCH name 的竞态闸：
    # 用户先改名时 PATCH 已清 false，此处 rowcount=0，不覆盖。
    with session_scope() as s:
        values: dict[str, Any] = {
            "auto_title_pending": False,
            "title_source": "auto",
        }
        if title:
            values["name"] = title
        result = s.execute(
            update(Task)
            .where(
                Task.task_id == task_id,
                Task.user_id == user_id,
                Task.auto_title_pending.is_(True),
                Task.auto_title_version == title_version,
            )
            .values(**values)
        )
        applied = bool(getattr(result, "rowcount", 0))
    if response is not None and caps is not None:
        usage = getattr(response, "usage", None)
        try:
            record_chat_usage(
                task_id=task_id,
                user_id=user_id,
                message_id=None,
                model_profile=f"{caps.family}.{caps.variant}",
                prompt_tokens=getattr(usage, "prompt_tokens", 0) or 0,
                completion_tokens=getattr(usage, "completion_tokens", 0) or 0,
                input_cny_per_mtoken=caps.input_cny_per_mtoken,
                output_cny_per_mtoken=caps.output_cny_per_mtoken,
                response=response,
                kind="task_title",
            )
        except Exception:
            pass
    return title if applied else None


def generate_task_title_safe(**kwargs: Any) -> Optional[str]:
    """后台辅助任务入口：DB/配置层异常也必须被吞掉，主对话永不受标题影响。"""
    try:
        return generate_task_title(**kwargs)
    except Exception as e:
        print(
            f"[task_title] auxiliary failed task={kwargs.get('task_id')}: "
            f"{type(e).__name__}: {e}",
            flush=True,
        )
        return None
