# 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 skill tools — uses the AT Protocol XRPC API via httpx (no extra dep).

Requires BLUESKY_HANDLE and BLUESKY_APP_PASSWORD environment variables.
Session management: JWT cached in module-level dict, refreshed before expiry.
"""

import logging
import os
import threading
import time
from datetime import datetime, timezone

import httpx

logger = logging.getLogger(__name__)

REQUEST_TIMEOUT = 30

# Module-level session cache
_session_lock = threading.Lock()
_session: dict = {}  # {"accessJwt": ..., "refreshJwt": ..., "did": ..., "expires_at": ...}


def _get_config() -> dict:
    return {
        "handle": os.environ.get("BLUESKY_HANDLE", ""),
        "app_password": os.environ.get("BLUESKY_APP_PASSWORD", ""),
        "pds_url": os.environ.get("BLUESKY_PDS_URL", "https://bsky.social").rstrip("/"),
    }


def _configured() -> bool:
    cfg = _get_config()
    return bool(cfg["handle"] and cfg["app_password"])


def _get_session() -> dict:
    """Get or refresh a valid XRPC session."""
    global _session

    with _session_lock:
        now = time.time()
        if _session and _session.get("expires_at", 0) > now + 60:
            return _session

        cfg = _get_config()
        if not cfg["handle"] or not cfg["app_password"]:
            return {}

        try:
            resp = httpx.post(
                f"{cfg['pds_url']}/xrpc/com.atproto.server.createSession",
                json={"identifier": cfg["handle"], "password": cfg["app_password"]},
                timeout=REQUEST_TIMEOUT,
            )
            if resp.status_code != 200:
                logger.error("Bluesky auth failed: %s", resp.text[:200])
                return {}

            data = resp.json()
            _session = {
                "accessJwt": data.get("accessJwt", ""),
                "refreshJwt": data.get("refreshJwt", ""),
                "did": data.get("did", ""),
                "handle": data.get("handle", ""),
                "expires_at": now + 3600,  # 1 hour cache
            }
            return _session
        except Exception as e:
            logger.error("Bluesky session error: %s", e)
            return {}


def _headers() -> dict:
    session = _get_session()
    if not session:
        return {}
    return {"Authorization": f"Bearer {session['accessJwt']}"}


def _base_url() -> str:
    return _get_config()["pds_url"]


# ── Tool handlers ────────────────────────────────────────────────


def bluesky_post(data: dict) -> str:
    """Create a post on Bluesky."""
    if not _configured():
        return "Bluesky not configured. Run /connect bluesky to set up."

    text = data.get("text", "").strip()
    if not text:
        return "Please provide 'text' for the post."
    if len(text) > 300:
        return "Post exceeds 300-char limit"

    session = _get_session()
    if not session:
        return "Failed to authenticate with Bluesky. Check your credentials."

    now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    record = {
        "$type": "app.bsky.feed.post",
        "text": text,
        "createdAt": now,
    }

    try:
        resp = httpx.post(
            f"{_base_url()}/xrpc/com.atproto.repo.createRecord",
            headers=_headers(),
            json={
                "repo": session["did"],
                "collection": "app.bsky.feed.post",
                "record": record,
            },
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code not in (200, 201):
            return f"Post failed (HTTP {resp.status_code}): {resp.text[:200]}"

        result = resp.json()
        uri = result.get("uri", "")
        # Convert AT URI to web URL
        parts = uri.split("/") if uri else []
        if len(parts) >= 5:
            rkey = parts[-1]
            handle = session.get("handle", "")
            web_url = f"https://bsky.app/profile/{handle}/post/{rkey}"
            return f"Posted to Bluesky: {web_url}"
        return f"Posted to Bluesky (URI: {uri})"
    except Exception as e:
        logger.error("Bluesky post error: %s", e)
        return f"Failed to post: {e}"


def bluesky_timeline(data: dict) -> str:
    """Get the home timeline."""
    if not _configured():
        return "Bluesky not configured. Run /connect bluesky to set up."

    limit = min(int(data.get("limit", 20)), 50)

    try:
        resp = httpx.get(
            f"{_base_url()}/xrpc/app.bsky.feed.getTimeline",
            headers=_headers(),
            params={"limit": str(limit)},
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code != 200:
            return f"Failed to fetch timeline (HTTP {resp.status_code})"

        result = resp.json()
        feed = result.get("feed", [])
        if not feed:
            return "Timeline is empty."

        lines = ["Home Timeline:"]
        for item in feed[:limit]:
            post = item.get("post", {})
            author = post.get("author", {})
            handle = author.get("handle", "?")
            record = post.get("record", {})
            text = record.get("text", "")
            text = text[:120] + "..." if len(text) > 120 else text
            ts = record.get("createdAt", "")[:10]
            lines.append(f"  @{handle} ({ts}): {text}")
        return "\n".join(lines)
    except Exception as e:
        logger.error("Bluesky timeline error: %s", e)
        return f"Failed to fetch timeline: {e}"


def bluesky_notifications(data: dict) -> str:
    """Get recent notifications."""
    if not _configured():
        return "Bluesky not configured. Run /connect bluesky to set up."

    limit = min(int(data.get("limit", 20)), 50)

    try:
        resp = httpx.get(
            f"{_base_url()}/xrpc/app.bsky.notification.listNotifications",
            headers=_headers(),
            params={"limit": str(limit)},
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code != 200:
            return f"Failed to fetch notifications (HTTP {resp.status_code})"

        result = resp.json()
        notifs = result.get("notifications", [])
        if not notifs:
            return "No recent notifications."

        lines = ["Recent Notifications:"]
        for n in notifs[:limit]:
            reason = n.get("reason", "?")
            author = n.get("author", {}).get("handle", "?")
            ts = n.get("indexedAt", "")[:10]
            record = n.get("record", {})
            text = record.get("text", "")
            text = text[:100] + "..." if len(text) > 100 else text
            detail = f": {text}" if text else ""
            lines.append(f"  [{reason}] @{author} ({ts}){detail}")
        return "\n".join(lines)
    except Exception as e:
        logger.error("Bluesky notifications error: %s", e)
        return f"Failed to fetch notifications: {e}"


def bluesky_search(data: dict) -> str:
    """Search for actors on Bluesky."""
    if not _configured():
        return "Bluesky not configured. Run /connect bluesky to set up."

    query = data.get("query", "").strip()
    if not query:
        return "Please provide a 'query' to search for."

    try:
        resp = httpx.get(
            f"{_base_url()}/xrpc/app.bsky.actor.searchActors",
            headers=_headers(),
            params={"q": query, "limit": "10"},
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code != 200:
            return f"Search failed (HTTP {resp.status_code})"

        result = resp.json()
        actors = result.get("actors", [])
        if not actors:
            return f"No results found for '{query}'."

        lines = [f"Search results for '{query}':"]
        for a in actors[:10]:
            handle = a.get("handle", "?")
            name = a.get("displayName", "")
            desc = a.get("description", "")
            desc = desc[:100] + "..." if len(desc) > 100 else desc
            name_str = f" ({name})" if name else ""
            lines.append(f"  @{handle}{name_str}")
            if desc:
                lines.append(f"    {desc}")
        return "\n".join(lines)
    except Exception as e:
        logger.error("Bluesky search error: %s", e)
        return f"Search failed: {e}"


def bluesky_profile(data: dict) -> str:
    """Get a user's profile information."""
    if not _configured():
        return "Bluesky not configured. Run /connect bluesky to set up."

    actor = data.get("handle", "").strip()
    if not actor:
        session = _get_session()
        actor = session.get("handle", "")
        if not actor:
            return "Failed to determine account handle."

    try:
        resp = httpx.get(
            f"{_base_url()}/xrpc/app.bsky.actor.getProfile",
            headers=_headers(),
            params={"actor": actor},
            timeout=REQUEST_TIMEOUT,
        )
        if resp.status_code != 200:
            return f"Failed to fetch profile (HTTP {resp.status_code})"

        p = resp.json()
        desc = p.get("description", "")
        desc = desc[:200] + "..." if len(desc) > 200 else desc
        return (
            f"Profile: @{p.get('handle', '?')}\n"
            f"  Name: {p.get('displayName', '')}\n"
            f"  Bio: {desc}\n"
            f"  Followers: {p.get('followersCount', 0)}\n"
            f"  Following: {p.get('followsCount', 0)}\n"
            f"  Posts: {p.get('postsCount', 0)}\n"
            f"  DID: {p.get('did', '')}"
        )
    except Exception as e:
        logger.error("Bluesky profile error: %s", e)
        return f"Failed to fetch profile: {e}"


