"""
LINE 事件 ↔ Cooper 內部頻道/訊息系統的橋接層(階段 6)。

職責:
  1. 每個 LINE source(group / room / 1對1)對應一個 Cooper Channel
     - 第一次看到時自動建一個 channel_type='line_<type>' 的 Channel
  2. 每個 LINE userId 對應一個 Cooper User
     - 沒綁定的會建立「影子帳號」(password_hash=""),日後可由 owner 合併到實體帳號
  3. message 事件 → 存成 Channel 的 Message → 觸發 tagging / index / Cooper 回應
  4. follow / join / leave 事件 → 維護 Channel 的存在 / 軟刪
  5. Cooper 回應一律用 push_text(LLM 跑完通常超過 reply token 的 1 分鐘 TTL)

設計原則:整段都是 best-effort,單一事件失敗只 log,不要把整批 events 拖垮。
"""

from __future__ import annotations

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

import anthropic
from sqlalchemy import select

from app.config import settings
from app.database import AsyncSessionLocal
from app.models.channel import Channel, Message
from app.models.user import User
from app.services import line_client


logger = logging.getLogger("coba.line.bridge")


# ============================================================
# Channel / User 對應(以 LINE id 為唯一鍵)
# ============================================================


def _resolve_source(src: dict[str, Any]) -> tuple[str | None, str | None]:
    """source dict → (source_type, source_id)。"""
    stype = src.get("type")
    if stype == "group":
        return "group", src.get("groupId")
    if stype == "room":
        return "room", src.get("roomId")
    if stype == "user":
        return "user", src.get("userId")
    return None, None


async def _get_or_create_channel(db, src: dict[str, Any]) -> Channel | None:
    stype, sid = _resolve_source(src)
    if not stype or not sid:
        return None

    ch = (await db.execute(
        select(Channel).where(
            Channel.line_source_id == sid,
            Channel.deleted_at.is_(None),
        )
    )).scalar_one_or_none()
    if ch is not None:
        return ch

    name = {
        "group": "LINE 群組",
        "room": "LINE 多人聊天室",
        "user": "LINE 私訊",
    }[stype]
    ch = Channel(
        name=name,
        icon="📱",
        description=f"從 LINE {stype} 自動建立",
        channel_type=f"line_{stype}",
        line_source_type=stype,
        line_source_id=sid,
        is_default=False,
        sort_order=50,
    )
    db.add(ch)
    await db.flush()
    return ch


async def _get_or_create_user(db, line_user_id: str | None, src: dict[str, Any]) -> User | None:
    if not line_user_id:
        return None
    u = (await db.execute(
        select(User).where(User.line_user_id == line_user_id, User.deleted_at.is_(None))
    )).scalar_one_or_none()
    if u is not None:
        return u

    # 抓 LINE profile 拿名字 / 頭像
    group_id = src.get("groupId") if src.get("type") == "group" else None
    profile = await line_client.get_profile(line_user_id, group_id=group_id)
    display_name = (profile or {}).get("displayName") or f"LINE-{line_user_id[:6]}"
    avatar = (profile or {}).get("pictureUrl")

    # 影子帳號:password_hash="" → 永遠驗不過 → 不能登入網頁
    u = User(
        email=f"line_{line_user_id}@cooper.local",
        password_hash="",
        display_name=display_name,
        avatar_url=avatar,
        role="member",
        line_user_id=line_user_id,
        is_active=True,
    )
    db.add(u)
    try:
        await db.flush()
    except Exception:
        # email 衝突或其他例外 → 重抓一次(可能是同時間另一個 webhook 已建)
        await db.rollback()
        u = (await db.execute(
            select(User).where(User.line_user_id == line_user_id)
        )).scalar_one_or_none()
    return u


# ============================================================
# 事件分派
# ============================================================


async def handle_event(event: dict[str, Any]) -> None:
    """LINE webhook 拋進來的單一事件。"""
    etype = event.get("type")
    src = event.get("source") or {}
    try:
        if etype == "message":
            await _handle_message(event, src)
        elif etype == "follow":
            await _handle_follow(event, src)
        elif etype == "unfollow":
            await _handle_unfollow(event, src)
        elif etype == "join":
            await _handle_join(event, src)
        elif etype == "leave":
            await _handle_leave(event, src)
        # postback / memberJoined / memberLeft 等暫時不處理
    except Exception:
        logger.exception("LINE 事件處理失敗 type=%s", etype)


# ----- message -----

_TEXT_PREFIX_MAP = {
    "image": "[圖片]",
    "audio": "[語音]",
    "video": "[影片]",
    "file": "[檔案]",
    "sticker": "[貼圖]",
    "location": "[位置]",
}


