# app/api/endpoints/activitypub.py
# MIT License
#
# ActivityPub endpoints for Kiwi instances:
#   GET  /.well-known/webfinger   — WebFinger JRD
#   GET  /ap/actor                — Instance actor document
#   POST /ap/actor/inbox          — Incoming activities
#   GET  /ap/outbox               — Outgoing activities (OrderedCollection)
#   GET  /ap/posts/{slug}         — Individual AP Note
#   GET  /ap/followers            — Followers collection (count only)
#   GET  /ap/following            — Following collection (empty stub)
#
# All endpoints are no-ops / 404 when AP_ENABLED=false or actor not loaded.
# The WebFinger and well-known routes are mounted at the root app level (not
# under /api/v1) — see main.py.

from __future__ import annotations

import asyncio
import json
import logging
from datetime import datetime, timezone

from fastapi import APIRouter, HTTPException, Request, Response

from app.core.config import settings
from app.services.ap.keys import get_actor

logger = logging.getLogger(__name__)

# ── Two routers: one for well-known (root mount), one for /ap prefix ─────────

webfinger_router = APIRouter(tags=["activitypub"])
ap_router = APIRouter(prefix="/ap", tags=["activitypub"])

_AP_CONTENT_TYPE = "application/activity+json"
_JRD_CONTENT_TYPE = "application/jrd+json"


def _actor_required():
    actor = get_actor()
    if actor is None:
        raise HTTPException(status_code=404, detail="ActivityPub not enabled on this instance.")
    return actor


# ── WebFinger ─────────────────────────────────────────────────────────────────

@webfinger_router.get("/.well-known/webfinger")
async def webfinger(resource: str | None = None):
    actor = get_actor()
    if actor is None:
        raise HTTPException(status_code=404, detail="ActivityPub not enabled.")

    expected = f"acct:kiwi@{settings.AP_HOST}"
    if resource and resource != expected:
        raise HTTPException(status_code=404, detail=f"Resource {resource!r} not found.")

    jrd = {
        "subject": expected,
        "links": [
            {
                "rel": "self",
                "type": _AP_CONTENT_TYPE,
                "href": actor.actor_id,
            }
        ],
    }
    return Response(
        content=json.dumps(jrd),
        media_type=_JRD_CONTENT_TYPE,
    )


# ── Actor ─────────────────────────────────────────────────────────────────────

@ap_router.get("/actor")
async def get_actor_doc():
    actor = _actor_required()
    return Response(
        content=json.dumps(actor.to_ap_dict()),
        media_type=_AP_CONTENT_TYPE,
    )


# ── Inbox (mounted via make_inbox_router below) ───────────────────────────────

async def _on_follow(activity: dict, headers: dict) -> None:
    """Accept Follow: add to ap_followers, send Accept(Follow) back."""
    actor_url = activity.get("actor", "")
    if not actor_url:
        return

    from app.core.config import settings as _settings
    db_path = _settings.DB_PATH

    inbox_url, shared_inbox = await asyncio.to_thread(_resolve_inbox, actor_url)
    if inbox_url is None:
        return

    import sqlite3
    conn = sqlite3.connect(str(db_path))
    try:
        conn.execute(
            """INSERT OR REPLACE INTO ap_followers
               (actor_id, inbox_url, shared_inbox, followed_at, active)
               VALUES (?, ?, ?, ?, 1)""",
            (actor_url, inbox_url, shared_inbox, datetime.now(timezone.utc).isoformat()),
        )
        conn.commit()
    finally:
        conn.close()

    actor = get_actor()
    if actor is None:
        return
    accept = {
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": f"{actor.actor_id}/accepts/{activity.get('id', 'unknown')}",
        "type": "Accept",
        "actor": actor.actor_id,
        "object": activity,
    }
    from circuitforge_core.activitypub import deliver_activity
    await asyncio.to_thread(deliver_activity, accept, inbox_url, actor, 10.0)


async def _on_undo(activity: dict, headers: dict) -> None:
    """Handle Undo(Follow): deactivate the follower row."""
    inner = activity.get("object", {})
    if isinstance(inner, dict) and inner.get("type") == "Follow":
        actor_url = activity.get("actor", "")
        if actor_url:
            import sqlite3
            conn = sqlite3.connect(str(settings.DB_PATH))
            try:
                conn.execute(
                    "UPDATE ap_followers SET active = 0 WHERE actor_id = ?", (actor_url,)
                )
                conn.commit()
            finally:
                conn.close()


async def _dedup_activity(activity_id: str | None) -> bool:
    """Return True (already seen) if activity_id is in ap_received; otherwise insert it."""
    if not activity_id:
        return False
    import sqlite3
    conn = sqlite3.connect(str(settings.DB_PATH))
    try:
        try:
            conn.execute(
                "INSERT INTO ap_received (activity_id) VALUES (?)", (activity_id,)
            )
            conn.commit()
            return False
        except sqlite3.IntegrityError:
            return True
    finally:
        conn.close()


def _build_inbox_router():
    from circuitforge_core.activitypub.inbox import make_inbox_router

    async def on_follow(activity: dict, headers: dict) -> None:
        if await _dedup_activity(activity.get("id")):
            return
        await _on_follow(activity, headers)

    async def on_undo(activity: dict, headers: dict) -> None:
        if await _dedup_activity(activity.get("id")):
            return
        await _on_undo(activity, headers)

    return make_inbox_router(
        handlers={"Follow": on_follow, "Undo": on_undo},
        verify_key_fetcher=None,   # Signature verification enabled in prod when actor is loaded
        path="/inbox",
    )


