"""§5 / §7 / §10 — PR-less per-RFC discussion endpoints (v0.5.0).

This module surfaces the discussion-without-PR shape committed by the
roadmap's item #3. The substrate is the existing `threads` /
`thread_messages` pair from §5: rows whose `branch_name` is NULL are
scoped to the RFC's main view (the schema comment on the column says
exactly this; until now no write path produced such rows). This module
is the read+write surface for those rows.

Contribution still requires a PR: the §10 PR flow is unchanged, the
branch-scoped chat in `api_branches.py` is unchanged, and accept /
decline of AI `<change>` blocks still lives on a branch. What this
module adds is the "discuss freely about the RFC, no branch yet" surface
— a place to drop a question, a flag-style observation, or a multi-turn
conversation that does not yet warrant cutting a branch.

Auth shape mirrors the v0.3.0 anonymous-read contract: reads are open,
writes require `auth.require_contributor`. Item #4 ("anon discuss/
contribute off-limits") tightens the read gate in v0.6.0; v0.5.0's
write gate already holds the line.

Notification routing reuses the existing `fan_out_chat_message` path
with `branch_name=None`; the `notifications.branch_name` column is
nullable, and the inbox row prose ("@alice posted a chat message on
<RFC title>") renders identically whether the chat lives on a branch
or on the RFC's discussion surface. The existing
`chat_message_in_participated_thread` / `chat_reply_to_my_message`
event kinds carry both shapes; introducing a parallel
`open_rfc_discussion_thread` / `post_rfc_discussion_message` enum pair
would split routing without adding signal. The §15 §19.2 candidate
"distinct event_kinds for PR-less discussion" notes the option for a
future session if evidence demands the split.
"""
from __future__ import annotations

import json
import logging
from typing import Any

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field

from . import auth, chat as chat_layer, db, rfc_links

log = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Request bodies
# ---------------------------------------------------------------------------


class DiscussionThreadCreateBody(BaseModel):
    """A discussion thread is a `thread_kind='chat'`, `anchor_kind='whole-doc'`,
    `branch_name=NULL` row. Anchored-range / per-paragraph threads on the
    RFC discussion surface are a §19.2 candidate — the schema supports
    them; the UI work to surface a range-anchor on a non-branch view is
    the deferred part. v0.5.0 keeps the shape narrow."""
    label: str | None = Field(default=None, max_length=400)
    message: str | None = Field(default=None, max_length=20_000)


class DiscussionMessageBody(BaseModel):
    text: str = Field(min_length=1, max_length=20_000)
    quote: str | None = Field(default=None, max_length=2000)


# ---------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------