async def _handle_message(event: dict[str, Any], src: dict[str, Any]) -> None:
    msg_obj = event.get("message") or {}
    mtype = msg_obj.get("type", "text")
    line_message_id = msg_obj.get("id")
    line_user_id = src.get("userId")
    reply_token = event.get("replyToken")

    async with AsyncSessionLocal() as db:
        ch = await _get_or_create_channel(db, src)
        if ch is None:
            return
        sender = await _get_or_create_user(db, line_user_id, src)

        if mtype == "text":
            content = msg_obj.get("text", "")
            message_type = "text"
        elif mtype in _TEXT_PREFIX_MAP:
            prefix = _TEXT_PREFIX_MAP[mtype]
            extra = ""
            if mtype == "file":
                extra = f" {msg_obj.get('fileName', '')}"
            content = f"{prefix}{extra}".strip()
            message_type = mtype if mtype in ("image", "audio", "file") else "text"
        else:
            content = f"[未支援訊息:{mtype}]"
            message_type = "text"

        msg = Message(
            channel_id=ch.id,
            user_id=sender.id if sender else None,
            content=content,
            message_type=message_type,
            line_message_id=line_message_id,
        )
        db.add(msg)
        await db.commit()
        msg_id = msg.id
        ch_id = ch.id
        ch_line_source_id = ch.line_source_id

    # ---- 背景副作用(同 messages.py 路徑)----
    try:
        from app.services.coba_tagger import trigger_tagging
        from app.services.coba_memory import trigger_index
        trigger_tagging(msg_id)
        trigger_index(
            message_id=msg_id,
            channel_id=ch_id,
            user_id=sender.id if sender else None,
            content=content,
            created_at_iso=datetime.now(timezone.utc).isoformat(),
            tags=None,
        )
    except Exception:
        logger.exception("LINE message 背景索引失敗")

    # ---- 階段 9-B:任務認領偵測(同 channel sender 是 assignee + 短確認語)----
    if message_type == "text" and sender and sender.id:
        try:
            from app.services.task_acceptance import trigger_acceptance
            trigger_acceptance(msg_id)
        except Exception:
            logger.exception("trigger_acceptance 失敗")

    # ---- 階段 9-C:Cooper ack 模式(分類訊息 + 對重要訊息短回應 + 抓體悟)----
    # 由 ACK_ENABLED 環境變數開關,預設關
    if message_type == "text":
        try:
            from app.services.coba_acknowledge import trigger_ack
            trigger_ack(msg_id)
        except Exception:
            logger.exception("trigger_ack 失敗")

    # ---- 媒體下載(背景)+ audio/video 自動轉 meeting pipeline ----
    if mtype in ("image", "audio", "video", "file") and line_message_id:
        asyncio.create_task(_download_media(
            line_message_id, mtype,
            channel_id=ch_id,
            sender_id=sender.id if sender else None,
        ))

    # ---- 綁定指令攔截(優先於 Cooper 對話)----
    if message_type == "text" and line_user_id:
        from app.services.line_binding import parse_bind_command
        bind_code = parse_bind_command(content)
        if bind_code:
            asyncio.create_task(_handle_bind_command(
                bind_code, line_user_id, ch_line_source_id, reply_token
            ))
            return  # 綁定指令不再走 Cooper 對話路徑

    # ---- 「取消最後一個任務」指令攔截 ----
    if message_type == "text":
        from app.services.task_extractor import is_cancel_command, cancel_last_auto_task
        if is_cancel_command(content):
            asyncio.create_task(_handle_cancel_command(
                sender.id if sender else None, ch_line_source_id, reply_token
            ))
            return

    # ---- Cooper 觸發判定(順序:先決定 Cooper 是否要回,再決定要不要跑 task_extractor)----
    if message_type != "text":
        return
    is_dm = src.get("type") == "user"
    from app.services.coba_chat import detect_coba_mention
    if is_dm:
        should_respond = True
    else:
        # 群組:DB-backed engagement window 需要 db session
        async with AsyncSessionLocal() as _detect_db:
            should_respond = await detect_coba_mention(
                content,
                channel_id=ch_id,
                user_id=sender.id if sender else None,
                db=_detect_db,
            )

    # ---- 自動派工偵測 ----
    # 重要:Cooper 直接回應時(should_respond=True),Cooper 自己會用 tool 建任務
    # → 這時跳過 task_extractor 避免雙重建立。
    # 只在「沒人 @ Cooper、純對話間派工」這種情況才讓 extractor 自動偵測 + 建。
    if not should_respond:
        from app.services.task_extractor import trigger_task_extract
        asyncio.create_task(trigger_task_extract(msg_id))
    if not should_respond:
        return
    asyncio.create_task(_cooper_respond_to_line(ch_id, msg_id, ch_line_source_id, reply_token))


