# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Microsoft Teams Channel - Phase 3

Bot Framework integration for Microsoft Teams.

Features:
- Direct messages to bot
- @mention in team channels
- Adaptive Cards for rich responses
- SSO pass-through with Entra ID
- Account linking (like Telegram/Discord)

Setup:
1. Register bot in Azure Portal (Bot Services)
2. Create App Registration for SSO
3. Create Teams App manifest
4. Deploy to Teams App Catalog

Configuration:
    channels:
      teams:
        enabled: true
        app_id: ${TEAMS_APP_ID}
        app_password: ${TEAMS_APP_PASSWORD}
        tenant_id: ${TEAMS_TENANT_ID}  # or "common" for multi-tenant

Dependencies:
    pip install botbuilder-core botbuilder-schema aiohttp
"""

from __future__ import annotations

import asyncio
import logging
import os
from dataclasses import dataclass, field
from typing import Dict, List, Optional

logger = logging.getLogger(__name__)

# Optional dependencies
try:
    from aiohttp import web
    from botbuilder.core import (
        ActivityHandler,
        BotFrameworkAdapter,
        BotFrameworkAdapterSettings,
        CardFactory,
        MessageFactory,
        TurnContext,
    )
    from botbuilder.schema import (
        Activity,
        ActivityTypes,
        ConversationReference,
    )

    HAS_BOTFRAMEWORK = True
except ImportError:
    HAS_BOTFRAMEWORK = False
    logger.warning("Bot Framework not installed.")

# Import channel auth
try:
    from .auth import (
        ChannelAuthenticator,
        ChannelType,
        ChannelUser,
        create_authenticator,
    )

    HAS_AUTH = True
except ImportError:
    HAS_AUTH = False


@dataclass
class TeamsConfig:
    """Teams channel configuration."""

    app_id: str = ""
    app_password: str = ""
    tenant_id: str = "common"  # "common" for multi-tenant

    # Bot settings
    bot_name: str = "Familiar"
    welcome_message: str = "👋 Hi! I'm Familiar, your AI assistant. How can I help?"

    # Features
    use_adaptive_cards: bool = True
    enable_sso: bool = True
    enable_typing_indicator: bool = True

    # Restrictions
    allowed_tenants: List[str] = field(default_factory=list)  # Empty = allow all

    @classmethod
    def from_env(cls) -> "TeamsConfig":
        """Load config from environment."""
        return cls(
            app_id=os.environ.get("TEAMS_APP_ID", ""),
            app_password=os.environ.get("TEAMS_APP_PASSWORD", ""),
            tenant_id=os.environ.get("TEAMS_TENANT_ID", "common"),
            bot_name=os.environ.get("TEAMS_BOT_NAME", "Familiar"),
        )

    @classmethod
    def from_dict(cls, d: Dict) -> "TeamsConfig":
        """Load from config dict."""
        return cls(
            app_id=d.get("app_id", os.environ.get("TEAMS_APP_ID", "")),
            app_password=d.get("app_password", os.environ.get("TEAMS_APP_PASSWORD", "")),
            tenant_id=d.get("tenant_id", os.environ.get("TEAMS_TENANT_ID", "common")),
            bot_name=d.get("bot_name", "Familiar"),
            welcome_message=d.get("welcome_message", "👋 Hi! I'm Familiar. How can I help?"),
            use_adaptive_cards=d.get("use_adaptive_cards", True),
            enable_sso=d.get("enable_sso", True),
            allowed_tenants=d.get("allowed_tenants", []),
        )


# ============================================================
# ADAPTIVE CARDS
# ============================================================


def create_text_card(text: str, title: str = None) -> Dict:
    """Create a simple text Adaptive Card."""
    body = []

    if title:
        body.append(
            {
                "type": "TextBlock",
                "text": title,
                "weight": "Bolder",
                "size": "Medium",
            }
        )

    body.append(
        {
            "type": "TextBlock",
            "text": text,
            "wrap": True,
        }
    )

    return {
        "type": "AdaptiveCard",
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.4",
        "body": body,
    }


def create_task_card(
    title: str,
    description: str = None,
    due_date: str = None,
    priority: str = None,
    task_id: str = None,
) -> Dict:
    """Create an Adaptive Card for a task."""
    body = [
        {
            "type": "TextBlock",
            "text": f"📋 {title}",
            "weight": "Bolder",
            "size": "Medium",
        }
    ]

    if description:
        body.append(
            {
                "type": "TextBlock",
                "text": description,
                "wrap": True,
            }
        )

    facts = []
    if due_date:
        facts.append({"title": "Due", "value": due_date})
    if priority:
        facts.append({"title": "Priority", "value": priority})

    if facts:
        body.append(
            {
                "type": "FactSet",
                "facts": facts,
            }
        )

    # Action buttons
    actions = []
    if task_id:
        actions.append(
            {
                "type": "Action.Submit",
                "title": "✅ Complete",
                "data": {"action": "complete_task", "task_id": task_id},
            }
        )
        actions.append(
            {
                "type": "Action.Submit",
                "title": "📝 Edit",
                "data": {"action": "edit_task", "task_id": task_id},
            }
        )

    return {
        "type": "AdaptiveCard",
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.4",
        "body": body,
        "actions": actions,
    }


def create_calendar_card(
    title: str, start_time: str, end_time: str = None, location: str = None, event_id: str = None
) -> Dict:
    """Create an Adaptive Card for a calendar event."""
    body = [
        {
            "type": "TextBlock",
            "text": f"📅 {title}",
            "weight": "Bolder",
            "size": "Medium",
        }
    ]

    facts = [{"title": "When", "value": start_time}]
    if end_time:
        facts.append({"title": "Until", "value": end_time})
    if location:
        facts.append({"title": "Where", "value": location})

    body.append(
        {
            "type": "FactSet",
            "facts": facts,
        }
    )

    actions = []
    if event_id:
        actions.append(
            {
                "type": "Action.Submit",
                "title": "✅ Accept",
                "data": {"action": "accept_event", "event_id": event_id},
            }
        )
        actions.append(
            {
                "type": "Action.Submit",
                "title": "❌ Decline",
                "data": {"action": "decline_event", "event_id": event_id},
            }
        )

    return {
        "type": "AdaptiveCard",
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.4",
        "body": body,
        "actions": actions,
    }


def create_link_account_card(code: str = None) -> Dict:
    """Create card for account linking flow."""
    if code:
        # Show verification code
        return {
            "type": "AdaptiveCard",
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.4",
            "body": [
                {
                    "type": "TextBlock",
                    "text": "🔗 Link Your Account",
                    "weight": "Bolder",
                    "size": "Medium",
                },
                {
                    "type": "TextBlock",
                    "text": "Enter this code in your email:",
                    "wrap": True,
                },
                {
                    "type": "TextBlock",
                    "text": code,
                    "weight": "Bolder",
                    "size": "ExtraLarge",
                    "horizontalAlignment": "Center",
                    "fontType": "Monospace",
                },
                {
                    "type": "TextBlock",
                    "text": "Code expires in 10 minutes",
                    "size": "Small",
                    "isSubtle": True,
                },
            ],
        }
    else:
        # Prompt for email
        return {
            "type": "AdaptiveCard",
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.4",
            "body": [
                {
                    "type": "TextBlock",
                    "text": "🔗 Link Your Account",
                    "weight": "Bolder",
                    "size": "Medium",
                },
                {
                    "type": "TextBlock",
                    "text": "Enter your organization email to link your Familiar account:",
                    "wrap": True,
                },
                {
                    "type": "Input.Text",
                    "id": "email",
                    "placeholder": "you@organization.org",
                    "style": "Email",
                },
            ],
            "actions": [
                {
                    "type": "Action.Submit",
                    "title": "Send Verification Code",
                    "data": {"action": "start_link"},
                }
            ],
        }


def create_whoami_card(name: str, email: str, role: str, is_linked: bool, teams_id: str) -> Dict:
    """Create card showing user info."""
    status = "✅ Linked" if is_linked else "❌ Not Linked"

    return {
        "type": "AdaptiveCard",
        "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
        "version": "1.4",
        "body": [
            {
                "type": "TextBlock",
                "text": "👤 Your Account",
                "weight": "Bolder",
                "size": "Medium",
            },
            {
                "type": "FactSet",
                "facts": [
                    {"title": "Name", "value": name},
                    {"title": "Email", "value": email},
                    {"title": "Role", "value": role},
                    {"title": "Status", "value": status},
                    {"title": "Teams ID", "value": teams_id[:16] + "..."},
                ],
            },
        ],
    }


# ============================================================
# TEAMS BOT
# ============================================================


try:
    from .connect_wizard import ConnectMixin
    from .formatting import FormatMode

    HAS_CONNECT = True
except ImportError:
    HAS_CONNECT = False


_teams_bases = []
if HAS_CONNECT:
    _teams_bases.append(ConnectMixin)
if HAS_BOTFRAMEWORK:
    _teams_bases.append(ActivityHandler)
if not _teams_bases:
    _teams_bases.append(object)


class FamiliarTeamsBot(*_teams_bases):
    """
    Microsoft Teams bot for Familiar.

    Handles:
    - Direct messages
    - Channel @mentions
    - Adaptive Card actions
    - Account linking
    - Service connection wizards (via ConnectMixin)
    """

    # ConnectMixin adapter properties
    if HAS_CONNECT:
        format_mode = FormatMode.MARKDOWN
        supports_buttons = True
        supports_message_deletion = False

    def __init__(
        self, agent, config: TeamsConfig = None, authenticator: ChannelAuthenticator = None
    ):
        if HAS_BOTFRAMEWORK:
            super().__init__()

        self.agent = agent
        self.config = config or TeamsConfig.from_env()

        # Channel authentication
        if authenticator:
            self.authenticator = authenticator
        elif HAS_AUTH:
            self.authenticator = create_authenticator()
        else:
            self.authenticator = None

        # Store conversation references for proactive messaging
        self._conversation_references: Dict[str, ConversationReference] = {}

        # ConnectMixin: store Teams turn contexts for wizard routing
        self._wizard_contexts: Dict[str, "TurnContext"] = {}

    # ── ConnectMixin adapter methods ────────────────────────────────

    async def wizard_send(self, recipient_id: str, text: str) -> None:
        ctx = self._wizard_contexts.get(recipient_id)
        if ctx and HAS_BOTFRAMEWORK:
            await ctx.send_activity(MessageFactory.text(text))

    async def wizard_send_menu(
        self, recipient_id: str, text: str, options: list[tuple[str, str]]
    ) -> None:
        ctx = self._wizard_contexts.get(recipient_id)
        if not ctx or not HAS_BOTFRAMEWORK:
            return

        # Build an Adaptive Card with Action.Submit buttons
        actions = []
        for key, label in options[:6]:
            actions.append(
                {
                    "type": "Action.Submit",
                    "title": label[:40],
                    "data": {"action": f"connect:{key}"},
                }
            )

        card = {
            "type": "AdaptiveCard",
            "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
            "version": "1.3",
            "body": [{"type": "TextBlock", "text": text, "wrap": True}],
            "actions": actions,
        }

        attachment = CardFactory.adaptive_card(card)
        await ctx.send_activity(MessageFactory.attachment(attachment))

    async def wizard_delete_message(self, recipient_id: str, message_ref) -> bool:
        return False  # Teams cannot delete user messages

    def _get_user_id(self, turn_context: "TurnContext") -> str:
        """Get unique user ID from Teams context."""
        return turn_context.activity.from_property.id

    def _get_channel_user(self, turn_context: "TurnContext") -> Optional["ChannelUser"]:
        """Authenticate user via ChannelAuthenticator."""
        if not self.authenticator:
            return None

        user_id = self._get_user_id(turn_context)
        display_name = turn_context.activity.from_property.name or "Teams User"

        return self.authenticator.authenticate(
            channel_type=ChannelType.TEAMS if hasattr(ChannelType, "TEAMS") else "teams",
            channel_id=user_id,
            display_name=display_name,
        )

    async def on_message_activity(self, turn_context: "TurnContext"):
        """Handle incoming messages."""
        # Store conversation reference for proactive messaging
        self._save_conversation_reference(turn_context.activity)

        # Get message text
        text = turn_context.activity.text or ""

        # Remove bot mention from channel messages
        text = self._remove_mention(turn_context, text)

        # Check for commands
        text_lower = text.lower().strip()

        if text_lower in ["hi", "hello", "hey", "start"]:
            await self._send_welcome(turn_context)
            return

        if text_lower == "link":
            await self._handle_link_start(turn_context)
            return

        if text_lower.startswith("confirm "):
            code = text_lower.replace("confirm ", "").strip()
            await self._handle_link_confirm(turn_context, code)
            return

        if text_lower == "unlink":
            await self._handle_unlink(turn_context)
            return

        if text_lower in ["whoami", "who am i", "me"]:
            await self._handle_whoami(turn_context)
            return

        if text_lower == "help":
            await self._send_help(turn_context)
            return

        if text_lower in ("trust", "/trust"):
            await self._handle_trust(turn_context)
            return

        if text_lower in ("budget", "/budget"):
            await self._handle_budget(turn_context)
            return

        if text_lower in ("caps", "capabilities", "/caps"):
            await self._handle_caps(turn_context)
            return

        if text_lower in ("status", "/status"):
            await self._handle_status(turn_context)
            return

        if text_lower.startswith("model") or text_lower.startswith("/model"):
            arg = text.split(maxsplit=1)[1] if " " in text else ""
            await self._handle_model(turn_context, arg)
            return

        if text_lower.startswith("remember ") or text_lower.startswith("/remember "):
            rest = text.split(maxsplit=1)[1] if " " in text else ""
            parts = rest.split(maxsplit=1)
            if len(parts) >= 2:
                await self._handle_remember(turn_context, parts[0], parts[1])
            else:
                await turn_context.send_activity(
                    MessageFactory.text("Usage: remember <key> <value>")
                )
            return

        if text_lower.startswith("recall ") or text_lower.startswith("/recall "):
            query = text.split(maxsplit=1)[1] if " " in text else ""
            await self._handle_recall(turn_context, query)
            return

        if text_lower in ("clear", "/clear"):
            user_id = self._get_user_id(turn_context)
            self.agent.clear_history(user_id, "teams")
            await turn_context.send_activity(MessageFactory.text("🧹 Conversation cleared!"))
            return

        if text_lower.startswith("connect") or text_lower.startswith("/connect"):
            user_id = self._get_user_id(turn_context)
            self._wizard_contexts[user_id] = turn_context
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(user_id, "teams")
                if session.trust_level != TrustLevel.OWNER:
                    await turn_context.send_activity(
                        MessageFactory.text("🔒 /connect requires OWNER trust level.")
                    )
                    return
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
            rest = text.split(maxsplit=1)[1] if " " in text else ""
            args = rest.split() if rest else []
            await self.handle_connect_command(user_id, args)
            return

        if text_lower.startswith("browse") or text_lower.startswith("/browse"):
            user_id = self._get_user_id(turn_context)
            self._wizard_contexts[user_id] = turn_context
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(user_id, "teams")
                if session.trust_level != TrustLevel.OWNER:
                    await turn_context.send_activity(
                        MessageFactory.text("\U0001f512 /browse requires OWNER trust level.")
                    )
                    return
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
            await self.handle_browse_command(user_id)
            return

        if text_lower.startswith("search") or text_lower.startswith("/search"):
            user_id = self._get_user_id(turn_context)
            self._wizard_contexts[user_id] = turn_context
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(user_id, "teams")
                if session.trust_level != TrustLevel.OWNER:
                    await turn_context.send_activity(
                        MessageFactory.text("\U0001f512 /search requires OWNER trust level.")
                    )
                    return
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
            rest = text.split(maxsplit=1)[1] if " " in text else ""
            args = rest.split() if rest else []
            await self.handle_search_command(user_id, args)
            return

        # Intercept messages during wizard flows (ConnectMixin)
        user_id = self._get_user_id(turn_context)
        self._wizard_contexts[user_id] = turn_context
        if await self.handle_wizard_message(user_id, text):
            return

        # Process through agent
        await self._process_message(turn_context, text)

    async def on_members_added_activity(self, members_added, turn_context: "TurnContext"):
        """Handle new members (including bot) added to conversation."""
        for member in members_added:
            if member.id != turn_context.activity.recipient.id:
                # New user added - send welcome
                await self._send_welcome(turn_context)

    async def on_adaptive_card_invoke(self, turn_context: "TurnContext", invoke_value: Dict):
        """Handle Adaptive Card action submissions."""
        action = invoke_value.get("action", "")

        if action == "start_link":
            email = invoke_value.get("email", "")
            if email:
                await self._handle_link_with_email(turn_context, email)
            return {"statusCode": 200}

        if action == "complete_task":
            task_id = invoke_value.get("task_id")
            await self._handle_complete_task(turn_context, task_id)
            return {"statusCode": 200}

        if action == "accept_event":
            event_id = invoke_value.get("event_id")
            await self._handle_accept_event(turn_context, event_id)
            return {"statusCode": 200}

        # ConnectMixin: route connect:* button presses to wizard
        if action.startswith("connect:") and HAS_CONNECT:
            user_id = self._get_user_id(turn_context)
            self._wizard_contexts[user_id] = turn_context
            key = action[len("connect:") :]
            await self.handle_connect_menu_selection(user_id, key)
            return {"statusCode": 200}

        return {"statusCode": 200}

    def _remove_mention(self, turn_context: "TurnContext", text: str) -> str:
        """Remove @mention from message text."""
        if not turn_context.activity.entities:
            return text

        for entity in turn_context.activity.entities:
            if entity.type == "mention":
                mention_text = entity.additional_properties.get("text", "")
                text = text.replace(mention_text, "").strip()

        return text

    def _save_conversation_reference(self, activity: "Activity"):
        """Store conversation reference for proactive messaging."""
        if HAS_BOTFRAMEWORK:
            ref = TurnContext.get_conversation_reference(activity)
            self._conversation_references[activity.from_property.id] = ref

    async def _send_welcome(self, turn_context: "TurnContext"):
        """Send welcome message."""
        channel_user = self._get_channel_user(turn_context)

        if channel_user and channel_user.is_linked:
            text = (
                f"👋 Welcome back, **{channel_user.display_name}**!\n\n"
                f"Your account is linked. How can I help?"
            )
        else:
            text = (
                f"{self.config.welcome_message}\n\n"
                f"💡 **Tip:** Use `link` to connect your organization account for full features."
            )

        await turn_context.send_activity(MessageFactory.text(text))

    async def _send_help(self, turn_context: "TurnContext"):
        """Send help message."""
        help_text = """
