"""
LINE Messaging API 客戶端(階段 6)。

這個模組是「跟 LINE 講話」的所有出口:
  1. verify_signature  ─ 驗 webhook 是不是真的來自 LINE
  2. reply_text         ─ 用 reply token 回應(免費,但 reply token 1 分鐘會過期)
  3. push_text          ─ 主動推送(用得到 to 是 userId/groupId/roomId,有 freemium 額度)
  4. get_profile        ─ 抓使用者顯示名稱與頭像
  5. get_message_content ─ 下載 image/audio/file 的二進位

設計原則:
  - 沒設 LINE_CHANNEL_ACCESS_TOKEN 就 silent skip(讓本機開發環境跑得起來)
  - 失敗只 log 不 raise(避免單一 LINE 訊息失敗就把整個 webhook handler 拖垮)
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import logging
from typing import Any

import httpx

from app.config import settings


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


LINE_API_BASE = "https://api.line.me/v2/bot"
LINE_DATA_API_BASE = "https://api-data.line.me/v2/bot"
# LINE 訊息單則 5000 char 上限,留一點 buffer
_TEXT_MAX = 4900


def _bearer_headers() -> dict[str, str]:
    return {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {settings.LINE_CHANNEL_ACCESS_TOKEN}",
    }


def verify_signature(body: bytes, x_line_signature: str | None) -> bool:
    """LINE webhook 簽章驗證(HMAC-SHA256 + Base64)。

    body  : 原始 request body bytes(不能用 json 字串重新序列化,要 raw bytes)
    x_line_signature: HTTP header 'X-Line-Signature' 的值

    沒設 LINE_CHANNEL_SECRET 一律不通過(避免測試環境誤開後門)。
    """
    if not settings.LINE_CHANNEL_SECRET or not x_line_signature:
        return False
    mac = hmac.new(
        settings.LINE_CHANNEL_SECRET.encode("utf-8"),
        body,
        hashlib.sha256,
    ).digest()
    expected = base64.b64encode(mac).decode("ascii")
    return hmac.compare_digest(expected, x_line_signature.strip())


def _truncate(text: str) -> str:
    if len(text) <= _TEXT_MAX:
        return text
    return text[: _TEXT_MAX - 1] + "…"


async def reply_text(reply_token: str, text: str) -> bool:
    """用 reply token 回應。回 True 代表送出成功。

    reply token 1 分鐘內有效,所以這個只適合「立即回」的場景。
    Cooper 想 LLM 一輪才回的話用 push_text。
    """
    import os
    if os.environ.get("LINE_NO_PUSH") == "1":
        logger.info("[NO_PUSH] reply skipped: %s", text[:80])
        return True
    if not settings.LINE_CHANNEL_ACCESS_TOKEN:
        logger.warning("LINE_CHANNEL_ACCESS_TOKEN 沒設,跳過 reply_text")
        return False
    payload = {
        "replyToken": reply_token,
        "messages": [{"type": "text", "text": _truncate(text)}],
    }
    try:
        async with httpx.AsyncClient(timeout=10.0) as c:
            r = await c.post(f"{LINE_API_BASE}/message/reply", json=payload, headers=_bearer_headers())
        if r.status_code >= 300:
            logger.error("LINE reply 失敗 %s: %s", r.status_code, r.text)
            return False
        return True
    except Exception:
        logger.exception("LINE reply 例外")
        return False


async def push_text(to: str, text: str) -> bool:
    """主動推送(to = userId / groupId / roomId)。"""
    import os
    if os.environ.get("LINE_NO_PUSH") == "1":
        logger.info("[NO_PUSH] push skipped to=%s: %s", to[:12], text[:80])
        return True
    if not settings.LINE_CHANNEL_ACCESS_TOKEN:
        logger.warning("LINE_CHANNEL_ACCESS_TOKEN 沒設,跳過 push_text")
        return False
    payload = {
        "to": to,
        "messages": [{"type": "text", "text": _truncate(text)}],
    }
    try:
        async with httpx.AsyncClient(timeout=10.0) as c:
            r = await c.post(f"{LINE_API_BASE}/message/push", json=payload, headers=_bearer_headers())
        if r.status_code >= 300:
            logger.error("LINE push 失敗 %s: %s", r.status_code, r.text)
            return False
        return True
    except Exception:
        logger.exception("LINE push 例外")
        return False


async def get_profile(user_id: str, group_id: str | None = None) -> dict[str, Any] | None:
    """抓 LINE 使用者顯示名 / 頭像。

    在群組裡要用 /group/{groupId}/member/{userId},不然會失敗。
    """
    if not settings.LINE_CHANNEL_ACCESS_TOKEN:
        return None
    if group_id:
        url = f"{LINE_API_BASE}/group/{group_id}/member/{user_id}"
    else:
        url = f"{LINE_API_BASE}/profile/{user_id}"
    try:
        async with httpx.AsyncClient(timeout=10.0) as c:
            r = await c.get(url, headers=_bearer_headers())
        if r.status_code == 200:
            return r.json()
        logger.warning("LINE profile 抓不到 %s: %s", user_id, r.status_code)
        return None
    except Exception:
        logger.exception("LINE profile 例外")
        return None


async def get_message_content(message_id: str) -> bytes | None:
    """下載 image / audio / video / file 訊息的二進位內容。

    注意 host 是 api-data.line.me 不是 api.line.me。
    """
    if not settings.LINE_CHANNEL_ACCESS_TOKEN:
        return None
    url = f"{LINE_DATA_API_BASE}/message/{message_id}/content"
    try:
        async with httpx.AsyncClient(timeout=30.0) as c:
            r = await c.get(url, headers={"Authorization": f"Bearer {settings.LINE_CHANNEL_ACCESS_TOKEN}"})
        if r.status_code == 200:
            return r.content
        logger.error("LINE content 下載失敗 %s: %s", r.status_code, r.text)
        return None
    except Exception:
        logger.exception("LINE content 下載例外")
        return None
