"""
Task Extractor(階段 7-B + 階段 9 主動模式):自動從訊息偵測「派工意圖」並抽出結構化任務。

被叫到的時機:
  - line_bridge 在訊息存好之後丟 background task 過來
  - messages.py(網頁版發訊息)同樣的點

設計:
  1. 只看內容 + 群裡的人員清單(避免拿到無關 user)
  2. 用 Haiku 4.5(快、便宜,夠準),不用 Sonnet
  3. 輸出 JSON:{is_task, confidence, assignee_name, title, due, priority, reasoning}
  4. 信心 < 0.5 整段 ignore
  5. 信心 0.5–0.65 → 「主動建議」soft push:「這要記成任務嗎?」(不真建)
  6. 信心 ≥ 0.65 → 寫 agent_activity + EXTRACTOR_LIVE=true 才真建任務 + 通知

階段 9 加上的「不洗版」防護:
  - 同頻道 60 分鐘最多 PROACTIVE_MAX_PER_HOUR 次主動行為
  - 23:00–07:00(台北)只記 log,不推 LINE
  - 已超過冷卻 → 高信心一樣建,但不推通知(避免半夜叮咚)
"""

from __future__ import annotations

import json
import logging
import os
import re
from datetime import date, datetime, time as dtime, timedelta, timezone
from uuid import UUID, uuid4

import anthropic
from sqlalchemy import and_, 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.task import Task
from app.models.user import User


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


# 全域開關:預設 dry-run,你看 1-2 天判定準不準才打開
EXTRACTOR_LIVE = os.environ.get("EXTRACTOR_LIVE", "false").lower() == "true"

# 階段 9:主動模式參數
PROACTIVE_MAX_PER_HOUR = int(os.environ.get("PROACTIVE_MAX_PER_HOUR", "3"))
PROACTIVE_QUIET_START = int(os.environ.get("PROACTIVE_QUIET_START", "23"))  # 23:00
PROACTIVE_QUIET_END = int(os.environ.get("PROACTIVE_QUIET_END", "7"))       # 07:00
PROACTIVE_BUILD_THRESHOLD = float(os.environ.get("PROACTIVE_BUILD_THRESHOLD", "0.65"))
PROACTIVE_SUGGEST_THRESHOLD = float(os.environ.get("PROACTIVE_SUGGEST_THRESHOLD", "0.5"))
TPE = timezone(timedelta(hours=8))


def _is_quiet_hours(now: datetime | None = None) -> bool:
    """台北 23:00-07:00 為靜音時段。"""
    now = now or datetime.now(TPE)
    if now.tzinfo is None:
        now = now.replace(tzinfo=TPE)
    h = now.astimezone(TPE).hour
    if PROACTIVE_QUIET_START < PROACTIVE_QUIET_END:
        return PROACTIVE_QUIET_START <= h < PROACTIVE_QUIET_END
    # 跨午夜
    return h >= PROACTIVE_QUIET_START or h < PROACTIVE_QUIET_END


async def _proactive_count_last_hour(channel_id: UUID) -> int:
    """這個頻道過去 60 分鐘有多少次主動行為(建任務 / 主動建議)。"""
    cutoff = datetime.now(timezone.utc) - timedelta(hours=1)
    async with AsyncSessionLocal() as db:
        rows = (await db.execute(
            select(AgentActivity).where(
                AgentActivity.activity_type.in_(["task_create_auto", "proactive_suggest"]),
                AgentActivity.created_at >= cutoff,
            )
        )).scalars().all()
    # 比對 extra.channel_id（不是 user_id）
    cid = str(channel_id)
    return sum(1 for r in rows if (r.extra or {}).get("channel_id") == cid)


