"""Feishu gateway channel based on lark-oapi long connections."""

from __future__ import annotations

import asyncio
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
import json
import logging
import threading
from typing import Any

from memnixa.channels.base import BaseChannel, InboundChannelMessage
from memnixa.config import FeishuChannelConfig

LOGGER = logging.getLogger(__name__)

try:
    import lark_oapi as lark
except ImportError:  # pragma: no cover
    lark = None


def _extract_post_text(content_json: dict[str, Any]) -> str:
    root = (
        content_json.get("post")
        if isinstance(content_json.get("post"), dict)
        else content_json
    )
    if not isinstance(root, dict):
        return ""

    blocks: list[dict[str, Any]] = []
    if "content" in root:
        blocks.append(root)
    for locale in ("zh_cn", "en_us", "ja_jp"):
        if isinstance(root.get(locale), dict):
            blocks.append(root[locale])

    for block in blocks:
        parts: list[str] = []
        title = block.get("title")
        if title:
            parts.append(str(title))
        for row in block.get("content", []):
            if not isinstance(row, list):
                continue
            for element in row:
                if not isinstance(element, dict):
                    continue
                if element.get("tag") in {"text", "a"} and element.get("text"):
                    parts.append(str(element["text"]))
                if element.get("tag") == "at" and element.get("user_name"):
                    parts.append(f"@{element['user_name']}")
        text = " ".join(part for part in parts if part).strip()
        if text:
            return text
    return ""


