"""
庫柏(Cooper)的工具集(階段 7-tools)。

這些是 Cooper 在對話中實際可以「執行」的動作 — 不是只能講,還能做。
透過 Anthropic Claude 的 tool use API 呼叫。

提供工具:
  - create_task         建立任務
  - list_tasks          查任務
  - update_task_status  改任務狀態(完成 / 進行中 / 取消)
  - delete_task         刪任務
  - get_team_members    取得團隊成員清單(給 LLM 知道誰可以指派)

設計重點:
  1. 每個工具回傳的 dict 直接 json.dumps 給 Claude 看,結構要對 LLM 友善
  2. 失敗用 {"error": "..."} 回,Claude 會自然解讀 + 跟使用者解釋
  3. 不 raise,也不靜默吞;失敗訊息要具體
  4. 寫入操作會記 AgentActivity,讓「庫柏動態」面板看得到
"""

from __future__ import annotations

import logging
from datetime import date, datetime, timezone
from typing import Any
from uuid import UUID

from sqlalchemy import or_, select

from app.database import AsyncSessionLocal
from app.models.agent_activity import AgentActivity
from app.models.task import Task
from app.models.user import User


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


# ============================================================
# Tool 定義(給 Anthropic API)
# ============================================================

TOOLS_SCHEMA = [
    {
        "name": "create_task",
        "description": (
            "建立一個新任務在團隊的行程規畫系統。"
            "當使用者說「幫某人建任務」、「把 X 加到任務」、「派 Y 給阿綠」這類明確命令時用。"
            "成功會回傳 task_id 跟細節。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "任務標題,動詞開頭,15-50 字最好"
                },
                "assignee_name": {
                    "type": "string",
                    "description": "負責人的 display_name(必須跟團隊成員實際名字一致;先用 get_team_members 確認)。沒指定就空字串。"
                },
                "due_date": {
                    "type": "string",
                    "description": "截止日期 YYYY-MM-DD。沒指定就 null。今天用今天日期。「明天/週三/下週」自己換算成日期。"
                },
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "urgent"],
                    "description": "預設 medium。看到「急」「緊」「重要」拉 high 或 urgent"
                },
                "description": {
                    "type": "string",
                    "description": "任務細節說明,可選"
                }
            },
            "required": ["title"]
        }
    },
    {
        "name": "list_tasks",
        "description": (
            "查詢任務清單。當使用者問「我有什麼任務」、「阿綠的任務」、"
            "「最近建了什麼」、「進行中的任務」時用。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "assignee_name": {
                    "type": "string",
                    "description": "篩選負責人。空字串 = 不篩"
                },
                "status": {
                    "type": "string",
                    "enum": ["todo", "in_progress", "review", "done", "cancelled", "any"],
                    "description": "篩選狀態。'any' 或省略 = 全部"
                },
                "limit": {
                    "type": "integer",
                    "description": "最多回幾筆,預設 10,最多 50"
                }
            }
        }
    },
    {
        "name": "update_task_status",
        "description": (
            "更新任務狀態。當使用者說「X 任務完成」、「Y 在做了」、「Z 取消」時用。"
            "用 task_id_prefix(8 字)或 title_keyword 找任務。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id_prefix": {
                    "type": "string",
                    "description": "任務 UUID 前 8 字(從 list_tasks 回傳的 id 取),最精確"
                },
                "title_keyword": {
                    "type": "string",
                    "description": "或用任務標題關鍵字模糊找(沒 id 才用)"
                },
                "new_status": {
                    "type": "string",
                    "enum": ["todo", "in_progress", "review", "done", "cancelled"]
                }
            },
            "required": ["new_status"]
        }
    },
    {
        "name": "delete_task",
        "description": "軟刪任務(可恢復)。使用者說「刪掉 X」、「取消那個任務」時用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "task_id_prefix": {"type": "string"},
                "title_keyword": {"type": "string"},
                "reason": {"type": "string", "description": "為什麼刪(audit 用)"}
            }
        }
    },
    {
        "name": "get_team_members",
        "description": "取得團隊全部成員的 display_name 清單。在指派任務前用,確認名字拼對。",
        "input_schema": {
            "type": "object",
            "properties": {}
        }
    },
    {
        "name": "share_dashboard_link",
        "description": (
            "給對方「工作台永久網址」+ 加到主畫面教學(用密碼或 Face ID 解鎖手機 = 免登)。"
            "適用情境:任何人問「工作台在哪」「我要進去看任務」「給我連結」「dashboard 網址」。"
            "回應時直接貼永久 URL + 簡短教學「加到主畫面以後免登」。"
            "可選參數 quick_login=true:額外多給一個 30 分鐘一次性 magic link,只用在"
            "「對方密碼忘了 / 緊急要立刻進不想登入」的場景。一般情況不用。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "quick_login": {
                    "type": "boolean",
                    "description": "true = 多給一個 30 分鐘一次性免密碼連結(緊急用),預設 false"
                }
            }
        }
    },
    {
        "name": "list_assignment_status",
        "description": (
            "列出最近 N 小時(預設 24)系統「自動建出」的任務 + 認領狀態(誰接了 / 誰還沒接)。"
            "用在使用者問「最近派的任務誰接了?」「有誰還沒回覆?」「上週派工狀態」"
            "回傳每筆:title / assignee / status / accepted(true/false) / accepted_at。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "hours": {"type": "integer", "description": "回看幾小時(預設 24,最大 168)"},
                "channel_only": {"type": "boolean", "description": "只看當前頻道(預設 false 看全部)"},
            }
        }
    },
    {
        "name": "get_team_completion_rate",
        "description": (
            "列團隊每個成員「本週」的任務完成率 — 完成數 / 總數 + %。"
            "用在使用者問「團隊本週進度」「誰最忙」「KPI」「完成率」。"
            "回傳每人 {name, total, done, percent}。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "days": {"type": "integer", "description": "回看天數(預設 7 = 本週)"},
            }
        }
    },
    {
        "name": "save_reflection",
        "description": (
            "把一段話存進「體悟 / 復盤」紀錄(寫進 reflection_note,**不會建任務、不會出現在任務看板**)。"
            "使用情境(必用此工具,不准用 create_task):"
            "  • 使用者明確說『記錄起來 / 記下 / 存起來』而內容是反思類(覆盤、教訓、學到、反省)"
            "  • 使用者打『覆盤』『體悟』『反省』『教訓』『學到』開頭的訊息"
            "  • 訊息內容是『下次要 X』『以後要記得 Y』這種自我提醒"
            "  • 訊息是事後的反思、洞察、心得,不是要 todo 的事項"
            "存進去後系統自動跑重複偵測,週六覆盤也會撈出來。"
            "你回應簡短:「✓ 記下來了」或「✓ 兩條都記了」就好,不要碎念。"
        ),
        "input_schema": {
            "type": "object",
            "required": ["content"],
            "properties": {
                "content": {"type": "string", "description": "要存的完整內容(原文或整理過的)"},
                "summary": {"type": "string", "description": "12 字內摘要(選填)"},
                "tag": {"type": "string", "description": "技術 / 客戶 / 團隊 / 自我管理 / 流程 / 商業 / 學習 / 其他"},
            }
        }
    },
    {
        "name": "list_recent_reflections",
        "description": (
            "列過去 N 小時(預設 168 = 一週)群組裡分享的「體悟 / 反思 / 心得」紀錄,"
            "由系統自動分類抓出。用在使用者問「最近大家有什麼學到 / 思考 / 體悟」、"
            "「上週的反思整理一下」、「我們團隊最近覺得什麼最重要」。"
            "回傳每筆 {created_at, by, summary, raw}。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "hours": {"type": "integer", "description": "回看小時數(預設 168 = 一週)"},
            }
        }
    },
]