**🐍 Familiar Commands**

• **link** - Connect your organization account
• **confirm [code]** - Complete account linking
• **unlink** - Disconnect your account
• **whoami** - Show your account info
• **connect** - Connect services (email, calendar, LLM, etc.)
• **help** - Show this message

Just type a message to chat with me!
        """
        await turn_context.send_activity(MessageFactory.text(help_text.strip()))

    async def _handle_link_start(self, turn_context: "TurnContext"):
        """Start account linking - show email input card."""
        if self.config.use_adaptive_cards:
            card = create_link_account_card()
            attachment = CardFactory.adaptive_card(card)
            await turn_context.send_activity(MessageFactory.attachment(attachment))
        else:
            await turn_context.send_activity(
                MessageFactory.text("To link your account, type: `link your@email.org`")
            )

    async def _handle_link_with_email(self, turn_context: "TurnContext", email: str):
        """Start link with provided email."""
        if not self.authenticator:
            await turn_context.send_activity(MessageFactory.text("Account linking not available."))
            return

        user_id = self._get_user_id(turn_context)

        # Check if already linked
        channel_user = self._get_channel_user(turn_context)
        if channel_user and channel_user.is_linked:
            await turn_context.send_activity(
                MessageFactory.text(f"Already linked to {channel_user.user.email}")
            )
            return

        # Start link
        success, message = self.authenticator.start_link(
            channel_type=ChannelType.TEAMS if hasattr(ChannelType, "TEAMS") else "teams",
            channel_id=user_id,
            email=email,
        )

        if success:
            # Extract code from message for card display
            code = None
            if "dev mode:" in message:
                code = message.split("dev mode: ")[-1].rstrip(")")

            if self.config.use_adaptive_cards and code:
                card = create_link_account_card(code)
                attachment = CardFactory.adaptive_card(card)
                await turn_context.send_activity(MessageFactory.attachment(attachment))
            else:
                await turn_context.send_activity(MessageFactory.text(message))
        else:
            await turn_context.send_activity(MessageFactory.text(f"❌ {message}"))

    async def _handle_link_confirm(self, turn_context: "TurnContext", code: str):
        """Confirm account link with code."""
        if not self.authenticator:
            await turn_context.send_activity(MessageFactory.text("Account linking not available."))
            return

        user_id = self._get_user_id(turn_context)

        success, message = self.authenticator.confirm_link(
            channel_type=ChannelType.TEAMS if hasattr(ChannelType, "TEAMS") else "teams",
            channel_id=user_id,
            code=code,
        )

        if success:
            await turn_context.send_activity(MessageFactory.text(f"✅ {message}"))
        else:
            await turn_context.send_activity(MessageFactory.text(f"❌ {message}"))

    async def _handle_unlink(self, turn_context: "TurnContext"):
        """Unlink account."""
        if not self.authenticator:
            await turn_context.send_activity(MessageFactory.text("Account linking not available."))
            return

        user_id = self._get_user_id(turn_context)

        success, message = self.authenticator.unlink(
            channel_type=ChannelType.TEAMS if hasattr(ChannelType, "TEAMS") else "teams",
            channel_id=user_id,
        )

        await turn_context.send_activity(MessageFactory.text(message))

    async def _handle_whoami(self, turn_context: "TurnContext"):
        """Show user info."""
        channel_user = self._get_channel_user(turn_context)
        user_id = self._get_user_id(turn_context)

        if channel_user and channel_user.is_linked:
            if self.config.use_adaptive_cards:
                card = create_whoami_card(
                    name=channel_user.display_name,
                    email=channel_user.user.email,
                    role=channel_user.role.value if channel_user.role else "staff",
                    is_linked=True,
                    teams_id=user_id,
                )
                attachment = CardFactory.adaptive_card(card)
                await turn_context.send_activity(MessageFactory.attachment(attachment))
            else:
                await turn_context.send_activity(
                    MessageFactory.text(
                        f"**{channel_user.display_name}**\n"
                        f"Email: {channel_user.user.email}\n"
                        f"Role: {channel_user.role.value if channel_user.role else 'staff'}\n"
                        f"Linked: ✅"
                    )
                )
        else:
            await turn_context.send_activity(
                MessageFactory.text(
                    f"**Guest**\n"
                    f"Teams ID: {user_id[:16]}...\n"
                    f"Linked: ❌\n\n"
                    f"Use `link` to connect your account."
                )
            )

    async def _handle_complete_task(self, turn_context: "TurnContext", task_id: str):
        """Handle task completion from card action."""
        await turn_context.send_activity(MessageFactory.text("✅ Task marked complete"))

    async def _handle_accept_event(self, turn_context: "TurnContext", event_id: str):
        """Handle event acceptance from card action."""
        await turn_context.send_activity(MessageFactory.text("✅ Event accepted"))

    async def _process_message(self, turn_context: "TurnContext", text: str):
        """Process message through agent."""
        # ── Owner PIN verification ──
        owner_id = os.environ.get("OWNER_TEAMS_ID")
        pin_hash = os.environ.get("OWNER_PIN_HASH")
        teams_user_id = self._get_user_id(turn_context)

        if not owner_id and pin_hash and text:
            try:
                from ..core.security import (
                    check_pin_rate_limit,
                    claim_ownership,
                    record_pin_attempt,
                    verify_owner_pin,
                )

                allowed, lockout_msg = check_pin_rate_limit(teams_user_id)
                if not allowed:
                    await turn_context.send_activity(MessageFactory.text(lockout_msg))
                    return
                if verify_owner_pin(text.strip()):
                    record_pin_attempt(teams_user_id, success=True)
                    claim_ownership(teams_user_id, "teams", self.agent.sessions)
                    await turn_context.send_activity(
                        MessageFactory.text("🔐 Owner verified. Full access granted.")
                    )
                    return
                elif text.strip().isdigit() and 4 <= len(text.strip()) <= 8:
                    record_pin_attempt(teams_user_id, success=False)
                    await turn_context.send_activity(MessageFactory.text("❌ Incorrect PIN."))
                    return
                else:
                    await turn_context.send_activity(
                        MessageFactory.text("🔒 Enter your owner PIN to claim this bot.")
                    )
                    return
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
        # Auto-promote owner
        if owner_id and teams_user_id == owner_id:
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(teams_user_id, "teams")
                if session.trust_level != TrustLevel.OWNER:
                    session.set_trust_level(TrustLevel.OWNER)
                    session.daily_budget = 50.0
                    self.agent.sessions.save_session(session)
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
        # Show typing indicator
        if self.config.enable_typing_indicator:
            await turn_context.send_activity(Activity(type=ActivityTypes.typing))

        # Authenticate user
        channel_user = self._get_channel_user(turn_context)
        user_context = (
            channel_user.to_context() if channel_user and channel_user.is_linked else None
        )

        user_id = (
            channel_user.user_id
            if channel_user and channel_user.is_linked
            else self._get_user_id(turn_context)
        )

        # Log
        user_info = (
            channel_user.user.email
            if channel_user and channel_user.is_linked
            else f"guest:{self._get_user_id(turn_context)[:16]}"
        )
        logger.info(f"[Teams:{user_info}] {text[:50]}...")

        try:
            # Run agent
            loop = asyncio.get_event_loop()
            # Check if user is responding to a pending confirmation
            if hasattr(self, "_pending_confirm") and user_id in self._pending_confirm:
                token = self._pending_confirm.pop(user_id)
                session = self.agent.sessions.get_or_create_session(user_id, "teams")
                pending = session.pending_confirmations.get(token)
                answer = text.strip().lower()
                if pending and answer.startswith("y"):
                    session.pending_confirmations.pop(token, None)
                    tool_input = dict(pending["tool_input"])
                    tool_input["_confirmed"] = True
                    try:
                        result = await loop.run_in_executor(
                            None,
                            lambda: self.agent.tools.execute(
                                pending["tool_name"],
                                tool_input,
                                context={
                                    "session": session,
                                    "session_manager": self.agent.sessions,
                                    "agent": self.agent,
                                    "user_id": user_id,
                                    "channel": "teams",
                                },
                            ),
                        )
                        from familiar.core.agent import _capture_context_from_result

                        _capture_context_from_result(
                            pending["tool_name"],
                            tool_input,
                            result,
                            session,
                            self.agent.sessions,
                        )
                        await self._send_response(turn_context, result or "✅ Done.")
                    except Exception as _ce:
                        await self._send_response(turn_context, f"⚠️ Action failed: {_ce}")
                elif pending and answer.startswith("n"):
                    session.pending_confirmations.pop(token, None)
                    self.agent.sessions.save_session(session)
                    await self._send_response(turn_context, "❌ Cancelled.")
                else:
                    self._pending_confirm[user_id] = token
                    await self._send_response(
                        turn_context, "Reply **Y** to confirm or **N** to cancel."
                    )
                return

            response = await loop.run_in_executor(
                None,
                lambda: self.agent.chat(
                    message=text,
                    user_id=user_id,
                    channel="teams",
                    user_context=user_context,
                ),
            )

            # Phase 2/4: intercept confirmation sentinel
            from familiar.core.confirmations import SENTINEL_PREFIX

            if response and response.startswith(SENTINEL_PREFIX):
                token = response[len(SENTINEL_PREFIX) :]
                session = self.agent.sessions.get_or_create_session(user_id, "teams")
                pending = session.pending_confirmations.get(token)
                if pending:
                    if not hasattr(self, "_pending_confirm"):
                        self._pending_confirm = {}
                    self._pending_confirm[user_id] = token
                    preview = pending.get("preview", "This action is ready to execute.")
                    risk = pending.get("risk", "medium")
                    flag = "⚠️ " if risk == "high" else ""
                    await self._send_response(
                        turn_context,
                        flag + preview + "\n\nReply **Y** to confirm or **N** to cancel.",
                    )
                else:
                    await self._send_response(
                        turn_context, "⚠️ Confirmation expired. Please try again."
                    )
            else:
                await self._send_response(turn_context, response)

        except Exception as e:
            logger.error(f"Teams error: {e}")
            await turn_context.send_activity(MessageFactory.text(f"❌ Error: {str(e)}"))

    async def _handle_trust(self, turn_context: "TurnContext"):
        """Handle trust command."""
        from ..core.security import TrustLevel

        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        levels = list(TrustLevel)
        lines = ["**🔐 Trust Level**", ""]
        for lvl in levels:
            if session.trust_level == lvl:
                m = "▶"
            elif levels.index(session.trust_level) > levels.index(lvl):
                m = "✅"
            else:
                m = "⬜"
            lines.append(f"{m} **{lvl.value.upper()}**")
        if session.trust_level == TrustLevel.STRANGER:
            prog = f"{session.positive_interactions}/10 to KNOWN"
        elif session.trust_level == TrustLevel.KNOWN:
            prog = f"{session.positive_interactions}/50 to TRUSTED"
        else:
            prog = "Maximum level ⭐"
        lines += ["", f"**Score:** {session.trust_score:.1f}", f"**Progress:** {prog}"]
        await turn_context.send_activity(MessageFactory.text("\n".join(lines)))

    async def _handle_budget(self, turn_context: "TurnContext"):
        """Handle budget command."""
        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        pct = (
            min(100, session.spent_today / session.daily_budget * 100)
            if session.daily_budget > 0
            else 0
        )
        filled = chr(9608) * int(pct / 10)
        empty = chr(9617) * (10 - int(pct / 10))
        bar = f"[{filled}{empty}]"
        msg = (
            f"**💰 Budget**\n"
            f"Limit: ${session.daily_budget:.2f}\n"
            f"{bar} {pct:.1f}%\n"
            f"Spent: ${session.spent_today:.4f}\n"
            f"Remaining: ${session.remaining_budget:.4f}"
        )
        await turn_context.send_activity(MessageFactory.text(msg))

    async def _handle_caps(self, turn_context: "TurnContext"):
        """Handle caps command."""
        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        caps = sorted([c.value for c in session.capabilities])
        cap_list = "\n".join(f"✅ {c}" for c in caps) or "(none yet)"
        await turn_context.send_activity(
            MessageFactory.text(f"**🔑 Capabilities ({len(caps)})**\n{cap_list}")
        )

    async def _handle_status(self, turn_context: "TurnContext"):
        """Handle status command."""
        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        status = self.agent.get_status()
        msg = (
            f"**📊 Status**\n\n"
            f"**You:** {session.trust_level.value.upper()} | "
            f"${session.remaining_budget:.2f} left\n\n"
            f"**System:**\n"
            f"Model: {status['provider']}\n"
            f"Memory: {status['memory_entries']} entries\n"
            f"Skills: {status['skills_loaded']}\n"
            f"Tasks: {status['scheduled_tasks']}\n"
            f"Security: {status['security_mode']}"
        )
        await turn_context.send_activity(MessageFactory.text(msg))

    async def _handle_model(self, turn_context: "TurnContext", provider: str):
        """Handle model command."""
        if not provider:
            from ..core.providers import get_available_providers

            providers = get_available_providers()
            msg = (
                f"**Current:** {self.agent.provider.name}\n"
                f"**Available:** {', '.join(providers)}\n\n"
                "Say `model <provider>` to switch."
            )
        else:
            msg = f"✓ {self.agent.switch_provider(provider)}"
        await turn_context.send_activity(MessageFactory.text(msg))

    async def _handle_remember(self, turn_context: "TurnContext", key: str, value: str):
        """Handle remember command."""
        from ..core.security import Capability

        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        if not session.has_capability(Capability.WRITE_MEMORY):
            await turn_context.send_activity(
                MessageFactory.text("⚠️ You don't have write memory permission yet.")
            )
            return
        self.agent.remember(key, value)
        await turn_context.send_activity(MessageFactory.text(f"✓ Remembered: **{key}**"))

    async def _handle_recall(self, turn_context: "TurnContext", query: str):
        """Handle recall command."""
        from ..core.security import Capability

        user_id = self._get_user_id(turn_context)
        session = self.agent.sessions.get_or_create_session(user_id, "teams")
        if not session.has_capability(Capability.READ_MEMORY):
            await turn_context.send_activity(
                MessageFactory.text("⚠️ You don't have read memory permission yet.")
            )
            return
        if not query:
            await turn_context.send_activity(MessageFactory.text("Usage: recall <query>"))
            return
        results = self.agent.recall(query)
        if not results:
            await turn_context.send_activity(MessageFactory.text(f"No memories matching '{query}'"))
            return
        lines = [f"• **{e.key}**: {e.value}" for e in results[:10]]
        await turn_context.send_activity(MessageFactory.text("\n".join(lines)))

    async def _send_response(self, turn_context: "TurnContext", text: str):
        """Send response, possibly as Adaptive Card for tasks/events."""
        import re as _re

        cards_sent = 0
        remaining_lines = []

        if self.config.use_adaptive_cards:
            task_cards = []
            calendar_cards = []

            for line in text.split("\n"):
                stripped = line.strip()
                # Detect task lines: "- [ ] ...", "- [x] ...", "Task: ..."
                task_match = _re.match(r"^[-*]\s*\[[ xX]?\]\s*(.+)$|^Task:\s*(.+)$", stripped)
                if task_match and len(task_cards) < 5:
                    title = task_match.group(1) or task_match.group(2)
                    task_cards.append(create_task_card(title=title.strip()))
                    continue

                # Detect calendar lines: "Event: ...", "Meeting: ..."
                event_match = _re.match(
                    r"^(?:Event|Meeting):\s*(.+?)(?:\s+at\s+(.+))?$",
                    stripped,
                    _re.IGNORECASE,
                )
                if event_match and len(calendar_cards) < 5:
                    title = event_match.group(1).strip()
                    time_str = event_match.group(2) or ""
                    calendar_cards.append(create_calendar_card(title=title, start_time=time_str))
                    continue

                remaining_lines.append(line)

            # Send Adaptive Cards
            for card in task_cards + calendar_cards:
                attachment = CardFactory.adaptive_card(card)
                await turn_context.send_activity(MessageFactory.attachment(attachment))
                cards_sent += 1

            text = "\n".join(remaining_lines).strip()

        # Send remaining text (or all text if no cards extracted)
        if text:
            if len(text) > 4000:
                chunks = [text[i : i + 4000] for i in range(0, len(text), 4000)]
                for chunk in chunks:
                    await turn_context.send_activity(MessageFactory.text(chunk))
            else:
                await turn_context.send_activity(MessageFactory.text(text))


# ============================================================
# TEAMS CHANNEL (HTTP SERVER)
# ============================================================


class TeamsChannel:
    """
    Microsoft Teams channel using Bot Framework.

    Usage:
        from familiar.core import Agent
        from familiar.channels.teams import TeamsChannel, TeamsConfig

        agent = Agent()
        config = TeamsConfig.from_env()

        channel = TeamsChannel(agent, config)
        channel.run(port=3978)  # Bot Framework default port
    """

    def __init__(
        self, agent, config: TeamsConfig = None, authenticator: ChannelAuthenticator = None
    ):
        if not HAS_BOTFRAMEWORK:
            raise RuntimeError("Bot Framework not available. Check server logs.")

        self.agent = agent
        self.config = config or TeamsConfig.from_env()

        # Create adapter
        settings = BotFrameworkAdapterSettings(
            app_id=self.config.app_id,
            app_password=self.config.app_password,
        )
        self.adapter = BotFrameworkAdapter(settings)

        # Error handler
        async def on_error(context: TurnContext, error: Exception):
            logger.error(f"Bot error: {error}")
            await context.send_activity(
                MessageFactory.text("Sorry, something went wrong. Please try again.")
            )

        self.adapter.on_turn_error = on_error

        # Create bot
        self.bot = FamiliarTeamsBot(agent, config, authenticator)

    def run(self, host: str = "127.0.0.1", port: int = 3978):
        """Run the Teams bot server."""
        app = web.Application()
        app.router.add_post("/api/messages", self._handle_messages)
        app.router.add_get("/health", self._handle_health)

        logger.info(f"Starting Teams bot on http://{host}:{port}")
        logger.info(f"Messaging endpoint: http://{host}:{port}/api/messages")

        # Start scheduler (backup tasks, proactive briefings)
        if hasattr(self, "agent") and hasattr(self.agent, "start_scheduler"):
            self.agent.start_scheduler()

        # Wire scheduler delivery → Teams proactive message
        if self.agent.scheduler and self.bot._conversation_references:
            _bot = self.bot
            _adapter = self.adapter

            def _deliver_to_teams(message, channel, chat_id):
                """Send proactive message to first stored conversation reference."""
                refs = list(_bot._conversation_references.values())
                if not refs:
                    logger.warning("Teams delivery: no conversation references stored yet")
                    return
                ref = _bot._conversation_references.get(chat_id, refs[0])
                import asyncio as _aio

                async def _send():
                    try:

                        async def callback(tc):
                            await tc.send_activity(MessageFactory.text(f"📬 {message}"))

                        await _adapter.continue_conversation(
                            ref, callback, _bot.config.app_id if _bot.config else ""
                        )
                    except Exception as e:
                        logger.error(f"Teams proactive delivery failed: {e}")

                try:
                    loop = _aio.get_event_loop()
                    _aio.run_coroutine_threadsafe(_send(), loop)
                except Exception as e:
                    logger.error(f"Teams delivery error: {e}")

            self.agent.scheduler.set_delivery_callback(_deliver_to_teams)
            logger.info("Scheduler delivery → Teams proactive messaging")

        web.run_app(app, host=host, port=port)

    async def _handle_messages(self, request: "web.Request") -> "web.Response":
        """Handle incoming Bot Framework messages."""
        if "application/json" not in request.content_type:
            return web.Response(status=415)

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

        async def call_bot(turn_context: TurnContext):
            await self.bot.on_turn(turn_context)

        try:
            await self.adapter.process_activity(activity, auth_header, call_bot)
            return web.Response(status=200)
        except Exception as e:
            logger.error(f"Message processing error: {e}")
            return web.Response(status=500)

    async def _handle_health(self, request: "web.Request") -> "web.Response":
        """Health check endpoint."""
        return web.json_response(
            {
                "status": "ok",
                "bot": self.config.bot_name,
                "channel": "teams",
            }
        )


# ============================================================
# CLI ENTRY POINT
# ============================================================


def main():
    """Run Teams bot from command line."""
    import argparse

    parser = argparse.ArgumentParser(description="Familiar Teams Bot")
    parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
    parser.add_argument("--port", type=int, default=3978, help="Port to bind to")
    parser.add_argument("--debug", action="store_true", help="Enable debug logging")

    args = parser.parse_args()

    logging.basicConfig(
        level=logging.DEBUG if args.debug else logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )

    # Check config
    config = TeamsConfig.from_env()
    if not config.app_id or not config.app_password:
        print("Error: TEAMS_APP_ID and TEAMS_APP_PASSWORD required")
        print("Set environment variables or configure in config.yaml")
        return 1

    # Create agent
    from familiar.core import Agent

    agent = Agent()

    # Run channel
    channel = TeamsChannel(agent, config)
    channel.run(host=args.host, port=args.port)


if __name__ == "__main__":
    main()