# Mount inbox at /ap/actor/inbox (AP spec: inbox is a sub-resource of the actor)
try:
    _inbox_sub = _build_inbox_router()
    ap_router.include_router(_inbox_sub, prefix="/actor")
except Exception as _e:
    logger.warning("AP inbox router not available: %s", _e)


# ── Outbox ────────────────────────────────────────────────────────────────────

@ap_router.get("/outbox")
async def get_outbox(page: int | None = None, request: Request = None):
    actor = _actor_required()
    from app.api.endpoints.community import _get_community_store
    store = _get_community_store()
    base = f"https://{settings.AP_HOST}"

    if store is None:
        collection = {
            "@context": "https://www.w3.org/ns/activitystreams",
            "id": f"{actor.outbox_url}",
            "type": "OrderedCollection",
            "totalItems": 0,
            "orderedItems": [],
        }
        return Response(content=json.dumps(collection), media_type=_AP_CONTENT_TYPE)

    PAGE_SIZE = 20
    offset = ((page or 1) - 1) * PAGE_SIZE
    posts = await asyncio.to_thread(store.list_posts, limit=PAGE_SIZE, offset=offset)
    items = [_post_to_ap_note(p, actor, base) for p in posts]

    collection = {
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": actor.outbox_url + (f"?page={page}" if page else ""),
        "type": "OrderedCollectionPage" if page else "OrderedCollection",
        "orderedItems": items,
    }
    return Response(content=json.dumps(collection), media_type=_AP_CONTENT_TYPE)


# ── Individual post ───────────────────────────────────────────────────────────

@ap_router.get("/posts/{slug}")
async def get_ap_post(slug: str):
    actor = _actor_required()
    from app.api.endpoints.community import _get_community_store
    store = _get_community_store()
    if store is None:
        raise HTTPException(status_code=404, detail="Community DB not available.")

    post = await asyncio.to_thread(store.get_post_by_slug, slug)
    if post is None:
        raise HTTPException(status_code=404, detail="Post not found.")

    base = f"https://{settings.AP_HOST}"
    note = _post_to_ap_note(post, actor, base)
    return Response(content=json.dumps(note), media_type=_AP_CONTENT_TYPE)


# ── Followers / Following ─────────────────────────────────────────────────────

@ap_router.get("/followers")
async def get_followers():
    actor = _actor_required()
    import sqlite3
    count = 0
    try:
        conn = sqlite3.connect(str(settings.DB_PATH))
        row = conn.execute("SELECT COUNT(*) FROM ap_followers WHERE active = 1").fetchone()
        conn.close()
        count = row[0] if row else 0
    except Exception:
        pass

    collection = {
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": f"{actor.actor_id}/followers",
        "type": "OrderedCollection",
        "totalItems": count,
    }
    return Response(content=json.dumps(collection), media_type=_AP_CONTENT_TYPE)


@ap_router.get("/following")
async def get_following():
    actor = _actor_required()
    collection = {
        "@context": "https://www.w3.org/ns/activitystreams",
        "id": f"{actor.actor_id}/following",
        "type": "OrderedCollection",
        "totalItems": 0,
        "orderedItems": [],
    }
    return Response(content=json.dumps(collection), media_type=_AP_CONTENT_TYPE)


# ── Helpers ───────────────────────────────────────────────────────────────────

def _post_to_ap_note(post, actor, base_url: str) -> dict:
    from circuitforge_core.activitypub import make_note

    from app.services.community.ap_compat import _build_content

    diet_tags: list[str] = list(getattr(post, "dietary_tags", []) or [])
    hashtags = [{"type": "Hashtag", "name": "#Kiwi", "href": f"{base_url}/ap/tags/kiwi"}]
    for tag in diet_tags[:4]:
        ht = "".join(w.capitalize() for w in tag.replace("-", " ").split())
        hashtags.append({"type": "Hashtag", "name": f"#{ht}"})

    content = _build_content(
        {
            "title": post.title,
            "description": getattr(post, "description", None),
            "outcome_notes": getattr(post, "outcome_notes", None),
            "dietary_tags": diet_tags,
        }
    )

    published = post.published
    note = make_note(
        actor_id=actor.actor_id,
        content=content,
        tag=hashtags,
        published=published if isinstance(published, datetime) else None,
    )
    note["id"] = f"{base_url}/ap/posts/{post.slug}"
    return note


def _resolve_inbox(actor_url: str) -> tuple[str | None, str | None]:
    """Fetch an AP actor document and extract inbox + sharedInbox URLs."""
    try:
        import httpx
        resp = httpx.get(
            actor_url,
            headers={"Accept": "application/activity+json"},
            timeout=8.0,
            follow_redirects=True,
        )
        resp.raise_for_status()
        doc = resp.json()
        inbox = doc.get("inbox")
        shared = doc.get("endpoints", {}).get("sharedInbox")
        return inbox, shared
    except Exception as exc:
        logger.debug("Could not resolve actor %s: %s", actor_url, exc)
        return None, None