# ============================================================
# 工具執行邏輯
# ============================================================


async def _find_user_by_name(db, name: str) -> User | None:
    """模糊比對成員名稱(支援部分相符,優先 exact)。"""
    if not name:
        return None
    # 先 exact
    u = (await db.execute(
        select(User).where(User.display_name == name, User.deleted_at.is_(None))
    )).scalar_one_or_none()
    if u:
        return u
    # 再 contains
    u = (await db.execute(
        select(User).where(
            User.display_name.contains(name),
            User.deleted_at.is_(None),
            User.is_active.is_(True),
        )
    )).scalars().first()
    return u


async def _find_task(db, task_id_prefix: str | None, title_keyword: str | None) -> Task | None:
    if task_id_prefix:
        # SQLite Uuid 存成 BLOB,query 時用 str 比對 hex prefix
        # 簡化:撈所有未刪任務,找 prefix 相符的(小團隊任務量不大)
        all_tasks = (await db.execute(
            select(Task).where(Task.deleted_at.is_(None))
            .order_by(Task.created_at.desc())
            .limit(200)
        )).scalars().all()
        prefix = task_id_prefix.lower().replace("-", "")
        for t in all_tasks:
            if str(t.id).replace("-", "").lower().startswith(prefix):
                return t
    if title_keyword:
        t = (await db.execute(
            select(Task).where(
                Task.title.contains(title_keyword),
                Task.deleted_at.is_(None),
            ).order_by(Task.created_at.desc())
        )).scalars().first()
        return t
    return None


async def _log(activity_type: str, summary: str, user_id: UUID | None, extra: dict | None = None):
    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 activity failed")


async def _broadcast_data_changed(entity: str, **extra) -> None:
    """工具改完 DB 後 push WS 事件,前端對應 view 自動 refresh,不用 F5。

    entity: 'tasks' / 'reflections' / 'meetings' / 'users'
    extra:  附加資訊(例:task_id, action='create'/'update'/'delete')
    """
    try:
        from app.core.ws_manager import manager as ws_manager
        await ws_manager.broadcast({
            "type": "data_changed",
            "entity": entity,
            **extra,
        })
    except Exception:
        logger.exception("broadcast data_changed failed entity=%s", entity)


async def tool_create_task(args: dict, requester_id: UUID) -> dict:
    title = (args.get("title") or "").strip()
    if not title:
        return {"error": "title 是必填"}
    if len(title) > 200:
        title = title[:200]

    async with AsyncSessionLocal() as db:
        # 找 assignee
        assignee_id = None
        assignee_name_actual = None
        assignee_name = (args.get("assignee_name") or "").strip()
        if assignee_name:
            u = await _find_user_by_name(db, assignee_name)
            if u:
                assignee_id = u.id
                assignee_name_actual = u.display_name
            else:
                return {
                    "error": f"找不到團隊成員「{assignee_name}」",
                    "hint": "用 get_team_members 看正確名字",
                }

        # due_date
        due_date = None
        due_str = args.get("due_date")
        if due_str:
            try:
                due_date = datetime.strptime(due_str, "%Y-%m-%d").date()
            except (ValueError, TypeError):
                return {"error": f"due_date 格式錯誤(期望 YYYY-MM-DD,給的是 {due_str!r})"}

        priority = (args.get("priority") or "medium").lower()
        if priority not in ("low", "medium", "high", "urgent"):
            priority = "medium"

        task = Task(
            title=title,
            description=args.get("description") or None,
            priority=priority,
            due_date=due_date,
            assignee_id=assignee_id,
            created_by=requester_id,
            status="todo",
        )
        db.add(task)
        await db.commit()
        await db.refresh(task)
        task_id = str(task.id)

    await _log(
        activity_type="task_create_auto",
        summary=f"庫柏建任務:{title}",
        user_id=requester_id,
        extra={
            "task_id": task_id,
            "assignee_name": assignee_name_actual,
            "via": "tool_call",
        },
    )

    return {
        "success": True,
        "task_id_short": task_id[:8],
        "task_id_full": task_id,
        "title": title,
        "assignee": assignee_name_actual or "未指派",
        "due_date": due_date.isoformat() if due_date else None,
        "priority": priority,
        "status": "todo",
    }