# ── Tool definitions ─────────────────────────────────────────────

TOOLS = [
    {
        "name": "bluesky_post",
        "description": "Create a post on Bluesky. Text-only posts up to 300 characters.",
        "input_schema": {
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "The post text (max 300 chars)"},
            },
            "required": ["text"],
        },
        "handler": bluesky_post,
        "category": "social",
    },
    {
        "name": "bluesky_timeline",
        "description": "Get the home timeline from Bluesky. Returns recent posts from followed accounts.",
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "description": "Number of posts to fetch (max 50)",
                },
            },
        },
        "handler": bluesky_timeline,
        "category": "social",
    },
    {
        "name": "bluesky_notifications",
        "description": "Get recent notifications from Bluesky (mentions, replies, likes, follows).",
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "description": "Number of notifications to fetch (max 50)",
                },
            },
        },
        "handler": bluesky_notifications,
        "category": "social",
    },
    {
        "name": "bluesky_search",
        "description": "Search for users on Bluesky by name or handle.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"},
            },
            "required": ["query"],
        },
        "handler": bluesky_search,
        "category": "social",
    },
    {
        "name": "bluesky_profile",
        "description": "Get a Bluesky user's profile. Leave handle empty to get your own profile.",
        "input_schema": {
            "type": "object",
            "properties": {
                "handle": {
                    "type": "string",
                    "description": "Handle to look up (e.g. user.bsky.social). Empty for own profile.",
                },
            },
        },
        "handler": bluesky_profile,
        "category": "social",
    },
]
