from __future__ import annotations

import typing
import asyncio
import traceback
import base64
import json

import nio

from langbot.pkg.utils import httpclient
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.platform.events as platform_events
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger


class MatrixMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
    @staticmethod
    async def yiri2target(message_chain: platform_message.MessageChain, client: nio.AsyncClient) -> list[dict]:
        components = []
        for component in message_chain:
            if isinstance(component, platform_message.Plain):
                components.append({'type': 'text', 'text': component.text})
            elif isinstance(component, platform_message.Image):
                image_bytes = None
                if component.base64:
                    b64_data = component.base64
                    if ';base64,' in b64_data:
                        b64_data = b64_data.split(';base64,', 1)[1]
                    image_bytes = base64.b64decode(b64_data)
                elif component.url:
                    session = httpclient.get_session()
                    async with session.get(component.url) as response:
                        image_bytes = await response.read()
                elif component.path:
                    with open(component.path, 'rb') as f:
                        image_bytes = f.read()
                if image_bytes:
                    resp = await client.upload(image_bytes, content_type='image/png')
                    if isinstance(resp, nio.UploadResponse):
                        components.append({'type': 'image', 'mxc_url': resp.content_uri})
            elif isinstance(component, platform_message.File):
                file_bytes = None
                if component.base64:
                    b64_data = component.base64
                    if ';base64,' in b64_data:
                        b64_data = b64_data.split(';base64,', 1)[1]
                    file_bytes = base64.b64decode(b64_data)
                elif component.url:
                    session = httpclient.get_session()
                    async with session.get(component.url) as response:
                        file_bytes = await response.read()
                elif component.path:
                    with open(component.path, 'rb') as f:
                        file_bytes = f.read()
                if file_bytes:
                    file_name = getattr(component, 'name', None) or 'file'
                    resp = await client.upload(file_bytes, content_type='application/octet-stream', filename=file_name)
                    if isinstance(resp, nio.UploadResponse):
                        components.append(
                            {
                                'type': 'file',
                                'mxc_url': resp.content_uri,
                                'filename': file_name,
                                'size': len(file_bytes),
                            }
                        )
            elif isinstance(component, platform_message.Forward):
                for node in component.node_list:
                    components.extend(await MatrixMessageConverter.yiri2target(node.message_chain, client))
        return components

    @staticmethod
    async def target2yiri(event: nio.RoomMessageText | nio.RoomMessageImage, client: nio.AsyncClient, bot_user_id: str):
        message_components = []

        if isinstance(event, nio.RoomMessageText):
            text = event.body
            if bot_user_id and bot_user_id in text:
                message_components.append(platform_message.At(target=bot_user_id))
                text = text.replace(bot_user_id, '').strip()
            message_components.append(platform_message.Plain(text=text))

        elif isinstance(event, nio.RoomMessageImage):
            mxc_url = event.url
            if mxc_url:
                resp = await client.download(mxc_url)
                if isinstance(resp, nio.DownloadResponse):
                    b64 = base64.b64encode(resp.body).decode('utf-8')
                    content_type = resp.content_type or 'image/png'
                    message_components.append(platform_message.Image(base64=f'data:{content_type};base64,{b64}'))
            if event.body:
                message_components.append(platform_message.Plain(text=event.body))

        return platform_message.MessageChain(message_components)