async def tool_list_tasks(args: dict, requester_id: UUID) -> dict:
    async with AsyncSessionLocal() as db:
        stmt = select(Task).where(Task.deleted_at.is_(None))

        st = (args.get("status") or "any").lower()
        if st in ("todo", "in_progress", "review", "done", "cancelled"):
            stmt = stmt.where(Task.status == st)

        assignee_name = (args.get("assignee_name") or "").strip()
        assignee_filter = None
        if assignee_name:
            u = await _find_user_by_name(db, assignee_name)
            if u:
                stmt = stmt.where(Task.assignee_id == u.id)
                assignee_filter = u.display_name
            else:
                return {"error": f"找不到「{assignee_name}」", "tasks": []}

        limit = min(max(1, int(args.get("limit") or 10)), 50)
        stmt = stmt.order_by(Task.created_at.desc()).limit(limit)
        tasks = (await db.execute(stmt)).scalars().all()

        # 拉 assignee 名字
        assignee_ids = {t.assignee_id for t in tasks if t.assignee_id}
        assignee_map = {}
        if assignee_ids:
            users = (await db.execute(
                select(User).where(User.id.in_(assignee_ids))
            )).scalars().all()
            assignee_map = {u.id: u.display_name for u in users}

    return {
        "filter": {
            "status": st if st != "any" else "all",
            "assignee": assignee_filter,
        },
        "count": len(tasks),
        "tasks": [
            {
                "id_short": str(t.id)[:8],
                "title": t.title,
                "status": t.status,
                "priority": t.priority,
                "due_date": t.due_date.isoformat() if t.due_date else None,
                "assignee": assignee_map.get(t.assignee_id) if t.assignee_id else None,
            }
            for t in tasks
        ],
    }


async def tool_update_task_status(args: dict, requester_id: UUID) -> dict:
    new_status = (args.get("new_status") or "").lower()
    if new_status not in ("todo", "in_progress", "review", "done", "cancelled"):
        return {"error": f"new_status 必須是 todo/in_progress/review/done/cancelled,給的是 {new_status!r}"}

    async with AsyncSessionLocal() as db:
        task = await _find_task(db, args.get("task_id_prefix"), args.get("title_keyword"))
        if task is None:
            return {"error": "找不到任務(用 task_id_prefix 8 字 或 title_keyword)"}
        old_status = task.status
        task.status = new_status
        if new_status == "done":
            task.completed_at = datetime.now(timezone.utc)
        else:
            task.completed_at = None
        await db.commit()
        title = task.title
        tid = str(task.id)

    await _log(
        activity_type="task_create_auto",   # reuse type
        summary=f"庫柏改狀態 {title}: {old_status} → {new_status}",
        user_id=requester_id,
        extra={"task_id": tid, "old_status": old_status, "new_status": new_status, "via": "tool_call"},
    )
    return {
        "success": True,
        "task_id_short": tid[:8],
        "title": title,
        "old_status": old_status,
        "new_status": new_status,
    }


async def tool_delete_task(args: dict, requester_id: UUID) -> dict:
    async with AsyncSessionLocal() as db:
        task = await _find_task(db, args.get("task_id_prefix"), args.get("title_keyword"))
        if task is None:
            return {"error": "找不到任務"}
        title = task.title
        tid = str(task.id)
        task.deleted_at = datetime.now(timezone.utc)
        task.status = "cancelled"
        await db.commit()

    await _log(
        activity_type="task_cancel",
        summary=f"庫柏刪任務:{title}",
        user_id=requester_id,
        extra={"task_id": tid, "reason": args.get("reason"), "via": "tool_call"},
    )
    return {"success": True, "task_id_short": tid[:8], "title": title}


async def tool_share_dashboard_link(args: dict, requester_id: UUID) -> dict:
    """預設給「永久工作台 URL」+ 加到主畫面說明。

    參數 quick_login=true(可選):多回一個 30 分鐘 magic link 給「沒空登入、要立刻進」的場景。
    """
    base_url = "https://cooper-nas.taila49e28.ts.net"
    quick = bool(args.get("quick_login"))
    result = {
        "permanent_url": base_url + "/",
        "note": (
            "這是工作台永久網址。第一次:打開 → 用 email + 密碼登一次 → "
            "iPhone Safari 點底部「分享」→「加入主畫面」"
            "(Android Chrome:右上「⋮」→「安裝應用程式」)"
            "之後從手機桌面 icon 開,Face ID 解鎖手機後直接進系統,永遠免登入。"
        ),
    }
    if quick and requester_id is not None:
        from app.services.magic_link import generate_magic_link
        result["quick_login_link"] = generate_magic_link(requester_id)
        result["quick_login_valid_seconds"] = 1800
        result["quick_login_note"] = "緊急免密碼進入(30 分鐘一次性,密碼忘了 / 急著看才用)"
    return result


async def tool_get_team_members(args: dict, requester_id: UUID) -> dict:
    async with AsyncSessionLocal() as db:
        users = (await db.execute(
            select(User)
            .where(User.is_active.is_(True), User.deleted_at.is_(None))
            .order_by(User.created_at)
        )).scalars().all()
    return {
        "members": [
            {
                "display_name": u.display_name,
                "role": u.role,
                "is_owner": u.role == "owner",
            }
            for u in users
        ]
    }


# ============================================================
# Dispatcher
# ============================================================


async def tool_list_assignment_status(args: dict, requester_id: UUID) -> dict:
    from app.services.task_acceptance import list_assignment_status
    hours = int(args.get("hours") or 24)
    hours = max(1, min(hours, 168))
    items = await list_assignment_status(channel_id=None, hours=hours)
    if not items:
        return {"hours": hours, "items": [], "summary": f"最近 {hours} 小時沒有自動建出的任務"}
    pending = [i for i in items if not i["accepted"]]
    accepted = [i for i in items if i["accepted"]]
    return {
        "hours": hours,
        "total": len(items),
        "accepted_count": len(accepted),
        "pending_count": len(pending),
        "items": [
            {
                "title": i["title"],
                "assignee": i["assignee_name"],
                "status": i["status"],
                "accepted": i["accepted"],
                "accepted_at": i["accepted_at"],
            }
            for i in items
        ],
    }