async def _handle_cancel_command(
    user_id: UUID | None,
    line_source_id: str | None,
    reply_token: str | None,
) -> None:
    """處理「@庫柏 取消最後一個任務」指令。"""
    from app.services.task_extractor import cancel_last_auto_task
    if user_id is None:
        return
    text = await cancel_last_auto_task(user_id, line_source_id)
    if line_source_id:
        sent = False
        if reply_token:
            sent = await line_client.reply_text(reply_token, text)
        if not sent:
            await line_client.push_text(line_source_id, text)


async def _handle_bind_command(
    code: str,
    line_user_id: str,
    line_source_id: str | None,
    reply_token: str | None,
) -> None:
    """跑 LINE 綁定 + 回 LINE。"""
    from app.services.line_binding import BindingError, redeem_code
    from sqlalchemy.exc import IntegrityError
    async with AsyncSessionLocal() as db:
        try:
            web_user = await redeem_code(db, code, line_user_id)
            await db.commit()
            text = (
                f"✓ 綁定成功!\n"
                f"你的 LINE 現在連到「山水話菁英團隊」行程規畫系統的帳號「{web_user.display_name}({web_user.role})」。\n"
                f"之後我用 LINE 找你都會用這個身份。"
            )
        except BindingError as e:
            text = f"⚠ {e}"
        except IntegrityError:
            await db.rollback()
            text = "⚠ 綁定遇到資料衝突,請稍後再試或聯絡管理員"
        except Exception as e:
            await db.rollback()
            logger.exception("綁定 redeem 失敗")
            text = f"⚠ 綁定失敗:{type(e).__name__}"

    if line_source_id:
        sent = False
        if reply_token:
            sent = await line_client.reply_text(reply_token, text)
        if not sent:
            await line_client.push_text(line_source_id, text)


# ----- follow / unfollow / join / leave -----


async def _handle_follow(event: dict[str, Any], src: dict[str, Any]) -> None:
    """使用者把 Cooper 加為好友 → 發歡迎詞。"""
    line_user_id = src.get("userId")
    reply_token = event.get("replyToken")
    if not line_user_id:
        return
    welcome = (
        "嗨,我是庫柏(Cooper)——「山水話菁英團隊」的 AI 同事。\n\n"
        "我駐紮在團隊的「行程規畫系統」裡。\n"
        "把我拉進團隊的 LINE 群組,我會默默讀訊息、在 @ 我的時候回應,\n"
        "也會處理會議錄音、每天 9 點推送個人簡報。\n\n"
        "現在你可以:\n"
        "• 直接傳訊息給我(我會用 Sonnet 回)\n"
        "• 邀我進群組\n"
        "• 到網頁版產生綁定碼,把 LINE 帳號連到行程規畫系統"
    )
    if reply_token:
        await line_client.reply_text(reply_token, welcome)
    else:
        await line_client.push_text(line_user_id, welcome)


async def _handle_unfollow(event: dict[str, Any], src: dict[str, Any]) -> None:
    """使用者封鎖 / 移除 Cooper。先不做事,只 log。"""
    logger.info("LINE unfollow: userId=%s", src.get("userId"))


async def _handle_join(event: dict[str, Any], src: dict[str, Any]) -> None:
    """Cooper 被拉進群 / 多人聊天室 → 預先建 Channel + 自我介紹。"""
    reply_token = event.get("replyToken")
    async with AsyncSessionLocal() as db:
        ch = await _get_or_create_channel(db, src)
        await db.commit()
        ch_line_source_id = ch.line_source_id if ch else None

    intro = (
        "嗨大家好,我是庫柏(Cooper),「山水話菁英團隊」的 AI 同事。\n"
        "在這個群裡 @ 我或開頭叫「庫柏」我就會回應。\n"
        "我會默默讀訊息建立記憶,需要時再叫我。"
    )
    if reply_token:
        await line_client.reply_text(reply_token, intro)
    elif ch_line_source_id:
        await line_client.push_text(ch_line_source_id, intro)


async def _handle_leave(event: dict[str, Any], src: dict[str, Any]) -> None:
    """Cooper 被踢出群 → 軟刪 Channel(訊息保留供日後 audit)。"""
    _, sid = _resolve_source(src)
    if not sid:
        return
    async with AsyncSessionLocal() as db:
        ch = (await db.execute(
            select(Channel).where(
                Channel.line_source_id == sid,
                Channel.deleted_at.is_(None),
            )
        )).scalar_one_or_none()
        if ch is None:
            return
        ch.deleted_at = datetime.now(timezone.utc)
        await db.commit()
    logger.info("LINE leave: source=%s 已軟刪 channel", sid)


