"""
MS Teams Agent Wrapper.

Connects MS Teams messages to AI-Parrot agents.
"""
import logging
import json
import re
import asyncio
import contextlib
from http import HTTPStatus
from pathlib import Path
from typing import Dict, Optional, Any, Union
from aiohttp import web
from botbuilder.core import (
    ActivityHandler,
    TurnContext,
    ConversationState,
    MemoryStorage,
    UserState,
    MessageFactory,
    CardFactory,
)
import jsonpickle
from botbuilder.core import MemoryStorage
from botbuilder.core.teams import TeamsInfo
from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, Attachment
from botbuilder.dialogs import DialogSet, DialogTurnStatus
from .models import MSTeamsAgentConfig
from .adapter import Adapter
from .handler import MessageHandler
from ..parser import parse_response, ParsedResponse
from ...models.outputs import OutputMode
from .dialogs.orchestrator import FormOrchestrator
from .dialogs.factory import FormDialogFactory
from .dialogs.card_builder import AdaptiveCardBuilder
from .dialogs.validator import FormValidator
from ..dialogs.models import FormDefinition, DialogPreset
from ..dialogs.cache import FormDefinitionCache
from .voice import VoiceTranscriber, VoiceTranscriberConfig


logging.getLogger('msrest').setLevel(logging.WARNING)

class DebugMemoryStorage(MemoryStorage):
    async def write(self, changes):
        for k, v in changes.items():
            try:
                jsonpickle.encode(v)
            except Exception as e:
                print("\n=== JSONPICKLE FAILED ===")
                print("storage key:", k)
                print("value type:", type(v))
                # if it's a dict, dump top-level types
                if isinstance(v, dict):
                    print("top-level dict keys/types:")
                    for kk, vv in v.items():
                        print(" ", kk, type(vv))
                raise
        return await super().write(changes)