async def tool_get_team_completion_rate(args: dict, requester_id: UUID) -> dict:
    from datetime import datetime, timezone, timedelta as td
    days = int(args.get("days") or 7)
    days = max(1, min(days, 90))
    cutoff_dt = datetime.now(timezone.utc) - td(days=days)
    async with AsyncSessionLocal() as db:
        users = (await db.execute(
            select(User).where(User.is_active.is_(True), User.deleted_at.is_(None))
        )).scalars().all()
        rows = (await db.execute(
            select(Task).where(Task.deleted_at.is_(None))
        )).scalars().all()

        def _tz_safe(dt):
            """SQLite 存的 datetime 可能是 naive(UTC),補上 tzinfo 再比較。"""
            if dt is None:
                return None
            if dt.tzinfo is None:
                return dt.replace(tzinfo=timezone.utc)
            return dt

        relevant = [
            t for t in rows
            if (_tz_safe(t.created_at) and _tz_safe(t.created_at) >= cutoff_dt) or
               (_tz_safe(t.completed_at) and _tz_safe(t.completed_at) >= cutoff_dt)
        ]
        stats = {}
        for u in users:
            stats[u.id] = {"name": u.display_name, "total": 0, "done": 0}
        for t in relevant:
            uid = t.assignee_id or t.created_by
            if uid not in stats:
                continue
            stats[uid]["total"] += 1
            if t.status == "done":
                stats[uid]["done"] += 1
    # 階段 10-D:用「累積完成件數」排序,百分比是輔助指標(避免 1/2 → 50% 誤導)
    members = []
    for uid, s in stats.items():
        pct = round(s["done"] / s["total"] * 100) if s["total"] else 0
        members.append({
            "name": s["name"],
            "total": s["total"],
            "done": s["done"],
            "percent": pct,    # 保留百分比但不當主排序
        })
    # 主排序:完成件數高到低;副排序:總任務數
    members.sort(key=lambda m: (-m["done"], -m["total"]))

    # 加團隊統計給 Cooper 用人話講
    total_done = sum(m["done"] for m in members)
    avg_done = round(total_done / len(members), 1) if members else 0
    note = (
        f"用累積完成件數排序(不用百分比,因為 1/2 = 50% 跟 9/20 = 45% 看起來假性接近,"
        f"實際產出差很多)。本期合計 {total_done} 件、平均 {avg_done} 件/人。"
    )
    return {
        "days": days,
        "members": members,
        "team_total_done": total_done,
        "avg_done_per_person": avg_done,
        "ranking_note": note,
    }


async def tool_save_reflection(args: dict, requester_id: UUID) -> dict:
    """把內容存進 reflection_note(體悟紀錄),不會建任務。"""
    from datetime import datetime, timedelta, timezone
    content = (args.get("content") or "").strip()
    if not content:
        return {"error": "content 必填"}
    summary = (args.get("summary") or content[:50]).strip()
    tag = (args.get("tag") or "").strip() or None

    # 跑重複偵測(同 ack 路徑)
    similar_to = []
    try:
        from app.services.coba_memory import search_relevant
        sim = await search_relevant(query=content[:300], limit=12)
        cutoff_dt = datetime.now(timezone.utc) - timedelta(days=30)
        for s in (sim or []):
            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({
                    "score": round(s["score"], 3),
                    "preview": (s.get("content") or "")[:80],
                })
        similar_to = similar_to[:5]
    except Exception:
        logger.exception("save_reflection 相似度比對失敗")

    async with AsyncSessionLocal() as db:
        db.add(AgentActivity(
            activity_type="reflection_note",
            user_id=requester_id,
            summary=f"體悟:{summary[:60]}",
            extra={
                "raw": content[:1000],
                "summary": summary,
                "tag": tag,
                "similar_to": similar_to,
                "similar_count": len(similar_to),
                "via": "tool_call",
            },
        ))
        await db.commit()

    return {
        "success": True,
        "summary": summary,
        "tag": tag,
        "similar_count": len(similar_to),
        "note": (f"⚠ 過去 30 天有 {len(similar_to)} 則相似體悟"
                 if similar_to else "已歸檔,週六覆盤會帶出來"),
    }


async def tool_list_recent_reflections(args: dict, requester_id: UUID) -> dict:
    from app.services.coba_acknowledge import list_recent_reflections
    hours = int(args.get("hours") or 168)
    hours = max(1, min(hours, 24 * 30))
    items = await list_recent_reflections(hours=hours)
    return {
        "hours": hours,
        "count": len(items),
        "items": items[:30],
    }


async def _find_reflection(db, reflection_id_prefix: str | None, content_keyword: str | None) -> AgentActivity | None:
    """找一筆 reflection_note。用 id 前 8 碼或 raw/summary 關鍵字。"""
    from datetime import datetime, timedelta, timezone
    cutoff = datetime.now(timezone.utc) - timedelta(days=30)
    if reflection_id_prefix:
        rows = (await db.execute(
            select(AgentActivity).where(
                AgentActivity.activity_type == "reflection_note",
                AgentActivity.deleted_at.is_(None),
            ).order_by(AgentActivity.created_at.desc()).limit(200)
        )).scalars().all()
        prefix = reflection_id_prefix.lower().replace("-", "")
        for r in rows:
            if str(r.id).replace("-", "").lower().startswith(prefix):
                return r
    if content_keyword:
        kw = content_keyword.strip()
        rows = (await db.execute(
            select(AgentActivity).where(
                AgentActivity.activity_type == "reflection_note",
                AgentActivity.deleted_at.is_(None),
                AgentActivity.created_at >= cutoff,
            ).order_by(AgentActivity.created_at.desc()).limit(200)
        )).scalars().all()
        for r in rows:
            e = r.extra or {}
            if kw in (e.get("raw") or "") or kw in (e.get("summary") or ""):
                return r
    return None