EXTRACTOR_PROMPT = """你是「派工偵測員」。讀一則團隊訊息,判定是否在派工作給某人。

【判斷準則】
✓ 算派工:
  - 「阿綠你做 X」「請阿白處理 Y」「Z 這個你跟一下」
  - 「煥誠記得在 X 之前完成 Y」
  - 「明天前把客戶 A 的提案弄好」(沒指名 = 派給整個團隊,assignee 留 null)
  - 「@阿綠 把週五會議紀錄整理出來」

✗ 不算派工:
  - 「阿綠你覺得呢?」「等等阿綠來」(問問題 / 提及人)
  - 「我來做 X」「我先試試」(自我表態)
  - 「等等開會聊」「下午處理」(無動作對象)
  - 一般陳述、抱怨、感想、閒聊
  - 庫柏自己的訊息
  - **「覆盤」「體悟」「反省」「教訓」「學到」「下次要 X」「以後要 Y」開頭或主軸的訊息**
    → 一律 is_task: false,不論句中有沒有提到人名或動作。這是反思類,有獨立系統處理。

【輸出 JSON 格式】嚴格 JSON,不要 markdown,不要說明:
{
  "is_task": true/false,
  "confidence": 0.0-1.0,
  "title": "簡短任務描述(15 字內,動詞開頭)",
  "assignee_name": "對應到的成員顯示名稱(必須是『群裡成員清單』裡列出的)" 或 null,
  "due": "YYYY-MM-DD" 或 null,
  "priority": "low" | "medium" | "high" | "urgent",
  "reasoning": "20 字內,為什麼這樣判"
}

【範例】

訊息:「阿綠週三前把客戶 A 的提案 v2 寄出去」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
今日日期:2026-05-06(三)
→ {"is_task": true, "confidence": 0.95, "title": "寄出客戶 A 的提案 v2", "assignee_name": "神狙手阿綠", "due": "2026-05-06", "priority": "medium", "reasoning": "明確派給阿綠,有截止日"}

訊息:「Jonina 妳怎麼想」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
→ {"is_task": false, "confidence": 0.92, "title": "", "assignee_name": null, "due": null, "priority": "medium", "reasoning": "詢問意見,不是派工"}

訊息:「下週要把官網改版上線,很急」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
今日日期:2026-05-06
→ {"is_task": true, "confidence": 0.78, "title": "官網改版上線", "assignee_name": null, "due": "2026-05-13", "priority": "urgent", "reasoning": "未指人但動作明確"}

訊息:「等等開會聊」
→ {"is_task": false, "confidence": 0.95, "title": "", "assignee_name": null, "due": null, "priority": "medium", "reasoning": "非具體任務"}

只回 JSON,不要其他字。
"""


def _parse_json(raw: str) -> dict | None:
    """容錯地把 Haiku 回的字串解析成 dict。"""
    raw = raw.strip()
    m = re.search(r"\{.*\}", raw, flags=re.DOTALL)
    if not m:
        return None
    try:
        result = json.loads(m.group(0))
    except json.JSONDecodeError:
        return None
    if not isinstance(result, dict):
        return None
    return result


def _normalize_priority(p: str | None) -> str:
    if not p:
        return "medium"
    p = p.strip().lower()
    if p in ("low", "medium", "high", "urgent"):
        return p
    return "medium"


def _normalize_due(due_str: str | None, today: date | None = None) -> date | None:
    if not due_str:
        return None
    try:
        d = datetime.strptime(due_str, "%Y-%m-%d").date()
        # 別讓 Haiku 給過去的日期(除非真的是「今天」)
        if today and d < today:
            return None
        return d
    except (ValueError, TypeError):
        return None


async def _log_activity(
    activity_type: str,
    summary: str,
    user_id: UUID | None = None,
    extra: dict | None = None,
    cost_usd: float | None = None,
    duration_ms: int | None = None,
    success: bool = True,
) -> None:
    """寫一筆 AgentActivity(失敗吞掉)。"""
    try:
        async with AsyncSessionLocal() as db:
            db.add(AgentActivity(
                activity_type=activity_type,
                user_id=user_id,
                summary=summary,
                success=success,
                extra=extra,
                cost_usd=cost_usd,
                duration_ms=duration_ms,
            ))
            await db.commit()
    except Exception:
        logger.exception("寫 AgentActivity 失敗 type=%s", activity_type)


# ============================================================
# 主入口:處理一則訊息
# ============================================================