class MSTeamsAgentWrapper(ActivityHandler, MessageHandler):
    """
    Wraps an Agent for MS Teams integration.

    Features:
    - Sends responses as Adaptive Cards with markdown support
    - Handles images, documents, code blocks, and tables
    - Supports rich formatting via ParsedResponse
    - Automatic form detection when LLM calls request_form
    - YAML-based form definitions
    - Multi-step wizard dialogs
    - Post-form tool execution
    """

    # Supported audio content types for voice notes
    AUDIO_CONTENT_TYPES = {
        "audio/ogg", "audio/mpeg", "audio/wav", "audio/x-wav",
        "audio/mp4", "audio/webm", "video/webm", "audio/m4a", "audio/mp3"
    }

    def __init__(
        self,
        agent: Any,
        config: MSTeamsAgentConfig,
        app: web.Application,
        forms_directory: Optional[str] = None,
        voice_config: Optional[VoiceTranscriberConfig] = None,
    ):
        super().__init__()
        self.agent = agent
        self.config = config
        self.app = app
        self.logger = logging.getLogger(f"MSTeamsWrapper.{config.name}")

        # Voice transcription
        self._voice_config = voice_config or VoiceTranscriberConfig(enabled=False)
        self._voice_transcriber: Optional[VoiceTranscriber] = None
        if self._voice_config.enabled:
            self._voice_transcriber = VoiceTranscriber(self._voice_config)

        # State Management
        self.memory = DebugMemoryStorage()
        self.conversation_state = ConversationState(self.memory)
        self.user_state = UserState(self.memory)

        # Form cache
        self.form_cache = FormDefinitionCache(
            forms_directory=forms_directory or str(
                Path(config.forms_directory) if hasattr(config, 'forms_directory') and config.forms_directory else None
            ),
            watch_files=True,
        )

        # Dialog state
        self.dialog_state = self.conversation_state.create_property(
            "DialogState"
        )
        self.dialogs = DialogSet(self.dialog_state)

        # Form components
        # NOTE: card_builder and validator are no longer stored - dialogs create fresh instances
        self.dialog_factory = FormDialogFactory()

        # Form orchestrator (handles LLM form requests)
        self.form_orchestrator = FormOrchestrator(
            agent=agent,
            dialog_factory=self.dialog_factory,
            form_cache=self.form_cache,
        )

        # Initialize Adapter
        self.adapter = Adapter(
            config=self.config,
            logger=self.logger,
            conversation_state=self.conversation_state
        )

        # Route
        # Clean chatbot_id to be safe for URL
        safe_id = self.config.chatbot_id.replace(' ', '_').lower()
        self.route = f"/api/teambots/{safe_id}/messages"
        # Register Handler
        self.app.router.add_post(self.route, self.handle_request)
        self.logger.info(
            f"Registered MS Teams webhook at {self.route}"
        )

        # Register route as auth exclusion
        if auth := self.app.get("auth"):
            auth.add_exclude_list(self.route)
            self.logger.info(
                f"Excluded {self.route} from auth middleware"
            )

        # Load predefined YAML forms
        asyncio.create_task(self._load_yaml_forms())

    async def _load_yaml_forms(self):
        """Load predefined form definitions from YAML files."""
        try:
            await self.form_cache.load_directory()
            self.logger.info("Loaded YAML form definitions")
        except Exception as e:
            self.logger.warning(f"Error loading YAML forms: {e}")

    # =========================================================================
    # Form Dialog Management
    # =========================================================================

    async def _start_form_dialog(
        self,
        dialog_context,
        form: FormDefinition,
        conversation_id: str,
        turn_context: TurnContext = None,
    ):
        """Start a form dialog."""

        # Create dialog from form definition
        # NOTE: We don't pass callbacks here to avoid serialization issues with jsonpickle
        # The wrapper handles completion/cancellation after dialog ends
        dialog = self.dialog_factory.create_dialog(
            form=form,
            on_complete=None,  # Handle in wrapper instead
            on_cancel=None,    # Handle in wrapper instead
        )

        # Store conversation_id in dialog for later use
        dialog._conversation_id = conversation_id

        # Add to dialog set (replace if exists)
        if form.form_id in self.dialogs._dialogs:
            self.dialogs._dialogs.pop(form.form_id)
        self.dialogs.add(dialog)

        # Begin dialog
        await dialog_context.begin_dialog(form.form_id)

        # Explicitly save state to ensure dialog is persisted before next interaction
        if turn_context:
            await self.conversation_state.save_changes(turn_context)

        self.logger.info(f"Started form dialog: {form.form_id}")

    async def _on_form_complete(
        self,
        form_data: Dict[str, Any],
        turn_context: TurnContext,
        conversation_id: str,
    ):
        """Handle form completion."""
        self.logger.info(f"Form completed with data: {list(form_data.keys())}")

        # Delegate to orchestrator for tool execution
        response = await self.form_orchestrator.handle_form_completion(
            form_data=form_data,
            conversation_id=conversation_id,
            turn_context=turn_context,
        )

        # Check if response is an Adaptive Card (dict) or plain text (str)
        if isinstance(response, dict):
            await self.send_card(response, turn_context)
        else:
            await self.send_text(response, turn_context)

    async def _on_form_cancel(
        self,
        turn_context: TurnContext,
        conversation_id: str,
    ):
        """Handle form cancellation."""
        await self.form_orchestrator.handle_form_cancellation(conversation_id)
        await self.send_text("Form cancelled.", turn_context)

    # =========================================================================
    # Authorization
    # =========================================================================

    def _is_authorized(self, conversation_id: str, user_id: str) -> bool:
        """Check if a conversation and user are authorized for this bot.

        Both whitelists apply with AND logic: a request must pass both the
        conversation whitelist and the user whitelist to be authorized.
        If a whitelist is None (default), that check is skipped (allow all).

        Args:
            conversation_id: The Teams conversation/channel ID.
            user_id: The Teams user ID.

        Returns:
            True if authorized, False otherwise.
        """
        if self.config.allowed_conversation_ids is not None:
            if conversation_id not in self.config.allowed_conversation_ids:
                return False
        if self.config.allowed_user_ids is not None:
            if user_id not in self.config.allowed_user_ids:
                return False
        return True

    # =========================================================================
    # Card Submission Handling
    # =========================================================================

    async def _handle_card_submission(
        self,
        turn_context: TurnContext,
        dialog_context,
    ):
        """Handle Adaptive Card form submission."""
        submitted_data = turn_context.activity.value

        if not submitted_data:
            return

        # Authorization check
        conversation_id = turn_context.activity.conversation.id
        user_id = turn_context.activity.from_property.id
        if not self._is_authorized(conversation_id, user_id):
            self.logger.warning(
                "Unauthorized card submission: user=%s, conversation=%s",
                user_id,
                conversation_id,
            )
            await turn_context.send_activity("You are not authorized to use this bot.")
            return

        self.logger.info(f"Card submission: {submitted_data}")

        # Check for action type
        action = submitted_data.get('_action', 'submit')

        if action == 'cancel':
            # Cancel active dialog
            await dialog_context.cancel_all_dialogs()
            await self._on_form_cancel(turn_context, conversation_id)
            return

        # Set agent in turn_state for dialogs to access (ephemeral per-turn)
        turn_context.turn_state["FormDialog.agent"] = self.agent

        # Continue dialog with submitted data
        # The dialog's waterfall will pick up the data from activity.value
        results = await dialog_context.continue_dialog()

        self.logger.info(f"Dialog continue result: status={results.status}")

        if results.status == DialogTurnStatus.Complete:
            # Dialog finished - handle completion
            form_data = results.result
            if form_data and not form_data.get('_cancelled'):
                await self._on_form_complete(form_data, turn_context, conversation_id)
            elif form_data and form_data.get('_cancelled'):
                await self._on_form_cancel(turn_context, conversation_id)

        elif results.status == DialogTurnStatus.Empty:
            # No active dialog - might be a standalone card
            self.logger.warning("Card submission but no active dialog")
            await self.send_text(
                "I received your submission but wasn't expecting it. Please try again.",
                turn_context
            )

    # =========================================================================
    # Webhook Handler
    # =========================================================================

    async def handle_request(self, request: web.Request) -> web.Response:
        """
        Handle incoming webhook requests.
        """
        if request.content_type.lower() != 'application/json':
            return web.Response(status=HTTPStatus.UNSUPPORTED_MEDIA_TYPE)

        body = await request.json()
        activity = Activity().deserialize(body)
        auth_header = request.headers.get('Authorization', '')

        try:
            response = await self.adapter.process_activity(
                auth_header, activity, self.on_turn
            )
            if response:
                return web.json_response(
                    data=response.body,
                    status=response.status
                )
            return web.Response(status=HTTPStatus.OK)

        except Exception as e:
            self.logger.error(f"Error processing request: {e}", exc_info=True)
            return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)

    async def on_turn(self, turn_context: TurnContext):
        """
        Handle the turn. Application logic.
        """
        # Save state changes after routing
        await super().on_turn(turn_context)
        await self.conversation_state.save_changes(turn_context)
        await self.user_state.save_changes(turn_context)

    async def on_message_activity(self, turn_context: TurnContext):
        """Handle incoming messages including voice notes."""
        # Authorization check
        conversation_id = turn_context.activity.conversation.id
        user_id = turn_context.activity.from_property.id
        if not self._is_authorized(conversation_id, user_id):
            self.logger.warning(
                "Unauthorized access attempt: user=%s, conversation=%s",
                user_id,
                conversation_id,
            )
            await turn_context.send_activity("You are not authorized to use this bot.")
            return

        # DEBUG: Log activity details
        self.logger.info(f"🔍 on_message_activity: type={turn_context.activity.type}, "
                         f"has_value={turn_context.activity.value is not None}, "
                         f"text={turn_context.activity.text[:50] if turn_context.activity.text else 'None'}")
        if turn_context.activity.value:
            self.logger.info(f"🔍 Activity value: {turn_context.activity.value}")

        # Create dialog context
        dialog_context = await self.dialogs.create_context(turn_context)
        conversation_id = turn_context.activity.conversation.id

        # DEBUG: Check dialog stack
        self.logger.info(f"🔍 Dialog stack size: {len(dialog_context.stack) if dialog_context.stack else 0}")

        # Handle Adaptive Card submissions
        if turn_context.activity.value:
            await self._handle_card_submission(
                turn_context,
                dialog_context
            )
            return

        # Check for audio attachments BEFORE text processing
        audio_attachment = self._find_audio_attachment(turn_context.activity)
        if audio_attachment and self._voice_config.enabled and self._voice_transcriber:
            await self._handle_voice_attachment(turn_context, audio_attachment)
            return

        # Continue existing dialog if any
        results = await dialog_context.continue_dialog()
        if results.status != DialogTurnStatus.Empty:
            return

        # Process new message
        text = turn_context.activity.text
        if not text:
            return

        # Check for group/channel processing
        # Ensure we handle mentions correctly in channels/groups
        conversation_type = turn_context.activity.conversation.conversation_type
        is_group = conversation_type in ('channel', 'groupChat')

        if is_group:
            # Check if mentions are enabled for groups
            if not self.config.enable_group_mentions:
                return

            # Verify bot was mentioned (Teams usually handles this but let's be safe)
            # We check if the bot is in the mentions list
            is_mentioned = False
            if turn_context.activity.entities:
                for entity in turn_context.activity.entities:
                    if entity.type == "mention":
                        mentioned = entity.additional_properties.get("mentioned", {})
                        if mentioned.get("id") == turn_context.activity.recipient.id:
                            is_mentioned = True
                            break
            
            # If not mentioned and it's a group/channel, ignore
            if not is_mentioned:
                return

        # Clean message (remove bot mentions)
        text = self._remove_mentions(turn_context.activity, text)

        # Sanitize text (normalize unicode characters like NBSP from French keyboards)
        text = self._sanitize_text(text)

        self.logger.info(f"Received message: {text}")

        # Send typing indicator
        await self.send_typing(turn_context)

        # Process message with form orchestrator
        # (Orchestrator now handles trigger phrase detection for YAML forms)
        result = await self.form_orchestrator.process_message(
            message=text,
            conversation_id=conversation_id,
            context={
                "user_id": turn_context.activity.from_property.id,
                "session_id": conversation_id,
            }
        )

        # Handle result
        if result.has_error:
            await self.send_text(result.error, turn_context)
            return

        if result.needs_form:
            # Show context message if provided
            if result.context_message:
                await self.send_text(result.context_message, turn_context)

            # Start form dialog
            await self._start_form_dialog(
                dialog_context,
                result.form,
                conversation_id,
                turn_context,  # Pass for explicit state save
            )
            return

        # Normal response - parse and send as Adaptive Card
        if result.raw_response is not None:
            # Parse response to detect Adaptive Cards, tables, code, images
            parsed = self._parse_response(result.raw_response)
            # Debug: Log what was parsed for images/documents
            if not isinstance(parsed, dict):
                self.logger.debug(
                    f"📊 Parsed response - images: {len(parsed.images)}, "
                    f"documents: {len(parsed.documents)}, charts: {len(parsed.charts)}"
                )
                if parsed.documents:
                    for i, doc in enumerate(parsed.documents[:3]):
                        doc_preview = str(doc)[:80] if isinstance(doc, str) else str(doc)
                        self.logger.debug(f"  📄 Document[{i}]: {doc_preview}...")
            await self._send_parsed_response(parsed, turn_context)
        elif result.response_text:
            # Fallback to plain text if no raw response
            await self.send_text(result.response_text, turn_context)

    # async def on_message_activity(self, turn_context: TurnContext):
    #     """
    #     Handle incoming text messages.
    #     """
    #     text = turn_context.activity.text
    #     if not text:
    #         return

    #     # Handle commands if any (simplified)
    #     # remove mentions if any
    #     text = self._remove_mentions(turn_context.activity, text)

    #     self.logger.info(f"Received message: {text}")

    #     # Send typing indicator
    #     await self.send_typing(turn_context)

    #     # Agent processing
    #     try:
    #         # We can use a per-conversation memory if the Agent supports it
    #         # For now, just simplistic call
    #         response = await self.agent.ask(text, output_mode=OutputMode.MSTEAMS)

    #         # Parse response into structured content
    #         parsed = self._parse_response(response)

    #         # Send response as Adaptive Card with attachments
    #         await self._send_parsed_response(parsed, turn_context)

    #     except Exception as e:
    #         self.logger.error(f"Agent error: {e}", exc_info=True)
    #         await self.send_text(
    #             "I encountered an error processing your request.",
    #             turn_context
    #         )

    async def on_members_added_activity(
        self,
        members_added: list[ChannelAccount],
        turn_context: TurnContext
    ):
        """
        Welcome new members.
        """
        for member in members_added:
            if member.id != turn_context.activity.recipient.id:
                if self.config.welcome_message:
                    # Send welcome as a simple card
                    welcome_card = self._build_adaptive_card(
                        ParsedResponse(text=self.config.welcome_message)
                    )
                    try:
                        await self.send_card(welcome_card, turn_context)
                    except Exception as e:
                        self.logger.warning(
                            f"Could not send welcome card to {member.id}: {e}"
                        )

    def _remove_mentions(self, activity: Activity, text: str) -> str:
        """Remove @bot mentions from text."""
        if not text:
            return ""

        # Remove mentions from activity.entities
        if activity.entities:
            for entity in activity.entities:
                if entity.type == "mention":
                    mentioned = entity.additional_properties.get("mentioned", {})
                    if mentioned.get("id") == activity.recipient.id:
                        # Remove the mention text
                        mention_text = entity.additional_properties.get("text", "")
                        text = text.replace(mention_text, "").strip()

        # Fallback: remove bot name at start
        with contextlib.suppress(Exception):
            bot_name = activity.recipient.name
            if bot_name and text.lower().startswith(f"@{bot_name.lower()}"):
                text = text[len(bot_name) + 1:].strip()

        return text.strip()

    def _sanitize_text(self, text: str) -> str:
        """
        Normalize unicode characters that may cause issues with LLM APIs.

        This handles French-specific characters like non-breaking spaces (NBSP)
        that are automatically inserted by French keyboards before punctuation.

        Args:
            text: Input text to sanitize

        Returns:
            Sanitized text with normalized unicode characters
        """
        if not text:
            return ""

        # Replace various non-breaking space variants with regular space
        # U+00A0: No-Break Space (common)
        # U+202F: Narrow No-Break Space (used in French typography)
        # U+2007: Figure Space
        # U+2060: Word Joiner
        replacements = {
            '\u00A0': ' ',   # NBSP
            '\u202F': ' ',   # Narrow NBSP (French)
            '\u2007': ' ',   # Figure Space
            '\u2060': '',    # Word Joiner (remove)
            '\uFEFF': '',    # BOM / Zero Width No-Break Space (remove)
        }

        for char, replacement in replacements.items():
            text = text.replace(char, replacement)

        return text

    async def send_typing(self, turn_context: TurnContext):
        activity = Activity(type=ActivityTypes.typing)
        activity.relates_to = turn_context.activity.conversation
        await turn_context.send_activity(activity)

    # =========================================================================
    # Voice Note Handling
    # =========================================================================

    def _find_audio_attachment(self, activity: Activity) -> Optional[Attachment]:
        """Find first audio attachment in activity.

        Args:
            activity: The activity to check for audio attachments.

        Returns:
            First audio attachment found, or None.
        """
        if not activity.attachments:
            return None

        for attachment in activity.attachments:
            content_type = (attachment.content_type or "").lower()
            if any(ct in content_type for ct in self.AUDIO_CONTENT_TYPES):
                return attachment

        return None

    async def _handle_voice_attachment(
        self,
        turn_context: TurnContext,
        attachment: Attachment,
    ) -> None:
        """Process voice note attachment.

        Downloads the audio, transcribes it, shows transcription to user
        (if configured), and processes the transcribed text through the agent.

        Args:
            turn_context: The turn context.
            attachment: The audio attachment to process.
        """
        conversation_id = turn_context.activity.conversation.id

        # Check for multiple audio attachments and warn
        audio_count = sum(
            1 for a in (turn_context.activity.attachments or [])
            if any(ct in (a.content_type or "").lower() for ct in self.AUDIO_CONTENT_TYPES)
        )
        if audio_count > 1:
            self.logger.warning(
                f"Multiple audio attachments ({audio_count}), processing first only"
            )

        # Send typing indicator
        await self.send_typing(turn_context)

        try:
            # Get bot access token for downloading (if needed)
            token = await self._get_attachment_token(turn_context)

            # Transcribe
            result = await self._voice_transcriber.transcribe_url(
                url=attachment.content_url,
                auth_token=token,
            )

            transcribed_text = result.text.strip()

            if not transcribed_text:
                await self.send_text(
                    "I couldn't understand the audio. Please try again or type your message.",
                    turn_context
                )
                return

            # Show transcription to user (if enabled)
            if self._voice_config.show_transcription:
                await self.send_text(
                    f'🎤 *"{transcribed_text}"*',
                    turn_context
                )

            self.logger.info(
                f"Transcribed voice note ({result.duration_seconds:.1f}s): "
                f"{transcribed_text[:50]}..."
            )

            # Process through normal flow
            await self._process_transcribed_message(
                turn_context,
                transcribed_text,
                conversation_id,
            )

        except ValueError as e:
            # Duration limit exceeded or other validation error
            self.logger.warning(f"Voice note validation error: {e}")
            await self.send_text(
                f"Voice note too long. Please keep it under "
                f"{self._voice_config.max_audio_duration_seconds} seconds.",
                turn_context
            )
        except Exception as e:
            self.logger.error(f"Voice transcription error: {e}", exc_info=True)
            await self.send_text(
                "Sorry, I couldn't process your voice note. Please try typing your message.",
                turn_context
            )

    async def _get_attachment_token(self, turn_context: TurnContext) -> Optional[str]:
        """Get token for downloading attachments from MS Teams CDN.

        MS Teams attachments may need the bot's OAuth token to download.
        For public attachments, this may return None.

        Args:
            turn_context: The turn context.

        Returns:
            OAuth token string, or None if not available.
        """
        try:
            # Try to get the token from adapter's credentials
            # MS Teams may require the bot token for CDN access
            connector_client = turn_context.adapter.connector_client
            if hasattr(connector_client, 'config') and hasattr(connector_client.config, 'credentials'):
                creds = connector_client.config.credentials
                if hasattr(creds, 'get_token'):
                    token_response = await creds.get_token()
                    if hasattr(token_response, 'token'):
                        return token_response.token
            return None
        except Exception as e:
            self.logger.debug(f"Could not get attachment token: {e}")
            return None

    async def _process_transcribed_message(
        self,
        turn_context: TurnContext,
        text: str,
        conversation_id: str,
    ) -> None:
        """Process transcribed text through agent.

        Args:
            turn_context: The turn context.
            text: The transcribed text to process.
            conversation_id: The conversation ID for context.
        """
        # Sanitize transcribed text (normalize unicode characters)
        text = self._sanitize_text(text)

        # Create dialog context for potential form handling
        dialog_context = await self.dialogs.create_context(turn_context)

        # Send typing indicator
        await self.send_typing(turn_context)

        # Process message with form orchestrator (same as text messages)
        result = await self.form_orchestrator.process_message(
            message=text,
            conversation_id=conversation_id,
            context={
                "user_id": turn_context.activity.from_property.id,
                "session_id": conversation_id,
                "source": "voice_note",  # Mark source for analytics
            }
        )

        # Handle result (same as text flow)
        if result.has_error:
            await self.send_text(result.error, turn_context)
            return

        if result.needs_form:
            # Show context message if provided
            if result.context_message:
                await self.send_text(result.context_message, turn_context)

            # Start form dialog
            await self._start_form_dialog(
                dialog_context,
                result.form,
                conversation_id,
                turn_context,
            )
            return

        # Normal response - parse and send as Adaptive Card
        if result.raw_response is not None:
            parsed = self._parse_response(result.raw_response)
            await self._send_parsed_response(parsed, turn_context)
        elif result.response_text:
            await self.send_text(result.response_text, turn_context)

    async def close_voice_transcriber(self) -> None:
        """Close voice transcriber and release resources."""
        if self._voice_transcriber:
            await self._voice_transcriber.close()
            self._voice_transcriber = None

    def _extract_adaptive_card_json(self, text: str) -> Optional[Dict[str, Any]]:
        """
        Extract Adaptive Card JSON from markdown code blocks.

        Looks for:
        - ```json ... ``` blocks containing AdaptiveCard
        - Validates if it's a proper Adaptive Card structure

        Returns:
            Adaptive Card dict if found and valid, None otherwise
        """
        if not text:
            return None

        # Try to find JSON code blocks with triple backticks
        json_pattern = r'```(?:json)?\s*\n(.*?)\n```'
        matches = re.findall(json_pattern, text, re.DOTALL | re.IGNORECASE)

        for match in matches:
            try:
                parsed_json = json.loads(match.strip())

                # Check if it's an Adaptive Card directly
                if isinstance(parsed_json, dict):
                    # Direct AdaptiveCard
                    if parsed_json.get('type') == 'AdaptiveCard':
                        self.logger.info("Detected direct AdaptiveCard in JSON block")
                        return parsed_json

                    # MS Teams message with attachments containing AdaptiveCard
                    if parsed_json.get('type') == 'message':
                        if attachments := parsed_json.get('attachments', []):
                            for attachment in attachments:
                                if isinstance(attachment, dict):
                                    # Check if attachment has contentType for adaptive card
                                    content_type = attachment.get('contentType', '')
                                    if 'adaptivecard' in content_type.lower():
                                        # Return the content of the adaptive card
                                        card_content = attachment.get('content')
                                        if card_content and isinstance(card_content, dict):
                                            self.logger.info("Detected AdaptiveCard in message attachment")
                                            return card_content

                            # If no specific adaptive card content type but has content
                            # Return first attachment's content if it looks like a card
                            first_attachment = attachments[0]
                            if isinstance(first_attachment, dict):
                                content = first_attachment.get('content', first_attachment)
                                if isinstance(content, dict) and content.get('type') == 'AdaptiveCard':
                                    self.logger.info("Detected AdaptiveCard in first attachment")
                                    return content

            except json.JSONDecodeError:
                continue

        return None

    def _parse_response(self, response: Any) -> Union[ParsedResponse, Dict[str, Any]]:
        """
        Parse agent response into structured content.

        For MSTEAMS output mode, checks if the response contains an Adaptive Card JSON.
        If found, returns the Adaptive Card dict directly.
        Otherwise, falls back to standard parse_response().

        Returns:
            Either a ParsedResponse object or an Adaptive Card dict
        """
        # First check if response contains an Adaptive Card JSON
        text_to_check = None

        if hasattr(response, 'output') and response.output:
            text_to_check = str(response.output)
        elif hasattr(response, 'content') and response.content:
            text_to_check = str(response.content)
        elif hasattr(response, 'response') and response.response:
            text_to_check = str(response.response)

        if text_to_check:
            adaptive_card = self._extract_adaptive_card_json(text_to_check)
            if adaptive_card:
                # Return the adaptive card directly as a dict marker
                # We'll handle this specially in _send_parsed_response
                return adaptive_card

        # Fall back to standard parsing
        return parse_response(response)

    def _extract_response_text(self, response: Any) -> str:
        """Extract text content from agent response (backward compatibility)."""
        parsed = self._parse_response(response)
        if isinstance(parsed, dict):
            # It's an Adaptive Card, return empty string
            return ""
        return parsed.text

    def _build_adaptive_card(self, parsed: ParsedResponse) -> Dict[str, Any]:
        """
        Build an Adaptive Card from parsed response.

        Features:
        - Text with markdown support (TextBlock wrap)
        - Code blocks with monospace font
        - Tables as FactSet or ColumnSet
        - Images inline in card

        Args:
            parsed: The parsed response content

        Returns:
            Adaptive Card JSON structure
        """
        card_body = []

        # Add main text content
        # Add main text content
        if parsed.text:
            # Parse for markdown tables
            content_elements = self._render_markdown_content(parsed.text)
            card_body.extend(content_elements)

        # Add code block if present
        if parsed.has_code:
            # Add separator
            card_body.append({
                "type": "TextBlock",
                "text": f"**Code** ({parsed.code_language or 'text'}):",
                "wrap": True,
                "weight": "Bolder",
                "spacing": "Medium"
            })

            # Code in monospace TextBlock
            card_body.append({
                "type": "TextBlock",
                "text": parsed.code,
                "wrap": True,
                "fontType": "Monospace",
                "spacing": "Small"
            })

        # Add table if present
        if parsed.has_table and parsed.table_data is not None:
            try:
                df = parsed.table_data
                columns = list(df.columns)

                # Create header row
                header_columns = [
                    {
                        "type": "Column",
                        "width": "stretch",
                        "items": [{
                            "type": "TextBlock",
                            "text": str(col),
                            "weight": "Bolder",
                            "wrap": True
                        }]
                    }
                    for col in columns
                ]

                card_body.append({
                    "type": "ColumnSet",
                    "columns": header_columns,
                    "spacing": "Medium"
                })

                # Add data rows (limit to 20 for card size)
                for idx, (_, row) in enumerate(df.head(20).iterrows()):
                    row_columns = [
                        {
                            "type": "Column",
                            "width": "stretch",
                            "items": [{
                                "type": "TextBlock",
                                "text": str(val),
                                "wrap": True
                            }]
                        }
                        for val in row.values
                    ]
                    card_body.append({
                        "type": "ColumnSet",
                        "columns": row_columns,
                        "separator": idx == 0
                    })

                if len(df) > 20:
                    card_body.append({
                        "type": "TextBlock",
                        "text": f"*... and {len(df) - 20} more rows*",
                        "wrap": True,
                        "isSubtle": True
                    })

            except Exception as e:
                # Fallback to markdown table
                if parsed.table_markdown:
                    card_body.append({
                        "type": "TextBlock",
                        "text": parsed.table_markdown,
                        "wrap": True,
                        "fontType": "Monospace"
                    })
        elif parsed.table_markdown:
            # If only markdown table available
            card_body.append({
                "type": "TextBlock",
                "text": parsed.table_markdown,
                "wrap": True,
                "fontType": "Monospace"
            })



        # Add charts as images inline
        if hasattr(parsed, 'charts') and parsed.charts:
            for chart in parsed.charts:
                try:
                    # Add chart title
                    card_body.append({
                        "type": "TextBlock",
                        "text": f"📊 {chart.title}",
                        "weight": "Bolder",
                        "spacing": "Medium",
                        "size": "Medium"
                    })
                    
                    # Convert chart to base64 data URI
                    data_uri = self._chart_to_data_uri(chart)
                    
                    # Add image element
                    card_body.append({
                        "type": "Image",
                        "url": data_uri,
                        "size": "Large",
                        "horizontalAlignment": "Center",
                        "spacing": "Small",
                        "altText": f"Chart: {chart.title}"
                    })
                    
                    # Add chart type info if available
                    if chart.chart_type and chart.chart_type != "unknown":
                        card_body.append({
                            "type": "TextBlock",
                            "text": f"*{chart.chart_type.replace('_', ' ').title()} Chart*",
                            "isSubtle": True,
                            "size": "Small",
                            "horizontalAlignment": "Center"
                        })
                    
                    self.logger.info(f"Added chart to Adaptive Card: {chart.title}")
                    
                except Exception as e:
                    self.logger.error(f"Failed to embed chart '{chart.title}': {e}")
                    # Add error placeholder
                    card_body.append({
                        "type": "TextBlock",
                        "text": f"⚠️ Chart '{chart.title}' could not be displayed",
                        "color": "Warning",
                        "wrap": True
                    })

        # Add images inline - handle URL images directly
        media_added = False
        for image_path in parsed.images[:3]:  # Limit to 3 images in card
            image_str = str(image_path) if hasattr(image_path, '__str__') else image_path
            self.logger.debug(f"🖼️ Processing image: {image_str[:80] if isinstance(image_str, str) else image_str}")
            # Check if it's a URL (http/https) - can be displayed directly
            if isinstance(image_str, str) and image_str.startswith(('http://', 'https://')):
                if not media_added:
                    card_body.append({
                        "type": "TextBlock",
                        "text": "---",
                        "spacing": "Medium"
                    })
                    media_added = True
                card_body.append({
                    "type": "Image",
                    "url": image_str,
                    "size": "Large",
                    "horizontalAlignment": "Center",
                    "spacing": "Medium",
                    "altText": "Generated Image"
                })
                self.logger.info(f"✅ Added URL image to card")
            elif hasattr(image_path, 'name'):
                # Local file path - show as text placeholder
                card_body.append({
                    "type": "TextBlock",
                    "text": f"📷 Image: {image_path.name}",
                    "wrap": True,
                    "isSubtle": True
                })

        # Add document mentions - handle base64 data URIs as inline images
        for doc in parsed.documents[:5]:
            doc_str = str(doc) if hasattr(doc, '__str__') else doc
            self.logger.debug(f"📄 Processing document: {doc_str[:80] if isinstance(doc_str, str) else 'Path object'}...")
            # Check if it's a base64 data URI (image)
            if isinstance(doc_str, str) and doc_str.startswith('data:image/'):
                if not media_added:
                    card_body.append({
                        "type": "TextBlock",
                        "text": "---",
                        "spacing": "Medium"
                    })
                    media_added = True
                card_body.append({
                    "type": "Image",
                    "url": doc_str,
                    "size": "Large",
                    "horizontalAlignment": "Center",
                    "spacing": "Medium",
                    "altText": "Generated Chart"
                })
                self.logger.info(f"✅ Added base64 image to card (length: {len(doc_str)} chars)")
            elif hasattr(doc, 'name'):
                # It's a Path object - show as document mention
                card_body.append({
                    "type": "TextBlock",
                    "text": f"📎 Document: {doc.name}",
                    "wrap": True,
                    "isSubtle": True
                })
            elif isinstance(doc_str, str) and not doc_str.startswith('data:'):
                # Unknown string document
                card_body.append({
                    "type": "TextBlock",
                    "text": f"📎 Document: {doc_str[:50]}...",
                    "wrap": True,
                    "isSubtle": True
                })

        # Build the card
        adaptive_card = {
            "type": "AdaptiveCard",
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.4",
            "body": card_body
        }

        return adaptive_card

    def _render_markdown_content(self, text: str) -> list[dict[str, Any]]:
        """
        Parse markdown text and convert tables to allowed Adaptive Card elements.
        Splits text into TextBlocks and ColumnSets for tables.
        """
        elements = []
        if not text:
            return elements

        # Regex to detect a markdown table row (starts and ends with pipe, at least one internal pipe)
        # We use a lookahead to ensure we don't match simple pipes in text unless they look like table rows
        table_row_pattern = re.compile(r'(?m)^.*\|.*\|.*$')

        lines = text.split('\n')
        current_text = []
        table_lines = []

        def flush_text():
            if current_text:
                txt = '\n'.join(current_text).strip()
                if txt:
                   # Check if the last part of the text looks like a started table on the same line
                   # e.g. "Here is the data: | Col1 | Col2 |"
                   last_line_match = re.search(r'(\|.*\|.*\|)', txt)
                   if last_line_match and len(txt) > len(last_line_match.group(1)) + 5:
                       # Split if meaningful text precedes the table part
                       pre_text = txt[:last_line_match.start()].strip()
                       table_part = txt[last_line_match.start():]
                       if pre_text:
                           elements.append({
                               "type": "TextBlock",
                               "text": pre_text,
                               "wrap": True
                           })
                       # Treat the table part as the first line of a potential table
                       table_lines.append(table_part)
                   else:
                       elements.append({
                           "type": "TextBlock",
                           "text": txt,
                           "wrap": True
                       })
                current_text.clear()

        def flush_table():
            if table_lines:
                # Need at least header and separator
                if len(table_lines) >= 2:
                     # Basic validation: second line should mimic separator |---|
                     if set(table_lines[1].strip()) <= {'|', '-', ':', ' '}:
                         table_element = self._markdown_table_to_adaptive(table_lines)
                         if table_element:
                             elements.append(table_element)
                         else:
                             # Fallback to monospace text
                             elements.append({
                                 "type": "TextBlock",
                                 "text": '\n'.join(table_lines),
                                 "wrap": True,
                                 "fontType": "Monospace"
                             })
                     else:
                        # Invalid table structure, revert to text
                        elements.append({
                            "type": "TextBlock",
                            "text": '\n'.join(table_lines),
                            "wrap": True
                        })
                else:
                     elements.append({
                         "type": "TextBlock",
                         "text": '\n'.join(table_lines),
                         "wrap": True
                     })
                table_lines.clear()

        for line in lines:
            stripped = line.strip()
            # Check if line looks like a table row
            if stripped.startswith('|') and stripped.endswith('|'):
                flush_text()
                table_lines.append(stripped)
            else:
                # Handle case where table is inline at end of text: "... text | Col | Col |"
                # This is tricky because "Text | Pipe | Text" is valid text.
                # Only assume it's a table start if subsequent lines confirm it (separator)
                if table_lines:
                    # Previous block was table, this line is NOT table -> flush table
                    flush_table()
                
                # Check for inline table start
                if '|' in stripped and stripped.endswith('|') and len(stripped.split('|')) > 2:
                     # Check if it looks like a header row followed by separator in next line?
                     # We can't know next line here easily without lookahead.
                     # But we can try to split:
                     match = re.search(r'(\|.*\|.*\|)$', stripped)
                     if match:
                         # Found a table-like structure at end of line.
                         # Defer decision? No, let flush_text handle the splitting logic
                         pass
                
                current_text.append(line)
        
        flush_table()
        flush_text()
        
        return elements

    def _markdown_table_to_adaptive(self, lines: list[str]) -> Optional[dict[str, Any]]:
        """Convert markdown table lines to Adaptive Card ColumnSet."""
        try:
            # Parse header
            header_line = lines[0].strip()[1:-1] # Remove leading/trailing |
            headers = [h.strip() for h in header_line.split('|')]
            
            # Parse rows (skip separator at index 1)
            rows = []
            for line in lines[2:]:
                row_line = line.strip()[1:-1]
                vals = [v.strip() for v in row_line.split('|')]
                # Pad with empty str if row is shorter
                if len(vals) < len(headers):
                    vals.extend([''] * (len(headers) - len(vals)))
                rows.append(vals[:len(headers)])

            container_items = []

            # Create Header Row
            header_columns = []
            for h in headers:
                header_columns.append({
                    "type": "Column",
                    "width": "stretch",
                    "items": [{
                        "type": "TextBlock",
                        "text": h,
                        "weight": "Bolder",
                        "wrap": True
                    }]
                })
            container_items.append({
                "type": "ColumnSet",
                "columns": header_columns,
                "spacing": "Medium"
            })

            # Create Data Rows
            for i, row in enumerate(rows):
                # Limit to 15 rows to prevent card overflow
                if i >= 15:
                    container_items.append({
                        "type": "TextBlock",
                        "text": f"*... {len(rows)-15} more rows ...*",
                        "isSubtle": True,
                        "spacing": "Small"
                    })
                    break

                row_columns = []
                for val in row:
                    row_columns.append({
                        "type": "Column",
                        "width": "stretch",
                        "items": [{
                            "type": "TextBlock",
                            "text": val,
                            "wrap": True
                        }]
                    })
                
                container_items.append({
                    "type": "ColumnSet",
                    "columns": row_columns,
                    "separator": True if i == 0 else False,
                    "spacing": "Small"
                })

            return {
                "type": "Container",
                "items": container_items,
                "spacing": "Medium"
            }
        except Exception as e:
            self.logger.warning(f"Error parsing markdown table: {e}")
            return None

    async def _send_parsed_response(
        self,
        parsed: Union[ParsedResponse, Dict[str, Any]],
        turn_context: TurnContext
    ) -> None:
        """
        Send parsed response to MS Teams.

        Handles both:
        - ParsedResponse: Sends an Adaptive Card built from parsed content
        - Dict (Adaptive Card): Sends the Adaptive Card directly

        Sends separate attachments for files if needed.

        Args:
            parsed: Either ParsedResponse or Adaptive Card dict
            turn_context: The turn context for sending
        """
        # Check if parsed is an Adaptive Card dict
        if isinstance(parsed, dict):
            self.logger.info("Sending Adaptive Card directly from LLM response")
            await self.send_card(parsed, turn_context)
            return

        # Standard ParsedResponse handling
        # Build and send Adaptive Card for main content
        if parsed.text or parsed.has_code or parsed.has_table:
            card = self._build_adaptive_card(parsed)
            await self.send_card(card, turn_context)

        # Send document attachments
        for doc_path in parsed.documents:
            try:
                await self.send_file_attachment(doc_path, turn_context)
            except Exception as e:
                self.logger.error(f"Failed to send document {doc_path}: {e}")

        # Send media attachments
        for media_path in parsed.media:
            try:
                await self.send_file_attachment(media_path, turn_context)
            except Exception as e:
                self.logger.error(f"Failed to send media {media_path}: {e}")