async def tool_delete_reflection(args: dict, requester_id: UUID) -> dict:
    """軟刪覆盤(set deleted_at)。"""
    from datetime import datetime, timezone
    async with AsyncSessionLocal() as db:
        target = await _find_reflection(
            db,
            args.get("reflection_id_prefix"),
            args.get("content_keyword"),
        )
        if target is None:
            return {"error": "找不到對應覆盤(用 reflection_id_prefix 8 字 或 content_keyword)"}
        e = target.extra or {}
        old_summary = e.get("summary") or (e.get("raw") or "")[:50]
        rid = str(target.id)
        target.deleted_at = datetime.now(timezone.utc)
        await db.commit()

    await _log(
        activity_type="reflection_delete",
        summary=f"庫柏刪覆盤:{old_summary[:50]}",
        user_id=requester_id,
        extra={"reflection_id": rid, "old_summary": old_summary, "via": "tool_call"},
    )
    return {
        "success": True,
        "reflection_id_short": rid[:8],
        "old_summary": old_summary,
    }


async def tool_update_reflection(args: dict, requester_id: UUID) -> dict:
    """改覆盤的 content / summary / tag。"""
    new_content = args.get("new_content")
    new_summary = args.get("new_summary")
    new_tag = args.get("new_tag")
    if new_content is None and new_summary is None and new_tag is None:
        return {"error": "至少要給 new_content / new_summary / new_tag 其中一個"}

    async with AsyncSessionLocal() as db:
        target = await _find_reflection(
            db,
            args.get("reflection_id_prefix"),
            args.get("content_keyword"),
        )
        if target is None:
            return {"error": "找不到對應覆盤"}

        e = dict(target.extra or {})
        old_e = dict(e)

        if new_content is not None:
            new_content = new_content.strip()
            if not new_content:
                return {"error": "new_content 不能是空字串"}
            e["raw"] = new_content[:1000]
        if new_summary is not None:
            e["summary"] = new_summary.strip()[:80]
        if new_tag is not None:
            e["tag"] = new_tag.strip() or None

        target.extra = e
        # 改 summary 顯示欄位也跟上
        if "summary" in e and e["summary"]:
            target.summary = f"體悟:{e['summary'][:60]}"

        rid = str(target.id)
        await db.commit()

    await _log(
        activity_type="reflection_update",
        summary=f"庫柏改覆盤:{(e.get('summary') or '')[:50]}",
        user_id=requester_id,
        extra={
            "reflection_id": rid,
            "old": {k: old_e.get(k) for k in ("raw", "summary", "tag")},
            "new": {k: e.get(k) for k in ("raw", "summary", "tag")},
            "via": "tool_call",
        },
    )
    return {
        "success": True,
        "reflection_id_short": rid[:8],
        "summary": e.get("summary"),
        "tag": e.get("tag"),
    }


async def tool_convert_reflection_to_task(args: dict, requester_id: UUID) -> dict:
    """誤分類修正:把覆盤升級成任務,順便軟刪原覆盤。

    必填:
      - reflection_id_prefix(或 content_keyword)
      - title:任務標題
    選填:assignee_name / due_date / priority / description / mark_done(bool,直接標完成)
    """
    from datetime import datetime, timezone
    title = (args.get("title") or "").strip()
    if not title:
        return {"error": "title 必填(任務標題)"}

    # 找原覆盤
    async with AsyncSessionLocal() as db:
        target = await _find_reflection(
            db,
            args.get("reflection_id_prefix"),
            args.get("content_keyword"),
        )
        if target is None:
            return {"error": "找不到要轉成任務的覆盤"}
        old_e = target.extra or {}
        old_raw = old_e.get("raw") or ""
        old_rid = str(target.id)
        # 軟刪原覆盤
        target.deleted_at = datetime.now(timezone.utc)
        await db.commit()

    # 用既有 create_task 邏輯建任務(description 預填原 raw 內容)
    create_args = {
        "title": title,
        "assignee_name": args.get("assignee_name"),
        "due_date": args.get("due_date"),
        "priority": args.get("priority"),
        "description": args.get("description") or old_raw,
    }
    create_result = await tool_create_task(create_args, requester_id)
    if "error" in create_result:
        # 建任務失敗就把覆盤救回來(rollback)
        async with AsyncSessionLocal() as db:
            t = (await db.execute(
                select(AgentActivity).where(AgentActivity.id == target.id)
            )).scalar_one()
            t.deleted_at = None
            await db.commit()
        return {"error": f"建任務失敗,覆盤已回滾:{create_result['error']}"}

    new_task_id = create_result.get("task_id_short")

    # 如果 mark_done 就立刻標完成
    if args.get("mark_done"):
        await tool_update_task_status(
            {"task_id_prefix": new_task_id, "new_status": "done"},
            requester_id,
        )

    await _log(
        activity_type="reflection_convert_to_task",
        summary=f"庫柏把覆盤轉成任務:{title}",
        user_id=requester_id,
        extra={
            "old_reflection_id": old_rid,
            "old_raw": old_raw[:300],
            "new_task_id_short": new_task_id,
            "marked_done": bool(args.get("mark_done")),
            "via": "tool_call",
        },
    )
    return {
        "success": True,
        "old_reflection_id_short": old_rid[:8],
        "new_task_id_short": new_task_id,
        "title": title,
        "marked_done": bool(args.get("mark_done")),
        "note": "原覆盤已軟刪,新任務建好" + ("並已標 done" if args.get("mark_done") else ""),
    }


# ============================================================
# 階段 10-B 新工具(SDK 暖機後加,讓 Cooper 能完整管網站)
# ============================================================

