# 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

"""
Bluesky / AT Protocol Channel

Connect to Bluesky via the atproto SDK.

Setup:
1. Install atproto:
   pip install familiar-agent[bluesky]

2. Set environment variables:
   BLUESKY_HANDLE=your.handle.bsky.social
   BLUESKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
   BLUESKY_PDS_URL=https://bsky.social       # optional, defaults to bsky.social
   OWNER_BLUESKY_HANDLE=your.handle.bsky.social
"""

import asyncio
import logging
import os
from threading import Thread

logger = logging.getLogger(__name__)

BLUESKY_HANDLE = os.environ.get("BLUESKY_HANDLE", "")
BLUESKY_APP_PASSWORD = os.environ.get("BLUESKY_APP_PASSWORD", "")
BLUESKY_PDS_URL = os.environ.get("BLUESKY_PDS_URL", "https://bsky.social")
OWNER_BLUESKY_HANDLE = os.environ.get("OWNER_BLUESKY_HANDLE", "")

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


class BlueskyChannel(ConnectMixin):
    """
    Bluesky integration via atproto SDK.

    Uses polling for notifications (AT Protocol has no WebSocket
    notification stream).
    """

    format_mode = FormatMode.PLAIN
    supports_buttons = False
    supports_message_deletion = False

    def __init__(
        self,
        agent,
        handle: str = None,
        app_password: str = None,
        pds_url: 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.handle = handle or BLUESKY_HANDLE
        self.app_password = app_password or BLUESKY_APP_PASSWORD
        self.pds_url = (pds_url or BLUESKY_PDS_URL).rstrip("/")
        self.owner_handle = owner_handle or OWNER_BLUESKY_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._did = None
        self._last_seen_at = None
        self._poll_interval = 15  # seconds

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

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

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

    async def _connect(self):
        """Connect to the Bluesky PDS and authenticate."""
        try:
            from atproto import Client
        except ImportError:
            from familiar.core.deps import BLUESKY_PACKAGES, ensure_packages

            ok, _ = ensure_packages(BLUESKY_PACKAGES)
            if ok:
                from atproto import Client
            else:
                raise RuntimeError("atproto not available. Install with: pip install atproto")

        self._client = Client(base_url=self.pds_url)
        profile = self._client.login(self.handle, self.app_password)
        self._did = profile.did
        logger.info("Bluesky authenticated as %s (DID: %s)", self.handle, self._did)

    async def _send_reply(self, uri: str, cid: str, text: str) -> None:
        """Send a reply to a post."""
        if not self._client:
            return
        try:
            from atproto import models

            loop = asyncio.get_event_loop()

            if uri and cid:
                reply_ref = models.AppBskyFeedPost.ReplyRef(
                    parent=models.create_strong_ref(uri=uri, cid=cid),
                    root=models.create_strong_ref(uri=uri, cid=cid),
                )
                await loop.run_in_executor(
                    None, lambda: self._client.send_post(text=text, reply_to=reply_ref)
                )
            else:
                await loop.run_in_executor(
                    None, lambda: self._client.send_post(text=text)
                )
        except Exception as e:
            logger.error("Failed to send Bluesky reply: %s", e)

    async def _poll_notifications(self):
        """Poll for new notifications."""
        if not self._client:
            return

        try:
            loop = asyncio.get_event_loop()
            response = await loop.run_in_executor(
                None, lambda: self._client.app.bsky.notification.list_notifications(
                    params={"limit": 20}
                )
            )

            for notif in response.notifications:
                # Skip if already processed
                if self._last_seen_at and notif.indexed_at <= self._last_seen_at:
                    continue

                if notif.reason not in ("mention", "reply"):
                    continue

                if not notif.is_read:
                    await self._handle_notification(notif)

            # Mark notifications as seen
            if response.notifications:
                self._last_seen_at = response.notifications[0].indexed_at
                try:
                    await loop.run_in_executor(
                        None, lambda: self._client.app.bsky.notification.update_seen(
                            data={"seenAt": self._last_seen_at}
                        )
                    )
                except Exception:
                    pass

        except Exception as e:
            logger.error("Bluesky poll error: %s", e)

    async def _handle_notification(self, notif):
        """Process a single notification."""
        sender_handle = notif.author.handle
        record = notif.record

        text = getattr(record, "text", "")
        if not text:
            return

        # Remove self-mention
        import re
        text = re.sub(rf"@{re.escape(self.handle)}\s*", "", text).strip()

        if not text:
            return

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

        # Owner auto-promote
        if self.owner_handle and sender_handle == self.owner_handle:
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender_handle, "bluesky")
                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("Bluesky from %s: %s", sender_handle, text[:50])

        # Process through agent
        try:
            response = self.agent.chat(text, user_id=sender_handle, channel="bluesky")
            if response:
                uri = getattr(notif, "uri", "")
                cid = getattr(notif, "cid", "")
                await self._send_reply(uri, cid, response)
        except Exception as e:
            logger.error("Error processing Bluesky message: %s", e)

    def run(self):
        """Start the Bluesky channel (blocking)."""
        if not self.handle:
            logger.error("BLUESKY_HANDLE not set")
            return

        if not self.app_password:
            logger.error("BLUESKY_APP_PASSWORD not set")
            return

        logger.info("Starting Bluesky 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 poll."""
        await self._connect()

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

        print(f"Bluesky channel active as {self.handle}")

        # Poll loop
        while self._running:
            await self._poll_notifications()
            await asyncio.sleep(self._poll_interval)

    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
