"""階段 9-C:Cooper 「點頭」+「體悟整理」。

對應 Robert 的需求 ②③:
  ② 群組分享體悟 → 庫柏整理到排程系統 → 加深思考 → 引導大家
  ③ 庫柏總是在群組訊息時表示「有收到 / 已記錄」(不洗版)

設計:
  1. 每則訊息進來,跑一次 Haiku 判斷類別 + 重要性
     - 類別:assignment / acceptance / progress / question / 體悟 / decision / 閒聊
     - 重要性 0-1
  2. 重要性 < 0.5 或閒聊 → 完全不 ack(避免洗版)
  3. 重要性 >= 0.5 → ack 一則極短訊息(模板化,< 30 字)
     - 對體悟:「📝 已記《...》明天簡報撈出來討論」+ 寫 reflection note
     - 對求助:「👀 看到了,等下幫看」
     - 對決議:「✅ 記下:《...》」
     - 對進度:「📈 收到」
  4. 同 channel 5 分鐘冷卻,避免連 ack 多則洗版

成本評估(Haiku 4.5):
  - 輸入 ~250 tokens / 訊息 → $0.00025
  - 輸出 ~30 tokens / 訊息(只要 JSON 標籤)→ $0.00015
  - 平均 ~$0.0004/訊息
  - 8 人群組假設 200 訊息/天 → $0.08/天 ≈ $2.4/月
  - 加上 ack push 訊息(只對 50% 重要的)→ 模板字串,沒額外 LLM cost
  → 月支出預估 < $3,可控
"""
from __future__ import annotations

import asyncio
import json
import logging
import os
import re
from datetime import datetime, timedelta, timezone
from uuid import UUID

import anthropic
from sqlalchemy import select

from app.config import settings
from app.database import AsyncSessionLocal
from app.models.agent_activity import AgentActivity
from app.models.channel import Channel, Message
from app.models.user import User


logger = logging.getLogger("coba.acknowledge")


# ack 冷卻時段(每頻道內最短 ack 間隔,避免洗版)
ACK_COOLDOWN_MIN = int(os.environ.get("ACK_COOLDOWN_MIN", "5"))
# 重要性門檻
ACK_IMPORTANCE_MIN = float(os.environ.get("ACK_IMPORTANCE_MIN", "0.5"))
# 是否啟用 ack(預設關,讓 Robert 自己評估後 turn on)
ACK_ENABLED = os.environ.get("ACK_ENABLED", "false").lower() == "true"

# 強引用避免 GC
_PENDING: set = set()


CLASSIFY_PROMPT = """你是「群組訊息分類員 + 重要性評估員」。讀一則團隊訊息,判定類別 + 重要性。

【類別(只能挑一個)】
  - assignment       派工(指派誰做什麼)
  - acceptance       認領 / 確認(收到、OK、好)
  - progress         進度回報、做完了、卡住
  - question         求助、問問題
  - reflection       體悟、心得、復盤、學到、卡住、突破、教訓、沒想到、提醒自己、警惕、反省、第一次、才發現、原來、應該要、下次要、不該、悟到、頓悟
  - decision         決議、定案、不要做了、改方向
  - announcement     公告、通知、行政
  - chitchat         閒聊、午餐、貼圖、emoji 反應、感謝詞

【重要性 importance(0.0–1.0)】
  - 0.9–1.0:重大決議 / 客戶相關 / 阻塞中
  - 0.6–0.8:派工、進度、求助、體悟
  - 0.3–0.5:認領、公告
  - 0.0–0.3:閒聊、貼圖、感謝詞

【若 category = reflection,額外給】
  - tag:技術 / 客戶 / 團隊 / 自我管理 / 流程 / 商業 / 學習 / 其他
  - actionable:這個體悟是否可以變成 todo(true / false)
    例:「下次要先 review 才上線」→ true
    例:「今天才發現原來自己以前都搞錯方向」→ false(只是 insight)

【輸出嚴格 JSON,沒其他字】
{
  "category": "...",
  "importance": 0.0,
  "summary": "12 字內(體悟、決議要抓重點;閒聊用『—』)",
  "tag": "...(只在 reflection 時必填)",
  "actionable": true/false (只在 reflection 時必填)
}
"""


