# 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

"""
Mastodon / GoToSocial Channel

Connect to any Mastodon-compatible instance via Mastodon.py.

Setup:
1. Install Mastodon.py:
   pip install familiar-agent[mastodon]

2. Set environment variables:
   MASTODON_INSTANCE_URL=https://mastodon.social  # or your GoToSocial URL
   MASTODON_ACCESS_TOKEN=your-access-token
   OWNER_MASTODON_HANDLE=@you@mastodon.social
"""

import asyncio
import logging
import os
from threading import Thread

logger = logging.getLogger(__name__)

MASTODON_INSTANCE_URL = os.environ.get("MASTODON_INSTANCE_URL", "")
MASTODON_ACCESS_TOKEN = os.environ.get("MASTODON_ACCESS_TOKEN", "")
OWNER_MASTODON_HANDLE = os.environ.get("OWNER_MASTODON_HANDLE", "")

from .connect_wizard import ConnectMixin  # noqa: E402
from .formatting import FormatMode  # noqa: E402


class MastodonChannel(ConnectMixin):
    """
    Mastodon / GoToSocial integration via Mastodon.py.

    Listens for mentions and DMs via streaming API,
    processes messages through the Familiar agent.
    """

    format_mode = FormatMode.PLAIN
    supports_buttons = False
    supports_message_deletion = False

    def __init__(
        self,
        agent,
        instance_url: str = None,
        access_token: str = None,
        owner_handle: str = None,
        allowed_users: list = None,
        respond_to_mentions: bool = True,
        respond_to_dms: bool = True,
        auth_mode=None,
    ):
        self.agent = agent
        self.instance_url = (instance_url or MASTODON_INSTANCE_URL).rstrip("/")
        self.access_token = access_token or MASTODON_ACCESS_TOKEN
        self.owner_handle = owner_handle or OWNER_MASTODON_HANDLE
        self.allowed_users = set(allowed_users) if allowed_users else None
        self.respond_to_mentions = respond_to_mentions
        self.respond_to_dms = respond_to_dms
        self.auth_mode = auth_mode

        self._client = None
        self._running = False
        self._authenticator = None
        self._pending_confirm = {}
        self._loop = None
        self._account_id = None

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

    async def wizard_send(self, recipient_id: str, text: str) -> None:
        await self._send_reply(recipient_id, text)

    async def wizard_delete_message(self, recipient_id: str, message_ref) -> bool:
        return False

    async def _connect(self):
        """Connect to the Mastodon instance and verify credentials."""
        try:
            from mastodon import Mastodon
        except ImportError:
            from familiar.core.deps import MASTODON_PACKAGES, ensure_packages

            ok, _ = ensure_packages(MASTODON_PACKAGES)
            if ok:
                from mastodon import Mastodon
            else:
                raise RuntimeError("Mastodon.py not available. Install with: pip install Mastodon.py")

        self._client = Mastodon(
            access_token=self.access_token,
            api_base_url=self.instance_url,
        )

        # Verify credentials
        account = self._client.account_verify_credentials()
        self._account_id = account["id"]
        logger.info("Mastodon authenticated as @%s@%s", account["acct"], self.instance_url)

    async def _send_reply(self, in_reply_to_id: str, text: str) -> None:
        """Send a reply to a status."""
        if not self._client:
            return
        try:
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(
                None, lambda: self._client.status_reply(
                    self._client.status(in_reply_to_id),
                    text,
                    visibility="direct" if self._is_dm_status(in_reply_to_id) else "unlisted",
                )
            )
        except Exception as e:
            logger.error("Failed to send Mastodon reply: %s", e)

    async def _send_dm(self, account_acct: str, text: str) -> None:
        """Send a direct message to a user."""
        if not self._client:
            return
        try:
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(
                None, lambda: self._client.status_post(
                    f"@{account_acct} {text}",
                    visibility="direct",
                )
            )
        except Exception as e:
            logger.error("Failed to send Mastodon DM: %s", e)

    def _is_dm_status(self, status_id: str) -> bool:
        """Check if a status is a DM."""
        try:
            status = self._client.status(status_id)
            return status.get("visibility") == "direct"
        except Exception:
            return False

    def _on_notification(self, notification):
        """Handle incoming notification (mention or DM)."""
        ntype = notification.get("type", "")
        if ntype not in ("mention",):
            return

        status = notification.get("status")
        if not status:
            return

        sender_acct = notification.get("account", {}).get("acct", "")
        text = status.get("content", "")
        status_id = str(status.get("id", ""))
        visibility = status.get("visibility", "")

        # Strip HTML tags from content
        import re
        text = re.sub(r"<[^>]+>", "", text).strip()
        # Remove self-mention at the start
        text = re.sub(r"^@\S+\s*", "", text).strip()

        if not text:
            return

        # Check allowed users
        if self.allowed_users and sender_acct not in self.allowed_users:
            if sender_acct != self.owner_handle.lstrip("@"):
                logger.debug("Ignoring message from non-allowed user %s", sender_acct)
                return

        is_dm = visibility == "direct"
        if is_dm and not self.respond_to_dms:
            return
        if not is_dm and not self.respond_to_mentions:
            return

        # Owner auto-promote
        if self.owner_handle and sender_acct == self.owner_handle.lstrip("@"):
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender_acct, "mastodon")
                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, AttributeError) as e:
                logger.debug("Optional dependency not available: %s", e)

        logger.info("Mastodon from %s: %s", sender_acct, text[:50])

        # Process through agent
        try:
            response = self.agent.chat(text, user_id=sender_acct, channel="mastodon")
            if response:
                if self._loop and self._loop.is_running():
                    asyncio.run_coroutine_threadsafe(
                        self._send_reply(status_id, response), self._loop
                    )
        except Exception as e:
            logger.error("Error processing Mastodon message: %s", e)

    def run(self):
        """Start the Mastodon channel (blocking)."""
        if not self.instance_url:
            logger.error("MASTODON_INSTANCE_URL not set")
            return

        if not self.access_token:
            logger.error("MASTODON_ACCESS_TOKEN not set")
            return

        logger.info("Starting Mastodon channel...")
        self._running = True
        self._loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._loop)

        try:
            self._loop.run_until_complete(self._run_async())
        except KeyboardInterrupt:
            pass
        finally:
            self._running = False
            self._loop.close()

    async def _run_async(self):
        """Async main loop — connect and stream notifications."""
        await self._connect()

        # Start scheduler
        if hasattr(self.agent, "start_scheduler"):
            self.agent.start_scheduler()

        # Wire scheduler delivery
        if hasattr(self.agent, "scheduler") and self.agent.scheduler:
            owner = self.owner_handle
            if owner:
                _self = self

                def _deliver_to_mastodon(message, channel, chat_id):
                    if owner:
                        loop = _self._loop
                        if loop and loop.is_running():
                            asyncio.run_coroutine_threadsafe(
                                _self._send_dm(owner.lstrip("@"), f"[Scheduled] {message}"),
                                loop,
                            )

                self.agent.scheduler.set_delivery_callback(_deliver_to_mastodon)
                logger.info("Scheduler delivery -> Mastodon (%s)", owner)

        print(f"Mastodon channel active on {self.instance_url}")

        # Stream user notifications
        loop = asyncio.get_event_loop()
        try:
            while self._running:
                try:
                    await loop.run_in_executor(
                        None, lambda: self._client.stream_user(
                            self._make_listener(),
                            run_async=False,
                            timeout=300,
                        )
                    )
                except Exception as e:
                    logger.error("Mastodon stream error: %s", e)
                    if self._running:
                        await asyncio.sleep(5)
        except Exception as e:
            logger.error("Mastodon main loop error: %s", e)

    def _make_listener(self):
        """Create a streaming listener that dispatches to _on_notification."""
        from mastodon import StreamListener

        channel = self

        class _Listener(StreamListener):
            def on_notification(self, notification):
                channel._on_notification(notification)

        return _Listener()

    def run_async(self):
        """Start in background thread."""
        thread = Thread(target=self.run, daemon=True)
        thread.start()
        return thread

    def stop(self):
        """Stop the channel."""
        self._running = False