async def tool_update_task(args: dict, requester_id: UUID) -> dict:
    """改任務的 due_date / priority / title / description / assignee。

    比 update_task_status 完整 — 改一切 task 欄位,但不改 status(用既有 update_task_status)。
    """
    async with AsyncSessionLocal() as db:
        task = await _find_task(db, args.get("task_id_prefix"), args.get("title_keyword"))
        if task is None:
            return {"error": "找不到任務(用 task_id_prefix 8 字 或 title_keyword)"}

        old_snapshot = {
            "title": task.title,
            "due_date": task.due_date.isoformat() if task.due_date else None,
            "priority": task.priority,
            "assignee_id": str(task.assignee_id) if task.assignee_id else None,
        }
        changes: dict[str, dict] = {}

        new_title = args.get("new_title")
        if new_title and new_title != task.title:
            changes["title"] = {"from": task.title, "to": new_title}
            task.title = new_title

        new_desc = args.get("new_description")
        if new_desc is not None and new_desc != task.description:
            changes["description"] = {"from": (task.description or "")[:50], "to": new_desc[:50]}
            task.description = new_desc

        new_priority = args.get("new_priority")
        if new_priority:
            new_priority = new_priority.lower()
            if new_priority not in ("low", "medium", "high", "urgent"):
                return {"error": f"new_priority 必須是 low/medium/high/urgent,給的是 {new_priority!r}"}
            if new_priority != task.priority:
                changes["priority"] = {"from": task.priority, "to": new_priority}
                task.priority = new_priority

        new_due = args.get("new_due_date")
        if new_due is not None:
            from datetime import date as _date
            if new_due == "" or new_due.lower() == "null":
                if task.due_date is not None:
                    changes["due_date"] = {"from": task.due_date.isoformat(), "to": None}
                    task.due_date = None
            else:
                try:
                    parsed = _date.fromisoformat(new_due)
                except ValueError:
                    return {"error": f"new_due_date 格式錯誤,要 YYYY-MM-DD,給的是 {new_due!r}"}
                if parsed != task.due_date:
                    changes["due_date"] = {
                        "from": task.due_date.isoformat() if task.due_date else None,
                        "to": parsed.isoformat(),
                    }
                    task.due_date = parsed

        new_assignee_name = args.get("new_assignee_name")
        if new_assignee_name is not None:
            if new_assignee_name == "" or new_assignee_name.lower() == "null":
                # 取消指派
                if task.assignee_id is not None:
                    changes["assignee"] = {"from_id": str(task.assignee_id), "to": None}
                    task.assignee_id = None
            else:
                target = await _find_user_by_name(db, new_assignee_name)
                if target is None:
                    return {"error": f"找不到名字含「{new_assignee_name}」的成員"}
                if task.assignee_id != target.id:
                    changes["assignee"] = {
                        "from_id": str(task.assignee_id) if task.assignee_id else None,
                        "to": target.display_name,
                        "to_id": str(target.id),
                    }
                    task.assignee_id = target.id

        if not changes:
            return {"success": True, "no_change": True, "task_id_short": str(task.id)[:8], "title": task.title}

        await db.commit()
        title = task.title
        tid = str(task.id)

    await _log(
        activity_type="task_create_auto",   # reuse 既有 type
        summary=f"庫柏改任務《{title}》: " + ", ".join(f"{k}" for k in changes.keys()),
        user_id=requester_id,
        extra={
            "task_id": tid,
            "changes": changes,
            "old_snapshot": old_snapshot,
            "via": "tool_call",
            "tool": "update_task",
        },
    )
    return {
        "success": True,
        "task_id_short": tid[:8],
        "title": title,
        "changes": changes,
    }


async def tool_reassign_task(args: dict, requester_id: UUID) -> dict:
    """轉指派任務 — `update_task` 的 sugar(只改 assignee)。"""
    return await tool_update_task(
        {
            "task_id_prefix": args.get("task_id_prefix"),
            "title_keyword": args.get("title_keyword"),
            "new_assignee_name": args.get("new_assignee_name"),
        },
        requester_id,
    )


async def tool_add_team_member(args: dict, requester_id: UUID) -> dict:
    """新增團隊成員。

    必填:display_name
    選填:role (member / manager / owner,預設 member)
          email
          line_user_id(LINE 個人 ID,Uxxxxxx 格式)
          job_title
    """
    display_name = (args.get("display_name") or "").strip()
    if not display_name:
        return {"error": "display_name 不能空"}

    role = (args.get("role") or "member").lower()
    if role not in ("owner", "manager", "member"):
        return {"error": f"role 必須是 owner/manager/member,給的是 {role!r}"}

    email = args.get("email")
    line_user_id = args.get("line_user_id")
    job_title = args.get("job_title")

    async with AsyncSessionLocal() as db:
        # 檢查 display_name 重複
        dup = (await db.execute(
            select(User).where(User.display_name == display_name, User.deleted_at.is_(None))
        )).scalar_one_or_none()
        if dup is not None:
            return {"error": f"已有同名成員「{display_name}」(id={str(dup.id)[:8]}),請改用別的 display_name"}

        # email 重複(若給了)
        if email:
            dup_e = (await db.execute(
                select(User).where(User.email == email, User.deleted_at.is_(None))
            )).scalar_one_or_none()
            if dup_e is not None:
                return {"error": f"email「{email}」已被「{dup_e.display_name}」使用"}

        # line_user_id 重複
        if line_user_id:
            dup_l = (await db.execute(
                select(User).where(User.line_user_id == line_user_id, User.deleted_at.is_(None))
            )).scalar_one_or_none()
            if dup_l is not None:
                return {"error": f"LINE id 已被「{dup_l.display_name}」使用"}

        # 建 user — password_hash 用 placeholder(請對方之後改密碼登入,或用 magic link 進來改)
        from app.core.security import hash_password
        new_user = User(
            display_name=display_name,
            email=email or f"placeholder.{display_name.lower().replace(' ', '_')}@coba.local",
            password_hash=hash_password("ChangeMe!_" + display_name),  # 一次性,要重設
            role=role,
            job_title=job_title,
            line_user_id=line_user_id,
            is_active=True,
        )
        db.add(new_user)
        await db.commit()
        await db.refresh(new_user)
        new_id = str(new_user.id)

    await _log(
        activity_type="task_create_auto",
        summary=f"庫柏新增成員:{display_name}({role})",
        user_id=requester_id,
        extra={
            "new_user_id": new_id,
            "display_name": display_name,
            "role": role,
            "has_email": bool(email),
            "has_line_user_id": bool(line_user_id),
            "via": "tool_call",
            "tool": "add_team_member",
        },
    )
    return {
        "success": True,
        "user_id_short": new_id[:8],
        "display_name": display_name,
        "role": role,
        "next_step": (
            "成員已建。請告知本人:用 email "
            f"「{email or '(請管理者改 email)'}」+ 預設密碼 ChangeMe!_{display_name} 登入,然後立刻去個人設定改密碼。"
            + ("LINE 已綁定。" if line_user_id else " 還沒綁 LINE,本人之後在 LINE 對 Cooper 機器人發 /bind 拿驗證碼綁定。")
        ),
    }