async def classify(content: str) -> dict | None:
    """跑 Haiku 分類。失敗回 None。"""
    if not settings.ANTHROPIC_API_KEY:
        return None
    try:
        client = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
        resp = await client.messages.create(
            model=settings.CLAUDE_MODEL_HAIKU,
            max_tokens=120,
            system=CLASSIFY_PROMPT,
            messages=[{"role": "user", "content": f"訊息:「{content[:600]}」"}],
        )
        raw = "".join(b.text for b in resp.content if b.type == "text")
        m = re.search(r"\{.*\}", raw, flags=re.DOTALL)
        if not m:
            return None
        return json.loads(m.group(0))
    except Exception:
        logger.exception("classify 失敗")
        return None


async def _recent_ack_count(channel_id: UUID, minutes: int) -> int:
    cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes)
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(
            select(AgentActivity).where(
                AgentActivity.activity_type == "ack",
                AgentActivity.created_at >= cutoff,
            )
        )).scalars().all()
    cid = str(channel_id)
    return sum(1 for r in rows if (r.extra or {}).get("channel_id") == cid)


def _ack_text(category: str, summary: str) -> str:
    summary = (summary or "").strip()
    if summary in ("—", "-", ""):
        summary = ""
    snippet = f"《{summary}》" if summary else ""
    if category == "reflection":
        return f"📝 已記{snippet} 等下幫你歸到知識庫"
    if category == "decision":
        return f"✅ 決議記下{snippet}"
    if category == "question":
        return f"👀 看到{snippet} 等等幫看"
    if category == "progress":
        return f"📈 收到進度{snippet}"
    if category == "announcement":
        return f"📌 公告收到{snippet}"
    return f"📥 收到{snippet}"


async def try_ack(message_id: UUID) -> None:
    """背景任務:跑 ack 流程。"""
    if not ACK_ENABLED:
        return
    async with AsyncSessionLocal() as db:
        msg = (await db.execute(select(Message).where(Message.id == message_id))).scalar_one_or_none()
        if msg is None or msg.user_id is None:        # 庫柏自己跳過
            return
        if msg.message_type != "text":
            return
        content = (msg.content or "").strip()
        if not content or len(content) < 4:           # 太短(emoji / 貼圖)跳過
            return
        ch = (await db.execute(select(Channel).where(Channel.id == msg.channel_id))).scalar_one_or_none()
        if ch is None or not ch.line_source_id:
            return
        # @庫柏 的訊息走原本 mention 路徑,這裡跳過(避免重複回應)
        if "庫柏" in content or "cooper" in content.lower() or "@coba" in content.lower():
            return

    # 冷卻
    if await _recent_ack_count(msg.channel_id, ACK_COOLDOWN_MIN) >= 1:
        return

    # 階段 9-H:防雙回應 — 如果這個 sender 在 engagement window 內,
    # Cooper 走 mention 路徑也會回(長 line_response),ack skip 避免「長+短兩則」。
    try:
        from app.services.coba_chat import is_in_engagement_db
        async with AsyncSessionLocal() as db:
            in_eng = await is_in_engagement_db(msg.channel_id, msg.user_id, db)
        if in_eng:
            return
    except Exception:
        pass

    # 跑分類
    result = await classify(content)
    if not result:
        return
    importance = float(result.get("importance") or 0.0)
    category = result.get("category") or "chitchat"
    summary = result.get("summary") or ""

    # 寫一筆「分類 log」(無論要不要 ack 都記)
    await _log("classify", f"{category} ({importance:.2f}): {summary[:30]}",
               user_id=msg.user_id, channel_id=msg.channel_id,
               extra={
                   "channel_id": str(msg.channel_id),
                   "message_id": str(message_id),
                   "category": category,
                   "importance": importance,
                   "summary": summary,
               })

    # 重要性低或閒聊 → 不 ack
    if importance < ACK_IMPORTANCE_MIN or category == "chitchat":
        return

    # === 體悟:完全靜默歸檔(不推 LINE,不打擾使用者)===
    # Robert 設計原則:「AI 不要輸出 AI 的想法,默默存就好」
    if category == "reflection":
        # 取 result 裡 Haiku 給的 tag(若有)
        ref_tag = (result.get("tag") or "").strip() or None
        actionable = bool(result.get("actionable"))
        # 跑重複偵測:Qdrant 比對過去 30 天 reflections
        similar_to = []
        try:
            from app.services.coba_memory import search_relevant
            sim_results = await search_relevant(query=content[:300], limit=15)
            cutoff_dt = datetime.now(timezone.utc) - timedelta(days=30)
            for s in (sim_results or []):
                if str(s.get("message_id")) == str(message_id):
                    continue
                # client side 過濾 30 天內 + 高相似度
                ts_str = s.get("created_at")
                if ts_str:
                    try:
                        dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
                        if dt < cutoff_dt:
                            continue
                    except (ValueError, TypeError):
                        pass
                if (s.get("score") or 0) >= 0.7:
                    similar_to.append({
                        "message_id": s.get("message_id"),
                        "score": round(s.get("score", 0), 3),
                        "preview": (s.get("content") or "")[:80],
                    })
            similar_to = similar_to[:5]
        except Exception:
            logger.exception("reflection 相似度比對失敗")

        await _log("reflection_note",
                   f"體悟:{summary[:60]}",
                   user_id=msg.user_id, channel_id=msg.channel_id,
                   extra={
                       "channel_id": str(msg.channel_id),
                       "message_id": str(message_id),
                       "raw": content[:500],
                       "summary": summary,
                       "tag": ref_tag,
                       "actionable": actionable,
                       "similar_to": similar_to,
                       "similar_count": len(similar_to),
                   })
        # 體悟「不」推 ack 訊息(完全靜默)
        return

    # push ack 訊息
    text = _ack_text(category, summary)
    try:
        from app.services import line_client
        await line_client.push_text(ch.line_source_id, text)
    except Exception:
        logger.exception("ack push 失敗")
    await _log("ack",
               f"{category}: {text}",
               user_id=msg.user_id, channel_id=msg.channel_id,
               extra={
                   "channel_id": str(msg.channel_id),
                   "message_id": str(message_id),
                   "category": category,
                   "importance": importance,
                   "ack_text": text,
               })


