"""Channel manager that bridges chat platforms into the agent runtime."""

from __future__ import annotations

import asyncio
import logging
import threading
from typing import Any

from memnixa.agent import AgentRuntime
from memnixa.channels.base import BaseChannel, InboundChannelMessage
from memnixa.channels.typing import create_reply_indicator_callbacks
from memnixa.config import ChannelsConfig
from memnixa.gateway.messages import InboundMessageContext
from memnixa.gateway.run_queue import InMemoryRunQueue
from memnixa.identity import (
    build_bound_identity,
    build_unknown_identity,
    is_direct_channel_message,
    resolve_session_id_for_channel,
)

LOGGER = logging.getLogger(__name__)


class ChannelManager:
    """Run enabled channels and route replies through the agent runtime."""

    def __init__(
        self,
        runtime: AgentRuntime,
        channels_config: ChannelsConfig,
        *,
        channel_factories: dict[str, type[BaseChannel]] | None = None,
        run_queue: InMemoryRunQueue | None = None,
    ) -> None:
        self.runtime = runtime
        self.channels_config = channels_config
        self.channel_factories = channel_factories or self._default_channel_factories()
        self.run_queue = run_queue or InMemoryRunQueue()
        self.channels: dict[str, BaseChannel] = {}
        self._loop: asyncio.AbstractEventLoop | None = None
        self._thread: threading.Thread | None = None
        self._started = threading.Event()
        self._channel_tasks: list[asyncio.Task[Any]] = []
        self._build_channels()

    def _default_channel_factories(self) -> dict[str, type[BaseChannel]]:
        from memnixa.channels.feishu import FeishuChannel
        from memnixa.channels.qq import QQChannel

        return {
            "feishu": FeishuChannel,
            "qq": QQChannel,
        }

    def _build_channels(self) -> None:
        specs = {
            "feishu": self.channels_config.feishu,
            "qq": self.channels_config.qq,
        }
        for name, config in specs.items():
            if not getattr(config, "enabled", False):
                continue
            factory = self.channel_factories.get(name)
            if factory is None:
                LOGGER.warning("Skipping unknown channel factory: %s", name)
                continue
            self.channels[name] = factory(config, self.handle_inbound)

    async def handle_inbound(self, message: InboundChannelMessage) -> None:
        """Run one inbound channel message through the agent and reply."""
        channel = self.channels.get(message.channel)
        if channel is None:
            LOGGER.warning("Received message for unknown channel: %s", message.channel)
            return

        actor = self._resolve_actor_identity(message)
        message.resolved_session_id = resolve_session_id_for_channel(
            actor=actor,
            channel=message.channel,
            chat_id=message.chat_id,
            is_direct=is_direct_channel_message(message),
        )

        inbound = InboundMessageContext(
            session_id=message.session_id,
            raw_body=message.raw_body or message.body,
            body_for_agent=message.body.strip(),
            run_id=message.run_id,
            message_id=message.message_id,
            received_at=message.received_at,
            source_mode="channel",
            channel=message.channel,
            capabilities=channel.capabilities,
            actor_identity_status=actor.identity_status,
            actor_user_id=actor.canonical_user_id,
            actor_role=actor.role,
            actor_external_id=actor.external_id,
            actor_is_owner=actor.is_owner,
        )
        self._update_runtime_metadata(inbound)
        reply_indicator = create_reply_indicator_callbacks(
            start=lambda: self._start_replying_indicator(channel, message, inbound),
            stop=lambda: self._stop_replying_indicator(channel, message, inbound),
            on_start_error=lambda exc: LOGGER.warning(
                "Reply indicator start failed for %s/%s: %s",
                message.channel,
                message.session_id,
                exc,
            ),
            on_stop_error=lambda exc: LOGGER.warning(
                "Reply indicator stop failed for %s/%s: %s",
                message.channel,
                message.session_id,
                exc,
            ),
        )
        await reply_indicator.on_reply_start()
        try:
            result = await asyncio.wrap_future(
                self.run_queue.enqueue(
                    inbound.session_id,
                    lambda: self.runtime.run_turn(inbound),
                )
            )
            reply = result.reply.strip()
            if not reply:
                return

            await channel.send_text(
                message.reply_target,
                reply,
                reply_target_type=message.reply_target_type,
                metadata={
                    "message_id": message.message_id,
                    "iterations": result.iterations,
                    "tool_events": result.tool_events,
                    **message.metadata,
                },
            )
        finally:
            if reply_indicator.on_idle:
                await reply_indicator.on_idle()

    async def _start_replying_indicator(
        self,
        channel: BaseChannel,
        message: InboundChannelMessage,
        inbound: InboundMessageContext,
    ) -> None:
        self._set_runtime_status(
            inbound.session_id,
            status="replying",
            run_id=inbound.run_id,
            source_mode=inbound.source_mode,
            channel=inbound.channel,
        )
        await channel.start_replying_indicator(message)

    async def _stop_replying_indicator(
        self,
        channel: BaseChannel,
        message: InboundChannelMessage,
        inbound: InboundMessageContext,
    ) -> None:
        try:
            await channel.stop_replying_indicator(message)
        finally:
            self._set_runtime_status(
                inbound.session_id,
                status="idle",
                run_id=None,
                source_mode=inbound.source_mode,
                channel=inbound.channel,
            )

    def _set_runtime_status(
        self,
        session_id: str,
        *,
        status: str,
        run_id: str | None,
        source_mode: str | None,
        channel: str | None,
    ) -> None:
        session_store = getattr(self.runtime, "session", None)
        if session_store is None:
            return
        updater = getattr(session_store, "update_runtime_status", None)
        if updater is None:
            return
        updater(
            session_id,
            status=status,
            run_id=run_id,
            source_mode=source_mode,
            channel=channel,
        )

    def _update_runtime_metadata(self, inbound: InboundMessageContext) -> None:
        session_store = getattr(self.runtime, "session", None)
        if session_store is None:
            return
        updater = getattr(session_store, "update_session_state", None)
        if updater is None:
            return
        metadata: dict[str, Any] = {}
        if inbound.source_mode:
            metadata["source_mode"] = inbound.source_mode
        if inbound.channel:
            metadata["channel"] = inbound.channel
        if inbound.capabilities:
            metadata["channel_capabilities"] = list(inbound.capabilities)
        metadata["actor_identity_status"] = inbound.actor_identity_status
        metadata["actor_role"] = inbound.actor_role
        metadata["actor_is_owner"] = inbound.actor_is_owner
        if inbound.actor_user_id:
            metadata["actor_user_id"] = inbound.actor_user_id
        if inbound.actor_external_id:
            metadata["actor_external_id"] = inbound.actor_external_id
        if metadata:
            updater(inbound.session_id, metadata=metadata)

    def _resolve_actor_identity(self, message: InboundChannelMessage):
        session_store = getattr(self.runtime, "session", None)
        if session_store is None:
            return build_unknown_identity(
                channel=message.channel,
                external_id=message.sender_id,
            )
        binding = getattr(session_store, "get_identity_binding", None)
        if binding is None:
            return build_unknown_identity(
                channel=message.channel,
                external_id=message.sender_id,
            )
        resolved = binding(channel=message.channel, external_id=message.sender_id)
        if resolved is None:
            return build_unknown_identity(
                channel=message.channel,
                external_id=message.sender_id,
            )
        return build_bound_identity(
            channel=resolved.channel,
            external_id=resolved.external_id,
            canonical_user_id=resolved.user_id,
            role=resolved.user_role,
        )

    def start_background(self) -> None:
        """Start channel listeners on a dedicated asyncio loop thread."""
        if not self.channels:
            return
        if self._thread and self._thread.is_alive():
            return

        self._started.clear()
        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()
        self._started.wait(timeout=5)

    def _run_loop(self) -> None:
        loop = asyncio.new_event_loop()
        self._loop = loop
        asyncio.set_event_loop(loop)
        self._channel_tasks = [
            loop.create_task(channel.start(), name=f"channel:{name}")
            for name, channel in self.channels.items()
        ]
        self._started.set()
        try:
            loop.run_forever()
        finally:
            pending = [task for task in asyncio.all_tasks(loop) if not task.done()]
            for task in pending:
                task.cancel()
            if pending:
                loop.run_until_complete(
                    asyncio.gather(*pending, return_exceptions=True)
                )
            loop.close()
            self._loop = None

    async def _stop_async(self) -> None:
        for channel in self.channels.values():
            try:
                await channel.stop()
            except Exception:  # pragma: no cover
                LOGGER.exception("Failed to stop channel %s", channel.name)
        if self._channel_tasks:
            await asyncio.gather(*self._channel_tasks, return_exceptions=True)
        loop = asyncio.get_running_loop()
        loop.stop()

    def stop_background(self) -> None:
        """Stop all channel listeners."""
        if not self._loop:
            return
        future = asyncio.run_coroutine_threadsafe(self._stop_async(), self._loop)
        future.result(timeout=10)
        if self._thread:
            self._thread.join(timeout=10)
        self._thread = None
        self._started.clear()

    def get_status(self) -> dict[str, Any]:
        """Expose channel status for health checks."""
        return {
            name: {
                "enabled": True,
                "running": channel.is_running,
                "replying_sessions": list(channel.replying_sessions),
                "replying_count": len(channel.replying_sessions),
            }
            for name, channel in self.channels.items()
        }

    def shutdown(self) -> None:
        """Stop channel listeners and release queue workers."""
        self.stop_background()
        self.run_queue.shutdown(wait=False)