# ============================================================
# 階段 10-C 會議工具(LINE 對話直接記會議,不需錄音上傳)
# ============================================================

async def tool_record_meeting(args: dict, requester_id: UUID) -> dict:
    """直接從文字記錄一場會議。

    必填:title (字串)
    選填:
      - executive_summary:濃縮摘要(2-4 句)
      - key_topics:list[str] 主題
      - decisions:list[str] 或 list[dict{decision, context}] — 重要決策
      - action_items:list[str] 或 list[dict{description, assignee_name, due_date}] — 待辦事項
      - meeting_date:YYYY-MM-DD,沒給就用今天

    建出來:status='completed',audio_path=None(代表手動輸入)
    """
    from datetime import date as _date, datetime
    from app.models.meeting import (
        Meeting, MeetingSummary, MeetingDecision, MeetingActionItem,
    )

    title = (args.get("title") or "").strip()
    if not title:
        return {"error": "title 必填"}

    summary = (args.get("executive_summary") or "").strip()
    key_topics = args.get("key_topics") or []
    if isinstance(key_topics, str):
        key_topics = [key_topics]

    decisions = args.get("decisions") or []
    if isinstance(decisions, str):
        decisions = [decisions]

    action_items = args.get("action_items") or []
    if isinstance(action_items, str):
        action_items = [action_items]

    async with AsyncSessionLocal() as db:
        meeting = Meeting(
            title=title,
            status="completed",
            created_by=requester_id,
        )
        db.add(meeting)
        await db.flush()
        mid = meeting.id

        # summary
        if summary or key_topics:
            db.add(MeetingSummary(
                meeting_id=mid,
                executive_summary=summary or "(無摘要)",
                key_topics=list(key_topics) if key_topics else None,
                generated_by="cooper-line-record",
            ))

        # decisions
        for i, d in enumerate(decisions):
            if isinstance(d, str):
                db.add(MeetingDecision(meeting_id=mid, decision=d.strip(), order_index=i))
            elif isinstance(d, dict):
                db.add(MeetingDecision(
                    meeting_id=mid,
                    decision=(d.get("decision") or "").strip(),
                    context=(d.get("context") or None),
                    order_index=i,
                ))

        # action_items(若帶 assignee_name 試著解析成 user)
        for i, a in enumerate(action_items):
            if isinstance(a, str):
                db.add(MeetingActionItem(
                    meeting_id=mid, description=a.strip(), order_index=i,
                ))
            elif isinstance(a, dict):
                desc = (a.get("description") or "").strip()
                if not desc:
                    continue
                assignee_id = None
                aname = a.get("assignee_name")
                if aname:
                    u = await _find_user_by_name(db, aname)
                    if u:
                        assignee_id = u.id
                due = a.get("due_date")
                # due_date 簡單驗證(meeting_action_items.suggested_due_date 是字串)
                if due:
                    try:
                        _date.fromisoformat(due)
                    except ValueError:
                        due = None
                db.add(MeetingActionItem(
                    meeting_id=mid,
                    description=desc,
                    suggested_assignee_id=assignee_id,
                    suggested_due_date=due,
                    order_index=i,
                ))

        await db.commit()
        mid_str = str(mid)

    await _log(
        activity_type="meeting_record",
        summary=f"庫柏記會議:{title[:50]}",
        user_id=requester_id,
        extra={
            "meeting_id": mid_str,
            "title": title,
            "decisions_count": len(decisions),
            "action_items_count": len(action_items),
            "via": "tool_call",
        },
    )
    return {
        "success": True,
        "meeting_id_short": mid_str[:8],
        "title": title,
        "summary_added": bool(summary or key_topics),
        "decisions_count": len(decisions),
        "action_items_count": len(action_items),
        "url": f"https://cooper-nas.taila49e28.ts.net/?meeting={mid_str[:8]}",
        "note": "會議已記錄。dashboard 的會議分頁可看詳細。",
    }


async def tool_list_meetings(args: dict, requester_id: UUID) -> dict:
    """列最近會議。"""
    from app.models.meeting import Meeting

    limit = int(args.get("limit") or 10)
    limit = max(1, min(limit, 50))

    async with AsyncSessionLocal() as db:
        rows = (await db.execute(
            select(Meeting).where(Meeting.deleted_at.is_(None))
            .order_by(Meeting.created_at.desc()).limit(limit)
        )).scalars().all()

        out = []
        for m in rows:
            # lazy summary
            summ = None
            if m.id:
                from app.models.meeting import MeetingSummary
                s = (await db.execute(
                    select(MeetingSummary).where(MeetingSummary.meeting_id == m.id)
                )).scalar_one_or_none()
                if s:
                    summ = s.executive_summary[:120]
            out.append({
                "id_short": str(m.id)[:8],
                "title": m.title,
                "status": m.status,
                "created_at": m.created_at.isoformat() if m.created_at else None,
                "summary_preview": summ,
            })
    return {"count": len(out), "items": out}