async def _log(activity_type: str, summary: str, user_id, channel_id, extra: dict):
    try:
        async with AsyncSessionLocal() as db:
            db.add(AgentActivity(
                activity_type=activity_type,
                user_id=user_id,
                summary=summary,
                extra=extra,
            ))
            await db.commit()
    except Exception:
        logger.exception("log %s failed", activity_type)


def trigger_ack(message_id: UUID) -> None:
    """fire-and-forget。"""
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return
    task = loop.create_task(try_ack(message_id))
    _PENDING.add(task)
    task.add_done_callback(_PENDING.discard)


# ============================================================
# 給 Cooper 工具用:列最近的體悟 / 反思紀錄
# ============================================================


async def list_recent_reflections(hours: int = 168) -> list[dict]:
    """列過去 N 小時的 reflection_note(預設一週)。"""
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(
            select(AgentActivity).where(
                AgentActivity.activity_type == "reflection_note",
                AgentActivity.created_at >= cutoff,
                AgentActivity.deleted_at.is_(None),
            ).order_by(AgentActivity.created_at.desc())
        )).scalars().all()
        # 收齊 user
        uids = list({r.user_id for r in rows if r.user_id})
        users_map = {}
        if uids:
            us = (await db.execute(select(User).where(User.id.in_(uids)))).scalars().all()
            users_map = {u.id: u.display_name for u in us}
    out = []
    for r in rows:
        e = r.extra or {}
        out.append({
            "id": str(r.id),                    # Cooper 用這個 id 才能 update / delete / convert
            "id_short": str(r.id)[:8],          # 短版,讓 Cooper 在自然語言裡指認
            "created_at": r.created_at.isoformat(),
            "by": users_map.get(r.user_id, "(未知)"),
            "summary": e.get("summary") or "",
            "raw": (e.get("raw") or "")[:200],
            "tag": e.get("tag"),
        })
    return out
