"""DingTalk/DingDing channel implementation using Stream Mode."""

import asyncio
import importlib
import json
import mimetypes
import os
import time
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import unquote, urlparse

import httpx
from loguru import logger

from bao.bus.events import OutboundMessage
from bao.bus.queue import MessageBus
from bao.channels.base import BaseChannel
from bao.channels.progress_text import ProgressBuffer
from bao.config.schema import DingTalkConfig

_dingtalk_available = False
try:
    importlib.import_module("dingtalk_stream")
    importlib.import_module("dingtalk_stream.chatbot")
    _dingtalk_available = True
except ImportError:
    pass

DINGTALK_AVAILABLE = _dingtalk_available


class DingTalkChannel(BaseChannel):
    """
    DingTalk channel using Stream Mode.

    Uses WebSocket to receive events via `dingtalk-stream` SDK.
    Uses direct HTTP API to send messages (SDK is mainly for receiving).

    Note: Currently only supports private (1:1) chat. Group messages are
    received but replies are sent back as private messages to the sender.
    """

    name = "dingtalk"
    _IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
    _AUDIO_EXTS = {".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac"}
    _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"}

    def __init__(self, config: DingTalkConfig, bus: MessageBus):
        super().__init__(config, bus)
        self.config: DingTalkConfig = config
        self._client: Any = None
        self._http: httpx.AsyncClient | None = None

        # Access Token management for sending messages
        self._access_token: str | None = None
        self._token_expiry: float = 0
        self._progress_token: str | None = None

        # Hold references to background tasks to prevent GC
        self._background_tasks: set[asyncio.Task[Any]] = set()
        self._progress_handler = ProgressBuffer(self._send_text)

    async def start(self) -> None:
        """Start the DingTalk bot with Stream Mode."""
        self.mark_not_ready()
        try:
            if not DINGTALK_AVAILABLE:
                logger.error("❌ 钉钉 SDK 未安装 / sdk missing: pip install dingtalk-stream")
                self.mark_ready()
                return

            from dingtalk_stream import (
                AckMessage,
                CallbackHandler,
                Credential,
                DingTalkStreamClient,
            )
            from dingtalk_stream.chatbot import ChatbotMessage

            client_secret = self.config.client_secret.get_secret_value()
            if not self.config.client_id or not client_secret:
                logger.error("❌ 钉钉配置缺失 / config missing: client_id and client_secret")
                self.mark_ready()
                return

            self._start_lifecycle()
            self._http = httpx.AsyncClient()
            self.mark_ready()

            logger.info(
                "📡 钉钉开始连接 / stream init: client_id={}...",
                self.config.client_id,
            )
            credential = Credential(self.config.client_id, client_secret)
            self._client = DingTalkStreamClient(credential)

            if TYPE_CHECKING:

                class _Handler(object):
                    def __init__(self, channel: "DingTalkChannel"):
                        self.channel = channel

                    async def process(self, message: Any) -> Any:
                        _ = message
                        raise NotImplementedError

            else:

                class _Handler(CallbackHandler):
                    def __init__(self, channel: "DingTalkChannel"):
                        super().__init__()
                        self.channel = channel

                    async def process(self, message: Any) -> Any:
                        try:
                            raw = getattr(message, "data", None) or {}
                            chatbot_msg = ChatbotMessage.from_dict(raw)
                            content = ""
                            if getattr(chatbot_msg, "text", None):
                                content = (chatbot_msg.text.content or "").strip()
                            if not content and isinstance(raw, dict):
                                content = str(raw.get("text", {}).get("content", "")).strip()
                            if not content:
                                return AckMessage.STATUS_OK, "OK"
                            sender_id = str(
                                getattr(chatbot_msg, "sender_staff_id", None)
                                or getattr(chatbot_msg, "sender_id", None)
                                or ""
                            )
                            sender_name = getattr(chatbot_msg, "sender_nick", None) or "Unknown"
                            task = asyncio.create_task(
                                self.channel._on_message(content, sender_id, sender_name)
                            )
                            self.channel._background_tasks.add(task)
                            task.add_done_callback(self.channel._background_tasks.discard)
                            return AckMessage.STATUS_OK, "OK"
                        except Exception as e:
                            logger.error("❌ 钉钉消息处理异常 / process error: {}", e)
                            return AckMessage.STATUS_OK, "Error"

            handler = _Handler(self)
            self._client.register_callback_handler(ChatbotMessage.TOPIC, handler)

            logger.info("✅ 钉钉已连接 / stream connected: stream mode started")

            await self._run_reconnect_loop(self._client.start, label="钉钉 Stream")

        except Exception as e:
            logger.exception("❌ 钉钉启动失败 / start failed: {}", e)
            self.mark_ready()

    async def stop(self) -> None:
        """Stop the DingTalk bot."""
        self._clear_progress()
        self._stop_lifecycle()
        self.mark_not_ready()
        # Close the shared HTTP client
        if self._http:
            await self._http.aclose()
            self._http = None
        # Cancel outstanding background tasks
        for task in self._background_tasks:
            task.cancel()
        self._background_tasks.clear()
        self._reset_lifecycle()

    async def _get_access_token(self) -> str | None:
        """Get or refresh Access Token."""
        if self._access_token and time.time() < self._token_expiry:
            return self._access_token

        url = "https://api.dingtalk.com/v1.0/oauth2/accessToken"
        data = {
            "appKey": self.config.client_id,
            "appSecret": self.config.client_secret.get_secret_value(),
        }

        if not self._http:
            logger.warning("⚠️ 钉钉客户端未就绪 / client not ready: cannot refresh token")
            return None

        try:
            resp = await self._http.post(url, json=data)
            resp.raise_for_status()
            res_data = resp.json()
            self._access_token = res_data.get("accessToken")
            # Expire 60s early to be safe
            self._token_expiry = time.time() + int(res_data.get("expireIn", 7200)) - 60
            return self._access_token
        except Exception as e:
            logger.error("❌ 钉钉令牌获取失败 / token failed: {}", e)
            return None

    @staticmethod
    def _is_http_url(value: str) -> bool:
        return urlparse(value).scheme in ("http", "https")

    def _guess_upload_type(self, media_ref: str) -> str:
        ext = Path(urlparse(media_ref).path).suffix.lower()
        if ext in self._IMAGE_EXTS:
            return "image"
        if ext in self._AUDIO_EXTS:
            return "voice"
        if ext in self._VIDEO_EXTS:
            return "video"
        return "file"

    def _guess_filename(self, media_ref: str, upload_type: str) -> str:
        name = os.path.basename(urlparse(media_ref).path)
        return name or {
            "image": "image.jpg",
            "voice": "audio.amr",
            "video": "video.mp4",
        }.get(upload_type, "file.bin")

    @staticmethod
    def _resolve_local_media_path(media_ref: str, os_name: str | None = None) -> Path:
        if not media_ref.startswith("file://"):
            return Path(os.path.expanduser(media_ref))

        parsed = urlparse(media_ref)
        host = parsed.netloc
        path_part = unquote(parsed.path or "")
        target_os = os_name or os.name

        if target_os == "nt":
            if host and host.lower() != "localhost":
                path_text = f"//{host}{path_part}"
            else:
                path_text = path_part
            if len(path_text) >= 3 and path_text[0] == "/" and path_text[2] == ":":
                path_text = path_text[1:]
            return Path(path_text)

        if host and host.lower() != "localhost":
            return Path(f"//{host}{path_part}")
        return Path(path_part)

    async def _read_media_bytes(
        self, media_ref: str
    ) -> tuple[bytes | None, str | None, str | None]:
        if not media_ref:
            return None, None, None

        if self._is_http_url(media_ref):
            if not self._http:
                return None, None, None
            try:
                resp = await self._http.get(media_ref, follow_redirects=True)
                if resp.status_code >= 400:
                    logger.warning(
                        "DingTalk media download failed status={} ref={}",
                        resp.status_code,
                        media_ref,
                    )
                    return None, None, None
                content_type = (resp.headers.get("content-type") or "").split(";")[0].strip()
                filename = self._guess_filename(media_ref, self._guess_upload_type(media_ref))
                return resp.content, filename, content_type or None
            except Exception as e:
                logger.error("DingTalk media download error ref={} err={}", media_ref, e)
                return None, None, None

        try:
            local_path = self._resolve_local_media_path(media_ref)
            if not local_path.is_file():
                logger.warning("DingTalk media file not found: {}", local_path)
                return None, None, None
            data = await asyncio.to_thread(local_path.read_bytes)
            content_type = mimetypes.guess_type(local_path.name)[0]
            return data, local_path.name, content_type
        except Exception as e:
            logger.error("DingTalk media read error ref={} err={}", media_ref, e)
            return None, None, None

    async def _upload_media(
        self,
        token: str,
        data: bytes,
        media_type: str,
        filename: str,
        content_type: str | None,
    ) -> str | None:
        if not self._http:
            return None
        url = f"https://oapi.dingtalk.com/media/upload?access_token={token}&type={media_type}"
        mime = content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
        files = {"media": (filename, data, mime)}

        try:
            resp = await self._http.post(url, files=files)
            text = resp.text
            result = (
                resp.json()
                if resp.headers.get("content-type", "").startswith("application/json")
                else {}
            )
            if resp.status_code >= 400:
                logger.error(
                    "DingTalk media upload failed status={} type={} body={}",
                    resp.status_code,
                    media_type,
                    text[:500],
                )
                return None
            errcode = result.get("errcode", 0)
            if errcode != 0:
                logger.error(
                    "DingTalk media upload api error type={} errcode={} body={}",
                    media_type,
                    errcode,
                    text[:500],
                )
                return None
            sub = result.get("result") or {}
            media_id = (
                result.get("media_id")
                or result.get("mediaId")
                or sub.get("media_id")
                or sub.get("mediaId")
            )
            if not media_id:
                logger.error("DingTalk media upload missing media_id body={}", text[:500])
                return None
            return str(media_id)
        except Exception as e:
            logger.error("DingTalk media upload error type={} err={}", media_type, e)
            return None

    async def _send_batch_message(
        self,
        token: str,
        chat_id: str,
        msg_key: str,
        msg_param: dict[str, Any],
    ) -> bool:
        if not self._http:
            logger.warning("DingTalk HTTP client not initialized, cannot send")
            return False

        url = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend"
        headers = {"x-acs-dingtalk-access-token": token}
        payload = {
            "robotCode": self.config.client_id,
            "userIds": [chat_id],
            "msgKey": msg_key,
            "msgParam": json.dumps(msg_param, ensure_ascii=False),
        }

        try:
            resp = await self._http.post(url, json=payload, headers=headers)
            body = resp.text
            if resp.status_code != 200:
                logger.error(
                    "DingTalk send failed msgKey={} status={} body={}",
                    msg_key,
                    resp.status_code,
                    body[:500],
                )
                return False
            try:
                result = resp.json()
            except Exception:
                result = {}
            errcode = result.get("errcode")
            if errcode not in (None, 0):
                logger.error(
                    "DingTalk send api error msgKey={} errcode={} body={}",
                    msg_key,
                    errcode,
                    body[:500],
                )
                return False
            logger.debug("DingTalk message sent to {} with msgKey={}", chat_id, msg_key)
            return True
        except Exception as e:
            logger.error("Error sending DingTalk message msgKey={} err={}", msg_key, e)
            return False

    async def _send_markdown_text(self, token: str, chat_id: str, content: str) -> bool:
        return await self._send_batch_message(
            token,
            chat_id,
            "sampleMarkdown",
            {"text": content, "title": "Bao Reply"},
        )

    async def _send_media_ref(self, token: str, chat_id: str, media_ref: str) -> bool:
        media_ref = (media_ref or "").strip()
        if not media_ref:
            return True

        upload_type = self._guess_upload_type(media_ref)
        if upload_type == "image" and self._is_http_url(media_ref):
            ok = await self._send_batch_message(
                token,
                chat_id,
                "sampleImageMsg",
                {"photoURL": media_ref},
            )
            if ok:
                return True
            logger.warning("DingTalk image url send failed, trying upload fallback: {}", media_ref)

        data, filename, content_type = await self._read_media_bytes(media_ref)
        if not data:
            logger.error("DingTalk media read failed: {}", media_ref)
            return False

        filename = filename or self._guess_filename(media_ref, upload_type)
        file_type = Path(filename).suffix.lower().lstrip(".")
        if not file_type:
            guessed = mimetypes.guess_extension(content_type or "")
            file_type = (guessed or ".bin").lstrip(".")
        if file_type == "jpeg":
            file_type = "jpg"

        media_id = await self._upload_media(
            token=token,
            data=data,
            media_type=upload_type,
            filename=filename,
            content_type=content_type,
        )
        if not media_id:
            return False

        return await self._send_batch_message(
            token,
            chat_id,
            "sampleFile",
            {"mediaId": media_id, "fileName": filename, "fileType": file_type},
        )

    async def send(self, msg: OutboundMessage) -> None:
        """Send a message through DingTalk."""
        token = await self._get_access_token()
        if not token:
            return

        self._progress_token = token
        await self._dispatch_progress_text(msg, flush_progress=False)

        for media_ref in msg.media or []:
            ok = await self._send_media_ref(token, msg.chat_id, media_ref)
            if ok:
                continue
            logger.error("DingTalk media send failed for {}", media_ref)
            filename = self._guess_filename(media_ref, self._guess_upload_type(media_ref))
            await self._send_markdown_text(
                token,
                msg.chat_id,
                f"[Attachment send failed: {filename}]",
            )

    async def _send_text(self, chat_id: str, text: str) -> None:
        token = getattr(self, "_progress_token", None)
        if not token or not text.strip():
            return
        await self._send_markdown_text(token, chat_id, text.strip())

    async def _on_message(self, content: str, sender_id: str, sender_name: str) -> None:
        """Handle incoming message (called by baoDingTalkHandler).

        Delegates to BaseChannel._handle_message() which enforces allow_from
        permission checks before publishing to the bus.
        """
        try:
            logger.debug("ℹ️ 钉钉入站消息 / inbound: {} from {}", content, sender_name)
            await self._handle_message(
                sender_id=sender_id,
                chat_id=sender_id,  # For private chat, chat_id == sender_id
                content=str(content),
                metadata={
                    "sender_name": sender_name,
                    "platform": "dingtalk",
                },
            )
        except Exception as e:
            logger.error("❌ 钉钉消息转发异常 / publish error: {}", e)