async def tool_get_meeting_detail(args: dict, requester_id: UUID) -> dict:
    """看一場會議完整內容(摘要 + 決策 + action items)。"""
    from app.models.meeting import (
        Meeting, MeetingSummary, MeetingDecision, MeetingActionItem,
    )
    from sqlalchemy.orm import selectinload

    async def _find(db):
        prefix = (args.get("meeting_id_prefix") or "").strip()
        title_kw = (args.get("title_keyword") or "").strip()
        if prefix:
            rows = (await db.execute(
                select(Meeting).where(Meeting.deleted_at.is_(None))
                .order_by(Meeting.created_at.desc()).limit(200)
            )).scalars().all()
            p = prefix.lower().replace("-", "")
            for m in rows:
                if str(m.id).replace("-", "").lower().startswith(p):
                    return m
        if title_kw:
            return (await db.execute(
                select(Meeting).where(
                    Meeting.title.contains(title_kw),
                    Meeting.deleted_at.is_(None),
                ).order_by(Meeting.created_at.desc())
            )).scalars().first()
        return None

    async with AsyncSessionLocal() as db:
        m = await _find(db)
        if m is None:
            return {"error": "找不到會議(用 meeting_id_prefix 或 title_keyword)"}

        s = (await db.execute(
            select(MeetingSummary).where(MeetingSummary.meeting_id == m.id)
        )).scalar_one_or_none()

        decisions = (await db.execute(
            select(MeetingDecision).where(MeetingDecision.meeting_id == m.id)
            .order_by(MeetingDecision.order_index)
        )).scalars().all()

        items = (await db.execute(
            select(MeetingActionItem).where(MeetingActionItem.meeting_id == m.id)
            .order_by(MeetingActionItem.order_index)
        )).scalars().all()

        # action_item assignee names
        from app.models.user import User
        a_uids = list({a.suggested_assignee_id for a in items if a.suggested_assignee_id})
        u_map = {}
        if a_uids:
            us = (await db.execute(select(User).where(User.id.in_(a_uids)))).scalars().all()
            u_map = {u.id: u.display_name for u in us}

        return {
            "id_short": str(m.id)[:8],
            "title": m.title,
            "status": m.status,
            "created_at": m.created_at.isoformat() if m.created_at else None,
            "executive_summary": s.executive_summary if s else None,
            "key_topics": s.key_topics if s else None,
            "decisions": [
                {"decision": d.decision, "context": d.context}
                for d in decisions
            ],
            "action_items": [
                {
                    "description": a.description,
                    "assignee": u_map.get(a.suggested_assignee_id),
                    "due_date": a.suggested_due_date,
                    "converted_task_id": str(a.converted_task_id) if a.converted_task_id else None,
                }
                for a in items
            ],
        }


async def tool_delete_meeting(args: dict, requester_id: UUID) -> dict:
    """軟刪會議。"""
    from datetime import datetime, timezone
    from app.models.meeting import Meeting

    prefix = (args.get("meeting_id_prefix") or "").strip()
    title_kw = (args.get("title_keyword") or "").strip()
    if not prefix and not title_kw:
        return {"error": "至少要給 meeting_id_prefix 或 title_keyword"}

    async with AsyncSessionLocal() as db:
        m = None
        if prefix:
            rows = (await db.execute(
                select(Meeting).where(Meeting.deleted_at.is_(None))
                .order_by(Meeting.created_at.desc()).limit(200)
            )).scalars().all()
            p = prefix.lower().replace("-", "")
            for x in rows:
                if str(x.id).replace("-", "").lower().startswith(p):
                    m = x; break
        if m is None and title_kw:
            m = (await db.execute(
                select(Meeting).where(
                    Meeting.title.contains(title_kw),
                    Meeting.deleted_at.is_(None),
                )
            )).scalars().first()
        if m is None:
            return {"error": "找不到會議"}

        title = m.title
        mid_str = str(m.id)
        m.deleted_at = datetime.now(timezone.utc)
        await db.commit()

    await _log(
        activity_type="meeting_delete",
        summary=f"庫柏刪會議:{title[:50]}",
        user_id=requester_id,
        extra={"meeting_id": mid_str, "via": "tool_call"},
    )
    return {"success": True, "meeting_id_short": mid_str[:8], "title": title}


_DISPATCH = {
    "create_task": tool_create_task,
    "list_tasks": tool_list_tasks,
    "update_task_status": tool_update_task_status,
    "update_task": tool_update_task,
    "reassign_task": tool_reassign_task,
    "delete_task": tool_delete_task,
    "get_team_members": tool_get_team_members,
    "share_dashboard_link": tool_share_dashboard_link,
    "list_assignment_status": tool_list_assignment_status,
    "get_team_completion_rate": tool_get_team_completion_rate,
    "list_recent_reflections": tool_list_recent_reflections,
    "save_reflection": tool_save_reflection,
    "delete_reflection": tool_delete_reflection,
    "update_reflection": tool_update_reflection,
    "convert_reflection_to_task": tool_convert_reflection_to_task,
    "add_team_member": tool_add_team_member,
    "record_meeting": tool_record_meeting,
    "list_meetings": tool_list_meetings,
    "get_meeting_detail": tool_get_meeting_detail,
    "delete_meeting": tool_delete_meeting,
}


async def execute_tool(name: str, args: dict, requester_id: UUID) -> dict:
    """執行單一工具呼叫,失敗也回 dict(讓 Claude 看得懂)。"""
    fn = _DISPATCH.get(name)
    if fn is None:
        return {"error": f"未知工具:{name}"}
    try:
        return await fn(args or {}, requester_id)
    except Exception as e:
        logger.exception("tool %s failed", name)
        return {"error": f"工具執行例外:{type(e).__name__}: {e}"}