async def trigger_task_extract(message_id: UUID) -> None:
    """背景任務:對一則訊息跑派工偵測。"""
    if not settings.ANTHROPIC_API_KEY:
        return

    started = datetime.now(timezone.utc)

    async with AsyncSessionLocal() as db:
        msg = (await db.execute(
            select(Message).where(Message.id == message_id)
        )).scalar_one_or_none()
        if msg is None:
            return
        # 庫柏自己的訊息跳過
        if msg.user_id is None:
            return
        # 太短(<6 字)直接 ignore
        if len(msg.content.strip()) < 6:
            return
        # 不是 text 類型(圖片 / 語音 / 貼圖)跳過
        if msg.message_type != "text":
            return

        content = msg.content.strip()
        sender_id = msg.user_id
        channel_id = msg.channel_id

        # 抓 channel 裡的成員(限定群組)+ owner 兜底
        members = (await db.execute(
            select(User).where(User.is_active.is_(True), User.deleted_at.is_(None))
        )).scalars().all()
        member_lines = "\n".join(f"  - {u.display_name}" for u in members)
        members_for_match = {u.display_name: u.id for u in members}
        sender = next((u for u in members if u.id == sender_id), None)

    today = date.today()
    user_prompt = (
        f"訊息:「{content}」\n"
        f"傳訊者:{sender.display_name if sender else '未知'}\n"
        f"群裡成員:\n{member_lines}\n"
        f"今日日期:{today.isoformat()}\n"
    )

    try:
        client = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
        response = await client.messages.create(
            model=settings.CLAUDE_MODEL_HAIKU,
            max_tokens=300,
            system=EXTRACTOR_PROMPT,
            messages=[{"role": "user", "content": user_prompt}],
        )
        raw = "".join(b.text for b in response.content if b.type == "text")
        result = _parse_json(raw)
        if result is None:
            logger.warning("Haiku 回了無法解析的內容:%s", raw[:200])
            return
        is_task = bool(result.get("is_task", False))
        confidence = float(result.get("confidence", 0.0) or 0.0)

        # 算成本
        usage = response.usage
        cost = (usage.input_tokens / 1_000_000) * 1.0 + (usage.output_tokens / 1_000_000) * 5.0  # Haiku 4.5 pricing
        duration_ms = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)

        # 不管結果都記 log(用 'task_extract' type)
        await _log_activity(
            activity_type="task_extract",
            summary=(
                f"偵測派工:{result.get('title', '')[:30]}"
                if is_task and confidence >= PROACTIVE_BUILD_THRESHOLD
                else f"訊息不像派工(信心 {confidence:.2f})"
            ),
            user_id=sender_id,
            extra={
                "message_id": str(message_id),
                "channel_id": str(channel_id),
                "content_preview": content[:120],
                "is_task": is_task,
                "confidence": confidence,
                "title": result.get("title"),
                "assignee_name": result.get("assignee_name"),
                "due": result.get("due"),
                "priority": result.get("priority"),
                "reasoning": result.get("reasoning"),
                "live_mode": EXTRACTOR_LIVE,
            },
            cost_usd=cost,
            duration_ms=duration_ms,
        )

        # 完全不像派工 → 不動作
        if not is_task or confidence < PROACTIVE_SUGGEST_THRESHOLD:
            return

        title = (result.get("title") or "").strip()
        if not title:
            return

        # 階段 9:冷卻 + 靜音時段檢查
        recent_count = await _proactive_count_last_hour(channel_id)
        cooldown_hit = recent_count >= PROACTIVE_MAX_PER_HOUR
        quiet = _is_quiet_hours()

        # 取 channel(共用)
        async with AsyncSessionLocal() as db:
            ch = (await db.execute(
                select(Channel).where(Channel.id == channel_id)
            )).scalar_one_or_none()
        line_source_id = ch.line_source_id if ch else None

        # 對應 assignee / priority / due
        assignee_id = members_for_match.get(result.get("assignee_name") or "")
        priority = _normalize_priority(result.get("priority"))
        due_date = _normalize_due(result.get("due"), today)

        # === 階段 9:borderline 區間 → 主動建議模式(不真建,只 push 一句問句) ===
        if confidence < PROACTIVE_BUILD_THRESHOLD:
            if cooldown_hit or quiet or not EXTRACTOR_LIVE:
                logger.info(
                    "[soft-suggest skip] %s | conf=%.2f | cooldown=%s quiet=%s live=%s",
                    title, confidence, cooldown_hit, quiet, EXTRACTOR_LIVE,
                )
                return
            assignee_text = f"@{result.get('assignee_name')} " if result.get("assignee_name") else ""
            due_text = f"({due_date.isoformat()} 前)" if due_date else ""
            suggest = (
                f"🤔 這聽起來像個任務:\n"
                f"《{title}》{due_text}\n"
                f"{assignee_text}要我幫你開嗎?回「好」我就建,不理我就當沒事。"
            )
            try:
                if line_source_id:
                    from app.services import line_client
                    await line_client.push_text(line_source_id, suggest)
            except Exception:
                logger.exception("LINE 主動建議推送失敗")
            await _log_activity(
                activity_type="proactive_suggest",
                summary=f"主動建議:{title}",
                user_id=sender_id,
                extra={
                    "channel_id": str(channel_id),
                    "title": title,
                    "confidence": confidence,
                    "from_message_id": str(message_id),
                    "assignee_name": result.get("assignee_name"),
                    "due": due_date.isoformat() if due_date else None,
                    "priority": priority,
                },
            )
            return

        # === 高信心(>= PROACTIVE_BUILD_THRESHOLD)→ 真建(LIVE 模式才建)===
        if not EXTRACTOR_LIVE:
            logger.info(
                "[dry-run] 偵測到派工:%s | 信心 %.2f | assignee=%s | due=%s",
                title, confidence, result.get("assignee_name"), result.get("due"),
            )
            return

        # 冷卻打到 → 高信心仍建,但不推 LINE 通知(避免洗版)
        # 靜音時段 → 高信心仍建,但不推 LINE 通知(等天亮再說)
        suppress_notify = cooldown_hit or quiet

        # 真建任務
        async with AsyncSessionLocal() as db:
            task = Task(
                title=title,
                priority=priority,
                due_date=due_date,
                assignee_id=assignee_id,
                created_by=sender_id,
                status="todo",
            )
            db.add(task)
            await db.commit()
            await db.refresh(task)

        await _log_activity(
            activity_type="task_create_auto",
            summary=f"自動建任務:{title}",
            user_id=sender_id,
            extra={
                "task_id": str(task.id),
                "channel_id": str(channel_id),
                "assignee_name": result.get("assignee_name"),
                "confidence": confidence,
                "from_message_id": str(message_id),
                "notify_suppressed": suppress_notify,
                "suppress_reason": (
                    "cooldown" if cooldown_hit else ("quiet_hours" if quiet else None)
                ),
            },
        )

        if suppress_notify or not line_source_id:
            return

        # 在 LINE 群裡通知
        try:
            from app.services import line_client
            assignee_text = (
                f"指派給:{result.get('assignee_name')}"
                if assignee_id
                else "(沒指明對象,先記在公共池)"
            )
            due_text = f" · 截止 {due_date.isoformat()}" if due_date else ""
            priority_text = (
                " · ⚡ 緊急" if priority == "urgent" else (" · 🔥 高優" if priority == "high" else "")
            )
            ack_text = (
                f"📋 我把這個記成任務:\n"
                f"《{title}》\n"
                f"{assignee_text}{due_text}{priority_text}\n\n"
                f"錯了 @ 我說「取消最後一個任務」就好"
            )
            await line_client.push_text(line_source_id, ack_text)
        except Exception:
            logger.exception("LINE 通知派工建立失敗")

    except Exception:
        logger.exception("task_extract 失敗")


