"""web 层共享常量与小 helper(从 app.py 析出,2026-07-23 拆分)。

只放「多个 router 都要用」的东西:实例/品牌常量、序列化 helper、task→JSON、
usage 聚合、SSE 帧格式。单一 router 专用的 helper 跟着各自 router 走,不进这里。
"""
from __future__ import annotations

import json
import os
from typing import Any, Optional

from sqlalchemy import select

from core.branding import brand_name
from core.storage.models import Task

# 蓝绿双实例部署(RUN.md B 档):实例名(blue/green),由 systemd 模板 unit 的
# per-instance env 注入;单实例部署不设 = ""。用途:① 起 run 时写 tasks.run_owner,
# 启动 reaper 只收自己实例的孤儿 ② sandbox 容器名/label 带实例色互不清扫(pool.py 同款
# 读法)③ /healthz 返回,蓝绿切换时验证 nginx 已指到新实例。
INSTANCE = os.getenv("ZCBOT_INSTANCE", "").strip()

# 对外品牌名:zcbot 只是内部项目代号,所有用户可见文案(页面标题 / 推送消息 / OAuth
# 提示页)统一用这个。/healthz 也返回,前端 boot 时拉取覆盖静态页里的默认值。
# 单一事实源在 core/branding.py(企业微信欢迎语等 web 层之外的文案也要用)。
BRAND = brand_name()

STATUS_FILTERS = ("active", "completed", "abandoned")
STATUS_WRITABLE = ("completed", "abandoned")  # web 不让从 web 端切回 active(走 CLI)
# 渠道镜像 task 的 channel 取值(每用户每渠道一条常驻只读对话):从普通任务列表排除,
# 改由 /v1/channel_tasks 单独取、前端做成固定卡片。新增渠道在此追加即可。
CHANNEL_MIRROR_KINDS = ("wechat", "wecom")
ORDER_FIELDS = ("created_at", "updated_at", "name", "status")
ORDER_DEFAULT = "-updated_at"


def norm_path(p: str) -> str:
    """跨 OS 显示归一:backslash → forward slash。"""
    return (p or "").replace("\\", "/")


def iso(dt: Optional[Any]) -> Optional[str]:
    return dt.isoformat() if dt else None


def outline_snippet(text: Optional[str], maxlen: int = 48) -> str:
    """user 消息正文 → 目录条目片段:取首个非空行,压平首尾空白,截断到 maxlen。"""
    if not text:
        return ""
    for line in text.splitlines():
        line = line.strip()
        if line:
            return line[:maxlen]
    return text.strip()[:maxlen]


def parse_ordering(s: Optional[str]) -> list:
    """DRF 风格 `ordering` 解析:逗号分隔多字段,`-` 前缀代表 desc。

    allowlist 见 `ORDER_FIELDS`;非法字段静默丢弃。全部非法或空串 → `ORDER_DEFAULT`(`-updated_at`)。
    返回 sqlalchemy `order_by` 列表(可直接 `*expand`)。
    """
    spec = (s or "").strip() or ORDER_DEFAULT
    cols = []
    for part in spec.split(","):
        p = part.strip()
        if not p:
            continue
        asc = True
        if p.startswith("-"):
            asc = False
            p = p[1:]
        if p in ORDER_FIELDS:
            col = getattr(Task, p)
            cols.append(col.asc() if asc else col.desc())
    if not cols:
        # 用户传了全无效字段 → fallback 默认
        cols = [Task.updated_at.desc()]
    # 所有允许字段都可能重复；UUID 兜底保证 OFFSET 分页在同一快照内稳定。
    last_desc = str(cols[-1]).upper().endswith(" DESC")
    cols.append(Task.task_id.desc() if last_desc else Task.task_id.asc())
    return cols


def task_dict(
    row: Any,
    *,
    n_messages: Optional[int] = None,
    usage: Optional[dict] = None,
) -> dict:
    """Task ORM row → API JSON dict。

    `usage`(可选)= `usage_aggregates` 算出的本 task 概要,带真实成本与缓存命中;
    缺省回退到 tasks.cost_cny 列(多为 0)与 0 命中,前端据此显 ¥ / 缓存命中率。
    """
    u = usage or {}
    # token 总量优先取 usage_events 聚合(用量 source-of-truth,且与 cache_hit 同源 →
    # 命中率分母一致、恒 ≤100%);无 usage 时回退 tasks 概览列。tasks.tokens_prompt 会被
    # 「清空对话」重置,不能与 usage_events 的 cache_hit 跨源相除。
    tokens_prompt = int(u["tokens_in"]) if "tokens_in" in u else (row.tokens_prompt or 0)
    tokens_completion = int(u["tokens_out"]) if "tokens_out" in u else (row.tokens_completion or 0)
    d = {
        "task_id": str(row.task_id),
        "name": row.name or "",
        "auto_title_pending": bool(getattr(row, "auto_title_pending", False)),
        "title_source": getattr(row, "title_source", None) or "manual",
        "description": row.description or "",
        "working_dir": norm_path(row.working_dir or ""),
        "status": row.status,
        "skill": row.skill or "",
        "channel": getattr(row, "channel", None) or "web",
        "model": row.model or "",
        "model_profile": row.model_profile or "",
        "tokens_prompt": tokens_prompt,
        "tokens_completion": tokens_completion,
        "tokens": tokens_prompt + tokens_completion,
        # 缓存命中 token(chat 前缀缓存)+ 真实成本(已按缓存折价,见 usage.py)。
        # on-the-fly 聚合;未传 usage 时回退列/0。
        "tokens_cache_hit": int(u.get("tokens_cache_hit", 0)),
        "cost_cny": float(u["cost_cny"]) if "cost_cny" in u else float(row.cost_cny or 0),
        # 当前 run 状态(0004 schema 简化:原 runs 表合并入 task)
        "run_status": row.run_status or "idle",
        "run_error": row.run_error or None,
        "created_at": iso(getattr(row, "created_at", None)),
        "updated_at": iso(getattr(row, "updated_at", None)),
    }
    if n_messages is not None:
        d["n_messages"] = n_messages
    return d


def sse_event(event_type: str, payload: dict) -> bytes:
    """格式化 SSE 一帧:`event: <type>` + `data: <json single-line>`。"""
    body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
    return f"event: {event_type}\ndata: {body}\n\n".encode("utf-8")


def assert_owns_task(s, tid, user_id) -> None:
    """task 归属校验(404 掩盖存在性,不泄露他人 task id 是否有效)。"""
    from fastapi import HTTPException
    ok = s.execute(
        select(Task.task_id).where(Task.task_id == tid, Task.user_id == user_id)
    ).first()
    if ok is None:
        raise HTTPException(404, f"task not found: {tid}")