class MatrixEventConverter(abstract_platform_adapter.AbstractEventConverter):
    @staticmethod
    async def yiri2target(event: platform_events.MessageEvent):
        return event.source_platform_object

    @staticmethod
    async def target2yiri(
        event: nio.RoomMessageText | nio.RoomMessageImage,
        room: nio.MatrixRoom,
        client: nio.AsyncClient,
        bot_user_id: str,
        bridge_user_ids: list[str] | None = None,
    ):
        lb_message = await MatrixMessageConverter.target2yiri(event, client, bot_user_id)

        # Determine if this is a direct/private chat or a group chat.
        # Exclude bot itself and bridge bots, count remaining real users.
        exclude_ids = {bot_user_id}
        if bridge_user_ids:
            exclude_ids.update(bridge_user_ids)
        real_users = [uid for uid in room.users if uid not in exclude_ids]
        is_direct = len(real_users) <= 1

        if is_direct:
            return platform_events.FriendMessage(
                sender=platform_entities.Friend(
                    id=event.sender,
                    nickname=room.user_name(event.sender) or event.sender,
                    remark='',
                ),
                message_chain=lb_message,
                time=event.server_timestamp / 1000.0,
                source_platform_object={'event': event, 'room': room},
            )
        else:
            return platform_events.GroupMessage(
                sender=platform_entities.GroupMember(
                    id=event.sender,
                    member_name=room.user_name(event.sender) or event.sender,
                    permission=platform_entities.Permission.Member,
                    group=platform_entities.Group(
                        id=room.room_id,
                        name=room.display_name or room.room_id,
                        permission=platform_entities.Permission.Member,
                    ),
                    special_title='',
                ),
                message_chain=lb_message,
                time=event.server_timestamp / 1000.0,
                source_platform_object={'event': event, 'room': room},
            )


class BridgeState:
    """Per-bridge runtime state."""

    def __init__(self, user_id: str, login_command: str, logout_command: str, success_keyword: str, check_command: str):
        self.user_id = user_id
        self.login_command = login_command
        self.logout_command = logout_command
        self.success_keyword = success_keyword
        self.check_command = check_command or login_command
        self.logged_in = False
        self.dm_room_id: str | None = None
        self.login_task: asyncio.Task | None = None
        self.check_task: asyncio.Task | None = None
        self.check_responded = False


class MatrixAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
    client: typing.Any = None
    message_converter: MatrixMessageConverter = MatrixMessageConverter()
    event_converter: MatrixEventConverter = MatrixEventConverter()
    config: dict
    listeners: typing.Dict[typing.Type[platform_events.Event], typing.Callable] = {}
    _running: bool = False
    _initial_sync_done: bool = False
    _bridges: list = []

    def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger):
        homeserver_url = config.get('homeserver_url', '')
        access_token = config.get('access_token', '')
        user_id = config.get('user_id', '')

        if not homeserver_url or not access_token or not user_id:
            raise ValueError('Matrix 机器人缺少必要配置项 (homeserver_url, user_id, access_token)')

        client = nio.AsyncClient(homeserver_url, user_id)
        client.access_token = access_token
        client.user_id = user_id

        super().__init__(
            config=config,
            logger=logger,
            bot_account_id=user_id,
            client=client,
            listeners={},
        )

        # Parse bridges config AFTER super().__init__() to avoid Pydantic resetting _bridges
        self._bridges = []
        bridges_raw = config.get('bridges', '')
        if bridges_raw:
            if isinstance(bridges_raw, str):
                try:
                    bridges_list = json.loads(bridges_raw)
                except (json.JSONDecodeError, TypeError) as e:
                    raise ValueError(f'bridges 配置 JSON 解析失败: {e}\n原始值: {bridges_raw}')
            else:
                bridges_list = bridges_raw
            for b in bridges_list:
                if isinstance(b, dict) and b.get('user_id', '').strip():
                    self._bridges.append(
                        BridgeState(
                            user_id=b['user_id'].strip(),
                            login_command=b.get('login_command', '').strip(),
                            logout_command=b.get('logout_command', '').strip(),
                            success_keyword=b.get('success_keyword', 'Successfully logged in').strip(),
                            check_command=b.get('check_command', '').strip(),
                        )
                    )
        # Backward compatibility: old single-bridge config
        if not self._bridges:
            old_user_id = config.get('bridge_user_id', '').strip()
            old_command = config.get('bridge_login_command', '').strip()
            old_keyword = config.get('bridge_login_success_keyword', 'Successfully logged in').strip()
            old_check = config.get('bridge_check_command', '').strip()
            old_logout = config.get('bridge_logout_command', '').strip()
            if old_user_id:
                self._bridges.append(
                    BridgeState(
                        user_id=old_user_id,
                        login_command=old_command,
                        logout_command=old_logout,
                        success_keyword=old_keyword,
                        check_command=old_check,
                    )
                )

    async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
        components = await self.message_converter.yiri2target(message, self.client)
        for component in components:
            await self._send_component(target_id, component)

    async def reply_message(
        self,
        message_source: platform_events.MessageEvent,
        message: platform_message.MessageChain,
        quote_origin: bool = False,
    ):
        source_obj = message_source.source_platform_object
        room_id = source_obj['room'].room_id
        components = await self.message_converter.yiri2target(message, self.client)

        for component in components:
            if quote_origin:
                original_event = source_obj['event']
                await self._send_component(room_id, component, reply_to=original_event.event_id)
            else:
                await self._send_component(room_id, component)

    async def _send_component(self, room_id: str, component: dict, reply_to: str | None = None):
        content = {}
        if component['type'] == 'text':
            content = {
                'msgtype': 'm.text',
                'body': component['text'],
            }
        elif component['type'] == 'image':
            content = {
                'msgtype': 'm.image',
                'body': 'image.png',
                'url': component['mxc_url'],
            }
        elif component['type'] == 'file':
            content = {
                'msgtype': 'm.file',
                'body': component.get('filename', 'file'),
                'url': component['mxc_url'],
                'info': {'size': component.get('size', 0)},
            }

        if reply_to and content:
            content['m.relates_to'] = {
                'm.in_reply_to': {'event_id': reply_to},
            }

        if content:
            await self.client.room_send(
                room_id=room_id,
                message_type='m.room.message',
                content=content,
            )

    def register_listener(
        self,
        event_type: typing.Type[platform_events.Event],
        callback: typing.Callable[
            [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
        ],
    ):
        self.listeners[event_type] = callback

    async def run_async(self):
        self._running = True
        await self.logger.info('Matrix adapter starting...')

        # Debug: log bridge parsing result
        bridges_raw = self.config.get('bridges', '')
        await self.logger.debug(f'bridges config raw: type={type(bridges_raw).__name__}, repr={repr(bridges_raw)}')
        await self.logger.debug(
            f'parsed _bridges count: {len(self._bridges)}, ids: {[b.user_id for b in self._bridges]}'
        )

        # Collect all bridge bot user IDs for filtering
        _bridge_user_ids = [b.user_id for b in self._bridges]
        _bridge_user_id_set = set(_bridge_user_ids)

        # Auto-join invited rooms
        async def on_invite(room: nio.MatrixRoom, event: nio.InviteMemberEvent):
            if event.membership == 'invite' and event.state_key == self.client.user_id:
                await self.client.join(room.room_id)
                await self.logger.debug(f'Auto-joined room: {room.display_name or room.room_id}')

        self.client.add_event_callback(on_invite, nio.InviteMemberEvent)

        # Handle text messages
        async def on_message(room: nio.MatrixRoom, event: nio.RoomMessageText):
            if not self._initial_sync_done:
                return
            if event.sender == self.client.user_id:
                return

            # Admin commands (from any non-bridge user)
            if event.sender not in _bridge_user_id_set:
                body = (event.body or '').strip()
                if body == '!relogin':
                    await self._handle_relogin_command(room.room_id)
                    return
                if body == '!status':
                    await self._handle_status_command(room.room_id)
                    return

            if event.sender in _bridge_user_id_set:
                return
            try:
                lb_event = await self.event_converter.target2yiri(
                    event, room, self.client, self.bot_account_id, _bridge_user_ids
                )
                if type(lb_event) in self.listeners:
                    result = self.listeners[type(lb_event)](lb_event, self)
                    if asyncio.iscoroutine(result):
                        await result
            except Exception:
                await self.logger.error(f'Error handling Matrix message: {traceback.format_exc()}')

        self.client.add_event_callback(on_message, nio.RoomMessageText)

        # Handle image messages
        async def on_image(room: nio.MatrixRoom, event: nio.RoomMessageImage):
            if not self._initial_sync_done:
                return
            if event.sender == self.client.user_id:
                return
            if event.sender in _bridge_user_id_set:
                return
            try:
                lb_event = await self.event_converter.target2yiri(
                    event, room, self.client, self.bot_account_id, _bridge_user_ids
                )
                if type(lb_event) in self.listeners:
                    result = self.listeners[type(lb_event)](lb_event, self)
                    if asyncio.iscoroutine(result):
                        await result
            except Exception:
                await self.logger.error(f'Error handling Matrix image: {traceback.format_exc()}')

        self.client.add_event_callback(on_image, nio.RoomMessageImage)

        # Set up bridge-specific callbacks for each bridge
        _disconnect_keywords = ['disconnected', 'logged out', 'connection lost', 'session expired', 'token expired']

        for bridge in self._bridges:
            # Login success detection (notice)
            async def on_bridge_notice(room: nio.MatrixRoom, event: nio.RoomMessageNotice, _b=bridge):
                if not self._initial_sync_done:
                    return
                if event.sender != _b.user_id:
                    return
                _b.check_responded = True
                if _b.success_keyword in (event.body or ''):
                    _b.logged_in = True
                    await self.logger.info(f'[{_b.user_id}] Bridge login succeeded.')
                # Disconnect detection
                body_lower = (event.body or '').lower()
                for kw in _disconnect_keywords:
                    if kw in body_lower and _b.logged_in:
                        _b.logged_in = False
                        await self.logger.info(f'[{_b.user_id}] Bridge 账号掉线 (检测到: "{kw}"), 将自动重新登录...')
                        self._restart_bridge_login(_b)
                        break

            self.client.add_event_callback(on_bridge_notice, nio.RoomMessageNotice)

            # Login success + disconnect detection (text)
            async def on_bridge_text(room: nio.MatrixRoom, event: nio.RoomMessageText, _b=bridge):
                if not self._initial_sync_done:
                    return
                if event.sender != _b.user_id:
                    return
                _b.check_responded = True
                if _b.success_keyword in (event.body or ''):
                    _b.logged_in = True
                    await self.logger.info(f'[{_b.user_id}] Bridge login succeeded.')
                body_lower = (event.body or '').lower()
                for kw in _disconnect_keywords:
                    if kw in body_lower and _b.logged_in:
                        _b.logged_in = False
                        await self.logger.info(f'[{_b.user_id}] Bridge 账号掉线 (检测到: "{kw}"), 将自动重新登录...')
                        self._restart_bridge_login(_b)
                        break

            self.client.add_event_callback(on_bridge_text, nio.RoomMessageText)

            # QR code image forwarding
            async def on_bridge_image(room: nio.MatrixRoom, event: nio.RoomMessageImage, _b=bridge):
                if not self._initial_sync_done:
                    return
                if event.sender != _b.user_id:
                    return
                mxc_url = event.url
                if not mxc_url:
                    return
                try:
                    resp = await self.client.download(mxc_url)
                    if isinstance(resp, nio.DownloadResponse):
                        b64 = base64.b64encode(resp.body).decode('utf-8')
                        content_type = resp.content_type or 'image/png'
                        await self.logger.info(
                            f'[{_b.user_id}] Bridge 发送了二维码，请扫码登录:',
                            images=[platform_message.Image(base64=f'data:{content_type};base64,{b64}')],
                        )
                except Exception:
                    await self.logger.error(
                        f'[{_b.user_id}] Failed to download bridge QR image: {traceback.format_exc()}'
                    )

            self.client.add_event_callback(on_bridge_image, nio.RoomMessageImage)

        await self.logger.debug('Matrix adapter running, starting sync...')

        # Initial sync to skip old messages
        resp = await self.client.sync(timeout=10000)
        if isinstance(resp, nio.SyncResponse):
            await self.logger.debug(f'Matrix initial sync done, next_batch: {resp.next_batch}')
        self._initial_sync_done = True

        # Display account info
        display_name = self.client.user_id
        try:
            profile_resp = await self.client.get_displayname(self.client.user_id)
            if isinstance(profile_resp, nio.ProfileGetDisplayNameResponse) and profile_resp.displayname:
                display_name = profile_resp.displayname
        except Exception:
            pass
        joined_rooms = len(self.client.rooms)
        homeserver = self.config.get('homeserver_url', '')
        bridge_info = ''
        if self._bridges:
            bridge_names = ', '.join(b.user_id for b in self._bridges)
            bridge_info = f' | 桥接: [{bridge_names}]'
        await self.logger.info(
            f'Matrix 账号: {display_name} ({self.client.user_id}) | '
            f'服务器: {homeserver} | 已加入 {joined_rooms} 个房间{bridge_info}'
        )

        # Start bridge login and status check tasks for each bridge
        for bridge in self._bridges:
            if bridge.login_command:
                await self.logger.info(
                    f'[{bridge.user_id}] Bridge login enabled (命令: "{bridge.login_command}", '
                    f'关键词: "{bridge.success_keyword}")'
                )
                bridge.login_task = asyncio.create_task(self._periodic_bridge_login(bridge))
                bridge.check_task = asyncio.create_task(self._periodic_bridge_check(bridge))
            else:
                await self.logger.debug(f'[{bridge.user_id}] Bridge login not configured (no login_command)')

        # Main sync loop
        while self._running:
            try:
                await self.client.sync(timeout=30000)
            except Exception:
                await self.logger.error(f'Matrix sync error: {traceback.format_exc()}')
                await asyncio.sleep(5)

    async def _periodic_bridge_login(self, bridge: BridgeState):
        """Periodically send login command to a bridge bot until login succeeds."""
        try:
            await self.logger.info(f'[{bridge.user_id}] Bridge login task started, looking for DM room...')
            dm_room_id = None
            for room_id, room in self.client.rooms.items():
                if room.member_count == 2 and bridge.user_id in [m for m in room.users]:
                    dm_room_id = room_id
                    break

            if not dm_room_id:
                resp = await self.client.room_create(
                    is_direct=True,
                    invite=[bridge.user_id],
                )
                if isinstance(resp, nio.RoomCreateResponse):
                    dm_room_id = resp.room_id
                    await self.logger.debug(f'[{bridge.user_id}] Created DM room: {dm_room_id}')
                else:
                    await self.logger.error(f'[{bridge.user_id}] Failed to create DM room: {resp}')
                    return

            bridge.dm_room_id = dm_room_id

            # Force logout first on every adapter start
            logout_cmd = bridge.logout_command or bridge.login_command.replace('login', 'logout')
            await self.logger.info(f'[{bridge.user_id}] 强制登出: "{logout_cmd}"')
            await self.client.room_send(
                room_id=dm_room_id,
                message_type='m.room.message',
                content={'msgtype': 'm.text', 'body': logout_cmd},
            )
            await asyncio.sleep(3)

            while self._running and not bridge.logged_in:
                await self.logger.debug(f'[{bridge.user_id}] Sending "{bridge.login_command}" in room {dm_room_id}')
                await self.client.room_send(
                    room_id=dm_room_id,
                    message_type='m.room.message',
                    content={'msgtype': 'm.text', 'body': bridge.login_command},
                )
                for _ in range(60):
                    if not self._running or bridge.logged_in:
                        break
                    await asyncio.sleep(1)

            if bridge.logged_in:
                await self.logger.debug(f'[{bridge.user_id}] Bridge login confirmed, periodic login stopped.')
        except asyncio.CancelledError:
            pass
        except Exception:
            await self.logger.error(f'[{bridge.user_id}] Bridge periodic login error: {traceback.format_exc()}')

    def _restart_bridge_login(self, bridge: BridgeState):
        """Cancel existing login task and start a new one."""
        if bridge.login_task and not bridge.login_task.done():
            bridge.login_task.cancel()
        bridge.login_task = asyncio.create_task(self._periodic_bridge_login(bridge))

    async def _periodic_bridge_check(self, bridge: BridgeState):
        """Periodically check a bridge's login status."""
        try:
            while self._running and not bridge.logged_in:
                await asyncio.sleep(5)

            check_interval = 300  # 5 minutes
            response_timeout = 30
            await self.logger.debug(f'[{bridge.user_id}] Bridge status check started (interval: {check_interval}s)')

            while self._running:
                for _ in range(check_interval):
                    if not self._running:
                        return
                    await asyncio.sleep(1)

                if not bridge.logged_in or not bridge.dm_room_id:
                    continue

                try:
                    bridge.check_responded = False
                    await self.client.room_send(
                        room_id=bridge.dm_room_id,
                        message_type='m.room.message',
                        content={'msgtype': 'm.text', 'body': bridge.check_command},
                    )
                    await self.logger.debug(f'[{bridge.user_id}] Bridge status check: sent "{bridge.check_command}"')

                    for _ in range(response_timeout):
                        if bridge.check_responded or not self._running:
                            break
                        await asyncio.sleep(1)

                    if bridge.check_responded:
                        await self.logger.debug(f'[{bridge.user_id}] Bridge status check: OK')
                    else:
                        await self.logger.info(
                            f'[{bridge.user_id}] Bridge status check: 无响应, 可能已掉线, 尝试重新登录...'
                        )
                        bridge.logged_in = False
                        self._restart_bridge_login(bridge)
                except Exception:
                    await self.logger.error(f'[{bridge.user_id}] Bridge status check error: {traceback.format_exc()}')
        except asyncio.CancelledError:
            pass
        except Exception:
            await self.logger.error(f'[{bridge.user_id}] Bridge status check fatal error: {traceback.format_exc()}')

    async def _handle_relogin_command(self, room_id: str):
        """Handle !relogin command: logout then re-login all bridges."""
        if not self._bridges:
            await self.client.room_send(
                room_id=room_id,
                message_type='m.room.message',
                content={'msgtype': 'm.text', 'body': '没有配置任何桥。'},
            )
            return

        lines = ['开始重新登录所有桥...']
        for bridge in self._bridges:
            if not bridge.login_command or not bridge.dm_room_id:
                lines.append(f'[{bridge.user_id}] 跳过（未配置登录命令或无DM房间）')
                continue

                # Use configured logout command, fallback to deriving from login command
                logout_cmd = bridge.logout_command or bridge.login_command.replace('login', 'logout')
            lines.append(f'[{bridge.user_id}] 发送 "{logout_cmd}"...')

            # Cancel existing tasks
            if bridge.login_task and not bridge.login_task.done():
                bridge.login_task.cancel()
            if bridge.check_task and not bridge.check_task.done():
                bridge.check_task.cancel()

            # Send logout
            try:
                await self.client.room_send(
                    room_id=bridge.dm_room_id,
                    message_type='m.room.message',
                    content={'msgtype': 'm.text', 'body': logout_cmd},
                )
            except Exception as e:
                lines.append(f'[{bridge.user_id}] logout 发送失败: {e}')

            await asyncio.sleep(2)

            # Reset state and restart login
            bridge.logged_in = False
            self._restart_bridge_login(bridge)
            lines.append(f'[{bridge.user_id}] 已触发重新登录')

        await self.client.room_send(
            room_id=room_id,
            message_type='m.room.message',
            content={'msgtype': 'm.text', 'body': '\n'.join(lines)},
        )

    async def _handle_status_command(self, room_id: str):
        """Handle !status command: show bridge states."""
        if not self._bridges:
            await self.client.room_send(
                room_id=room_id,
                message_type='m.room.message',
                content={'msgtype': 'm.text', 'body': '没有配置任何桥。'},
            )
            return

        lines = ['桥状态:']
        for bridge in self._bridges:
            status = '已登录 ✓' if bridge.logged_in else '未登录 ✗'
            dm = bridge.dm_room_id or '无'
            lines.append(f'• {bridge.user_id}: {status} (DM: {dm})')
        await self.client.room_send(
            room_id=room_id,
            message_type='m.room.message',
            content={'msgtype': 'm.text', 'body': '\n'.join(lines)},
        )

    async def kill(self) -> bool:
        self._running = False
        for bridge in self._bridges:
            if bridge.login_task and not bridge.login_task.done():
                bridge.login_task.cancel()
            if bridge.check_task and not bridge.check_task.done():
                bridge.check_task.cancel()
        if self.client:
            await self.client.close()
        await self.logger.debug('Matrix adapter stopped')
        return True

    async def unregister_listener(
        self,
        event_type: typing.Type[platform_events.Event],
        callback: typing.Callable[
            [platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
        ],
    ):
        if event_type in self.listeners:
            del self.listeners[event_type]