# ============================================================
# Cooper 回應 → push 回 LINE
# ============================================================


async def _cooper_respond_to_line(
    channel_id: UUID,
    triggered_msg_id: UUID,
    line_source_id: str | None,
    reply_token: str | None,
) -> None:
    """LINE 路徑的 Cooper 回應 → 全程走 coba_chat.coba_respond_to_mention(含 tool use 工具)
    跑完後把產出的 Cooper 訊息推回 LINE 群。
    """
    # 階段 10:Cooper 主對話走 SDK + OAuth(Max 訂閱)。任何一個憑證在就放行。
    if not (settings.ANTHROPIC_API_KEY or settings.CLAUDE_CODE_OAUTH_TOKEN):
        logger.warning("ANTHROPIC_API_KEY 跟 CLAUDE_CODE_OAUTH_TOKEN 都沒設,LINE Cooper 回應跳過")
        return
    if not line_source_id:
        return

    # 1. 跑 Cooper(包含 tool use loop + 自動建任務 / 改狀態 等)
    from app.services.coba_chat import coba_respond_to_mention
    try:
        await coba_respond_to_mention(channel_id, triggered_msg_id)
    except Exception:
        logger.exception("Cooper 回應(LINE)失敗")
        # 即使失敗也繼續 — coba_respond_to_mention 內部已 save error message

    # 2. 抓最新 Cooper 訊息(coba_respond_to_mention 已存到 DB)
    async with AsyncSessionLocal() as db:
        last = (await db.execute(
            select(Message)
            .where(Message.user_id.is_(None), Message.channel_id == channel_id, Message.deleted_at.is_(None))
            .order_by(Message.created_at.desc())
            .limit(1)
        )).scalars().first()
        text = last.content if last else "(庫柏沒回應)"

    # 3. 推回 LINE — reply 失敗 fallback push
    sent = False
    if reply_token:
        sent = await line_client.reply_text(reply_token, text)
    if not sent:
        await line_client.push_text(line_source_id, text)


# ============================================================
# 媒體下載
# ============================================================


async def _download_media(line_message_id: str, mtype: str, channel_id: UUID | None = None, sender_id: UUID | None = None) -> None:
    """下載 LINE 上傳的 image / audio / video / file 到本機 LINE_MEDIA_DIR。

    階段 8:audio/video → 自動建 Meeting + 觸發轉錄/摘要/action_item → Task pipeline。
    """
    import os
    from pathlib import Path

    data = await line_client.get_message_content(line_message_id)
    if not data:
        return
    media_dir = Path(settings.LINE_MEDIA_DIR)
    media_dir.mkdir(parents=True, exist_ok=True)
    ext = {"image": ".jpg", "audio": ".m4a", "video": ".mp4", "file": ".bin"}.get(mtype, ".bin")
    out = media_dir / f"{line_message_id}{ext}"
    try:
        out.write_bytes(data)
        logger.info("LINE 媒體下載 %s → %s (%dB)", line_message_id, out, len(data))
    except Exception:
        logger.exception("LINE 媒體寫檔失敗 %s", out)
        return

    # 階段 8:audio / video 自動進會議處理 pipeline
    if mtype in ("audio", "video") and channel_id and sender_id:
        try:
            from datetime import datetime
            from app.models.meeting import Meeting
            from app.services.meeting_processor import trigger_meeting_processing
            async with AsyncSessionLocal() as db:
                title = f"LINE 錄音 {datetime.now().strftime('%m/%d %H:%M')}"
                m = Meeting(
                    title=title,
                    channel_id=channel_id,
                    audio_path=str(out),
                    audio_filename=out.name,
                    uploaded_by=sender_id,
                    status="pending",
                )
                db.add(m)
                await db.commit()
                await db.refresh(m)
                meeting_id = m.id
                ch_line_id = (await db.execute(
                    select(Channel).where(Channel.id == channel_id)
                )).scalar_one_or_none()
                ch_line_id = ch_line_id.line_source_id if ch_line_id else None

            # 跑處理(背景)
            trigger_meeting_processing(meeting_id)
            # 立刻通知群組
            if ch_line_id:
                await line_client.push_text(
                    ch_line_id,
                    f"📥 收到錄音了,庫柏在處理中...\n約 1-3 分鐘後我會把摘要 + 任務丟回來。"
                )
        except Exception:
            logger.exception("LINE audio → meeting pipeline 失敗")