class FeishuChannel(BaseChannel):
    """Feishu text channel for the gateway runtime."""

    name = "feishu"
    _REPLYING_REACTION_TYPE = "Typing"

    def __init__(
        self,
        config: FeishuChannelConfig,
        inbound_handler,
    ) -> None:
        super().__init__(config, inbound_handler)
        self.config = config
        self._client: Any = None
        self._ws_client: Any = None
        self._loop: asyncio.AbstractEventLoop | None = None
        self._ws_thread: threading.Thread | None = None
        self._processed_ids: OrderedDict[str, None] = OrderedDict()
        self._active_reactions: dict[str, dict[str, str]] = {}

    @property
    def capabilities(self) -> tuple[str, ...]:
        return ("reply_status", "message_reactions")

    async def start(self) -> None:
        """Connect to Feishu and listen indefinitely."""
        if lark is None:
            raise RuntimeError("Feishu support requires the `lark-oapi` package.")
        if not self.config.app_id or not self.config.app_secret:
            raise ValueError("Feishu channel requires app_id and app_secret.")

        self._running = True
        self._loop = asyncio.get_running_loop()
        self._client = (
            lark.Client.builder()
            .app_id(self.config.app_id)
            .app_secret(self.config.app_secret)
            .log_level(lark.LogLevel.INFO)
            .build()
        )
        event_handler = (
            lark.EventDispatcherHandler.builder(
                self.config.encrypt_key or "",
                self.config.verification_token or "",
            )
            .register_p2_im_message_receive_v1(self._on_message_sync)
            .build()
        )
        self._ws_client = lark.ws.Client(
            self.config.app_id,
            self.config.app_secret,
            event_handler=event_handler,
            log_level=lark.LogLevel.INFO,
        )
        self._ws_thread = threading.Thread(target=self._run_ws_client, daemon=True)
        self._ws_thread.start()

        while self._running:
            await asyncio.sleep(1)

    def _run_ws_client(self) -> None:
        import lark_oapi.ws.client as lark_ws_client

        ws_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(ws_loop)
        lark_ws_client.loop = ws_loop
        try:
            while self._running:
                try:
                    self._ws_client.start()
                except Exception:  # pragma: no cover
                    LOGGER.exception("Feishu websocket client failed")
                if self._running:
                    ws_loop.run_until_complete(asyncio.sleep(5))
        finally:
            ws_loop.close()

    async def stop(self) -> None:
        """Stop the Feishu listener."""
        self._running = False
        if self._ws_thread:
            self._ws_thread.join(timeout=3)
            self._ws_thread = None

    def _on_message_sync(self, data: Any) -> None:
        if self._loop and self._loop.is_running():
            asyncio.run_coroutine_threadsafe(self._on_message(data), self._loop)

    async def _on_message(self, data: Any) -> None:
        event = data.event
        message = event.message
        sender = event.sender

        if getattr(sender, "sender_type", None) == "bot":
            return

        message_id = str(message.message_id)
        if message_id in self._processed_ids:
            return
        self._processed_ids[message_id] = None
        if len(self._processed_ids) > 1000:
            self._processed_ids.popitem(last=False)

        sender_id = (
            getattr(getattr(sender, "sender_id", None), "open_id", None) or "unknown"
        )
        chat_id = str(message.chat_id)
        chat_type = str(message.chat_type)
        msg_type = str(message.message_type)
        if chat_type == "group" and self.config.group_policy == "mention":
            if not self._is_bot_mentioned(message):
                return

        content = self._extract_content(msg_type, message.content)
        if not content:
            return

        reply_target = chat_id if chat_type == "group" else sender_id
        reply_target_type = "chat_id" if chat_type == "group" else "open_id"
        await self.emit(
            InboundChannelMessage(
                channel=self.name,
                chat_id=chat_id,
                sender_id=sender_id,
                body=content,
                raw_body=message.content,
                message_id=message_id,
                reply_target=reply_target,
                reply_target_type=reply_target_type,
                metadata={"chat_type": chat_type, "message_type": msg_type},
            )
        )

    def _is_bot_mentioned(self, message: Any) -> bool:
        raw_content = message.content or ""
        if "@_all" in raw_content:
            return True
        for mention in getattr(message, "mentions", None) or []:
            mention_id = getattr(mention, "id", None)
            if mention_id and getattr(mention_id, "open_id", None):
                return True
        return False

    def _extract_content(self, msg_type: str, raw_content: str | None) -> str:
        try:
            content_json = json.loads(raw_content) if raw_content else {}
        except json.JSONDecodeError:
            return (raw_content or "").strip()

        if msg_type == "text":
            return str(content_json.get("text") or "").strip()
        if msg_type == "post":
            return _extract_post_text(content_json)
        return ""

    async def send_text(
        self,
        reply_target: str,
        content: str,
        *,
        reply_target_type: str,
        metadata: dict[str, Any] | None = None,
    ) -> None:
        payload = json.dumps({"text": content}, ensure_ascii=False)
        await self._run_blocking(
            self._send_text_sync,
            reply_target_type,
            reply_target,
            payload,
        )

    def _send_text_sync(
        self,
        reply_target_type: str,
        reply_target: str,
        payload: str,
    ) -> None:
        if self._client is None:
            raise RuntimeError("Feishu client is not initialized.")

        from lark_oapi.api.im.v1 import CreateMessageRequest, CreateMessageRequestBody

        request = (
            CreateMessageRequest.builder()
            .receive_id_type(reply_target_type)
            .request_body(
                CreateMessageRequestBody.builder()
                .receive_id(reply_target)
                .msg_type("text")
                .content(payload)
                .build()
            )
            .build()
        )
        response = self._client.im.v1.message.create(request)
        if not response.success():
            raise RuntimeError(
                f"Feishu send failed: code={response.code}, msg={response.msg}"
            )

    async def start_replying_indicator(self, message: InboundChannelMessage) -> None:
        """Mark one Feishu message as being processed via reaction."""
        await super().start_replying_indicator(message)
        existing = self._active_reactions.get(message.session_id)
        if existing and existing.get("message_id") == message.message_id:
            return
        if existing:
            await self.stop_replying_indicator(message)
            await super().start_replying_indicator(message)
        if self._client is None:
            return
        reaction_id = await self._run_blocking(
            self._create_reaction_sync,
            message.message_id,
            self._REPLYING_REACTION_TYPE,
        )
        self._active_reactions[message.session_id] = {
            "message_id": message.message_id,
            "reaction_id": reaction_id,
        }

    async def stop_replying_indicator(self, message: InboundChannelMessage) -> None:
        """Clear the Feishu reply reaction for the active session."""
        try:
            active = self._active_reactions.pop(message.session_id, None)
            if active and self._client is not None:
                await self._run_blocking(
                    self._delete_reaction_sync,
                    active["message_id"],
                    active["reaction_id"],
                )
        finally:
            await super().stop_replying_indicator(message)

    async def _run_blocking(self, func, *args):
        loop = asyncio.get_running_loop()
        with ThreadPoolExecutor(
            max_workers=1,
            thread_name_prefix=f"memnixa-{self.name}",
        ) as executor:
            return await loop.run_in_executor(executor, func, *args)

    def _create_reaction_sync(self, message_id: str, emoji_type: str) -> str:
        if self._client is None:
            raise RuntimeError("Feishu client is not initialized.")

        from lark_oapi.api.im.v1 import (
            CreateMessageReactionRequest,
            CreateMessageReactionRequestBody,
            Emoji,
        )

        request = (
            CreateMessageReactionRequest.builder()
            .message_id(message_id)
            .request_body(
                CreateMessageReactionRequestBody.builder()
                .reaction_type(Emoji.builder().emoji_type(emoji_type).build())
                .build()
            )
            .build()
        )
        response = self._client.im.v1.message_reaction.create(request)
        if not response.success():
            raise RuntimeError(
                f"Feishu reaction create failed: code={response.code}, msg={response.msg}"
            )
        reaction_id = getattr(getattr(response, "data", None), "reaction_id", None)
        if not reaction_id:
            raise RuntimeError("Feishu reaction create succeeded without reaction_id.")
        return str(reaction_id)

    def _delete_reaction_sync(self, message_id: str, reaction_id: str) -> None:
        if self._client is None:
            raise RuntimeError("Feishu client is not initialized.")

        from lark_oapi.api.im.v1 import DeleteMessageReactionRequest

        request = (
            DeleteMessageReactionRequest.builder()
            .message_id(message_id)
            .reaction_id(reaction_id)
            .build()
        )
        response = self._client.im.v1.message_reaction.delete(request)
        if not response.success():
            raise RuntimeError(
                f"Feishu reaction delete failed: code={response.code}, msg={response.msg}"
            )