# ============================================================
# 取消最後一個自動建的任務
# ============================================================


CANCEL_RE = re.compile(
    r"^(?:@?庫柏|@coba)\s*[,,]?\s*(?:取消|刪掉|不要)(?:最後一個|那個|剛剛(?:那個|建的)?)?\s*任務?\s*$",
    re.IGNORECASE,
)


def is_cancel_command(content: str) -> bool:
    if not content:
        return False
    return bool(CANCEL_RE.match(content.strip()))


async def cancel_last_auto_task(triggered_by_user_id: UUID, line_source_id: str | None) -> str:
    """取消「最後一個自動建的任務」。

    定義:agent_activity 中最近一筆 type='task_create_auto' 對應的 task。
    操作:把 task 軟刪 + 記 'task_cancel' activity。回傳要在 LINE 講的話。
    """
    async with AsyncSessionLocal() as db:
        # 拉最近 1 筆自動建的任務 activity(限定 LINE 群相關)
        last = (await db.execute(
            select(AgentActivity)
            .where(AgentActivity.activity_type == "task_create_auto")
            .order_by(AgentActivity.created_at.desc())
            .limit(1)
        )).scalar_one_or_none()

        if last is None or not last.extra or not last.extra.get("task_id"):
            return "找不到最近的自動任務,沒東西可以取消。"

        task_id = last.extra["task_id"]
        task = (await db.execute(
            select(Task).where(Task.id == task_id, Task.deleted_at.is_(None))
        )).scalar_one_or_none()

        if task is None:
            return "那個任務已經不在了(可能你之前已經刪過)。"

        title = task.title
        task.deleted_at = datetime.now(timezone.utc)
        task.status = "cancelled"
        await db.commit()

    await _log_activity(
        activity_type="task_cancel",
        summary=f"取消自動任務:{title}",
        user_id=triggered_by_user_id,
        extra={"task_id": task_id, "trigger": "user_command"},
    )

    return f"✓ 取消了:《{title}》"