def make_router() -> APIRouter:
    router = APIRouter()

    # -------------------------------------------------------------------
    # GET /api/rfcs/<slug>/discussion/threads
    # Lists every PR-less thread on the RFC. The default whole-doc thread
    # is materialized lazily on first list (mirroring the §8.12 branch-
    # chat default-thread treatment) so the UI always has a target for
    # the compose-message affordance.
    # -------------------------------------------------------------------

    @router.get("/api/rfcs/{slug}/discussion/threads")
    async def list_discussion_threads(slug: str, request: Request) -> dict[str, Any]:
        viewer = auth.current_user(request)
        _require_rfc_readable(slug, viewer)
        # Ensure the default whole-doc discussion thread exists. We mint
        # it on first read regardless of viewer (anonymous viewers can
        # trigger the creation — the row's `created_by` is null in that
        # case, mirroring `_ensure_branch_chat_thread`).
        _ensure_discussion_thread(slug, viewer)
        rows = db.conn().execute(
            """
            SELECT id, anchor_kind, anchor_payload, thread_kind, label, state,
                   created_by, created_at, resolved_at, resolved_by
              FROM threads
             WHERE rfc_slug = ? AND branch_name IS NULL
             ORDER BY id
            """,
            (slug,),
        ).fetchall()
        return {"items": [_serialize_thread(r) for r in rows]}

    # -------------------------------------------------------------------
    # POST /api/rfcs/<slug>/discussion/threads
    # Open a fresh discussion thread. Writes require require_contributor
    # — anonymous viewers can read but cannot open a thread, per item
    # #4's hardening anticipated in v0.6.0 (we already enforce it here
    # to avoid the open window).
    # -------------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/discussion/threads")
    async def create_discussion_thread(
        slug: str, body: DiscussionThreadCreateBody, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        _require_rfc_readable(slug, viewer)
        # v0.16.0 (roadmap item #12): the per-RFC discussion is now a
        # gated surface. The platform-level `require_contributor` above
        # ensures the user is signed in + admin-granted; this layer
        # narrows further to "is this user named for this RFC?" The
        # 403 here is structurally the v0.6.0 anon-write refusal
        # extended to non-invited platform users.
        if not auth.can_discuss_rfc(viewer, slug):
            raise HTTPException(
                403,
                "This RFC's owner has not invited you to its discussion",
            )
        cur = db.conn().execute(
            """
            INSERT INTO threads
                (rfc_slug, branch_name, anchor_kind, anchor_payload,
                 thread_kind, label, created_by)
            VALUES (?, NULL, 'whole-doc', NULL, 'chat', ?, ?)
            """,
            (slug, body.label, viewer.user_id),
        )
        thread_id = cur.lastrowid
        message_id = None
        if body.message:
            message_id = chat_layer.append_user_message(
                thread_id=thread_id,
                author_user_id=viewer.user_id,
                text=body.message,
                quote=None,
            )
        return {"thread_id": thread_id, "message_id": message_id}

    # -------------------------------------------------------------------
    # GET /api/rfcs/<slug>/discussion/threads/<thread_id>/messages
    # -------------------------------------------------------------------

    @router.get("/api/rfcs/{slug}/discussion/threads/{thread_id}/messages")
    async def get_discussion_thread_messages(
        slug: str, thread_id: int, request: Request
    ) -> dict[str, Any]:
        viewer = auth.current_user(request)
        _require_rfc_readable(slug, viewer)
        thread = _require_discussion_thread(slug, thread_id)
        rows = db.conn().execute(
            """
            SELECT m.id, m.role, m.author_user_id,
                   u.gitea_login AS author_login,
                   u.display_name AS author_display,
                   m.model_id, m.text, m.quote, m.created_at
              FROM thread_messages m
         LEFT JOIN users u ON u.id = m.author_user_id
             WHERE m.thread_id = ?
             ORDER BY m.id
            """,
            (thread_id,),
        ).fetchall()
        # Roadmap #28 Part 1: enrich each discussion comment with RFC
        # auto-link segments scanned against the live accepted-RFC corpus
        # (read-time; see rfc_links.py). exclude_slug suppresses self-links
        # to this RFC inside its own discussion.
        link_index = rfc_links.build_index(db.conn(), exclude_slug=slug)
        messages = [_serialize_message(r) for r in rows]
        for m in messages:
            m["text_segments"] = link_index.segment(m["text"])
        return {
            "thread": _serialize_thread(thread),
            "messages": messages,
        }

    # -------------------------------------------------------------------
    # POST /api/rfcs/<slug>/discussion/threads/<thread_id>/messages
    # -------------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/discussion/threads/{thread_id}/messages")
    async def post_discussion_message(
        slug: str, thread_id: int, body: DiscussionMessageBody, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        _require_rfc_readable(slug, viewer)
        # v0.16.0 (item #12): same per-RFC gate as create_discussion_thread.
        if not auth.can_discuss_rfc(viewer, slug):
            raise HTTPException(
                403,
                "This RFC's owner has not invited you to its discussion",
            )
        _require_discussion_thread(slug, thread_id)
        message_id = chat_layer.append_user_message(
            thread_id=thread_id,
            author_user_id=viewer.user_id,
            text=body.text,
            quote=body.quote,
        )
        return {"ok": True, "message_id": message_id}

    # -------------------------------------------------------------------
    # POST /api/rfcs/<slug>/discussion/threads/<thread_id>/resolve
    # -------------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/discussion/threads/{thread_id}/resolve")
    async def resolve_discussion_thread(
        slug: str, thread_id: int, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        rfc = _require_rfc_readable(slug, viewer)
        thread = _require_discussion_thread(slug, thread_id)
        if not _can_resolve(rfc, thread, viewer):
            raise HTTPException(
                403,
                "Only the thread creator, an RFC owner/arbiter, or an app admin/owner may resolve",
            )
        db.conn().execute(
            """
            UPDATE threads
               SET state = 'resolved',
                   resolved_by = ?,
                   resolved_at = datetime('now')
             WHERE id = ?
            """,
            (viewer.user_id, thread_id),
        )
        return {"ok": True, "thread_id": thread_id}

    return router


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _require_rfc_readable(slug: str, viewer):
    """Per the v0.3.0 anonymous-read contract: any cached RFC in a *readable*
    project is readable by anyone. The §22.5 visibility gate is subtractive on
    top (§22.7): a gated project's entries 404 to non-members. Withdrawn
    entries refuse reads of every shape — same rule `_require_rfc_with_repo`
    in `api_branches.py` follows."""
    row = db.conn().execute(
        "SELECT *, (SELECT c.project_id FROM collections c WHERE c.id = cached_rfcs.collection_id) AS project_id FROM cached_rfcs WHERE slug = ?", (slug,)
    ).fetchone()
    if row is None:
        raise HTTPException(404, "RFC not found")
    auth.require_project_readable(viewer, row["project_id"])
    if row["state"] == "withdrawn":
        raise HTTPException(409, "RFC is withdrawn")
    # §13.7: a retired entry is soft-deleted — refuse reads of every shape
    # (a 404, not a 409: the entry is not surfaced anywhere a browser looks).
    if row["state"] == "retired":
        raise HTTPException(404, "RFC not found")
    return row


def _require_discussion_thread(slug: str, thread_id: int):
    """A discussion thread is one whose (rfc_slug, branch_name) = (slug,
    NULL). Refuse cleanly if the thread id resolves to a branch-scoped
    thread instead — that lookup belongs on the branch endpoints."""
    row = db.conn().execute(
        """
        SELECT * FROM threads
         WHERE id = ? AND rfc_slug = ? AND branch_name IS NULL
        """,
        (thread_id, slug),
    ).fetchone()
    if not row:
        raise HTTPException(404, "Discussion thread not found")
    return row


def _ensure_discussion_thread(slug: str, viewer) -> int:
    """Per the §8.12 lazy-create pattern, materialize a default whole-doc
    chat thread on the RFC's discussion surface on first read. Created_by
    is null when an anonymous viewer triggers creation — the thread is
    structurally owned by the RFC, not by whoever opened the view."""
    row = db.conn().execute(
        """
        SELECT id FROM threads
         WHERE rfc_slug = ? AND branch_name IS NULL
           AND anchor_kind = 'whole-doc' AND thread_kind = 'chat'
         ORDER BY id LIMIT 1
        """,
        (slug,),
    ).fetchone()
    if row:
        return row["id"]
    cur = db.conn().execute(
        """
        INSERT INTO threads
            (rfc_slug, branch_name, anchor_kind, thread_kind, label, created_by)
        VALUES (?, NULL, 'whole-doc', 'chat', NULL, ?)
        """,
        (slug, viewer.user_id if viewer else None),
    )
    return cur.lastrowid


def _can_resolve(rfc, thread, viewer) -> bool:
    if viewer is None:
        return False
    if auth.is_collection_superuser(viewer, rfc["collection_id"]):
        return True
    owners = json.loads(rfc["owners_json"] or "[]")
    arbiters = json.loads(rfc["arbiters_json"] or "[]")
    if viewer.gitea_login in owners or viewer.gitea_login in arbiters:
        return True
    if thread["created_by"] == viewer.user_id:
        return True
    return False


# ---------------------------------------------------------------------------
# Serializers — mirror api_branches.py's shape
# ---------------------------------------------------------------------------


def _serialize_thread(row) -> dict[str, Any]:
    payload = row["anchor_payload"]
    try:
        anchor = json.loads(payload) if payload else None
    except Exception:
        anchor = None
    return {
        "id": row["id"],
        "anchor_kind": row["anchor_kind"],
        "anchor_payload": anchor,
        "thread_kind": row["thread_kind"],
        "label": row["label"],
        "state": row["state"],
        "created_by": row["created_by"],
        "created_at": row["created_at"],
        "resolved_at": row["resolved_at"] if "resolved_at" in row.keys() else None,
        "resolved_by": row["resolved_by"] if "resolved_by" in row.keys() else None,
    }


def _serialize_message(row) -> dict[str, Any]:
    return {
        "id": row["id"],
        "role": row["role"],
        "author_user_id": row["author_user_id"],
        "author_login": row["author_login"],
        "author_display": row["author_display"],
        "model_id": row["model_id"],
        "text": row["text"],
        "quote": row["quote"],
        "created_at": row["created_at"],
    }
