"""v0.29.0 / roadmap #28 Part 3 — offer-to-contribute-to-a-pending-RFC.

When the #28 scanner (see ``rfc_links.py``) matches a term in submitted
PR/comment text to a **pending** RFC — a super-draft
(``cached_rfcs.state='super-draft'``: accepted as an idea, owned, with a
contribution surface, but not yet graduated to an active RFC) — the
reader is offered an "ask to contribute" popover. This module is the
backend for that flow:

  * ``GET  /api/rfcs/{slug}/contribution-target``      — what the
    contribute form needs (RFC title, owner display, the viewer's
    eligibility + whether they already have a pending ask).
  * ``POST /api/rfcs/{slug}/contribution-requests``    — submit the ask
    (who-I-am / why / optional use-case); lands a row + one §15
    notification per owner.
  * ``POST /api/rfcs/{slug}/contribution-requests/{id}/accept``  — owner:
    accept, which fires #12's owner-invite flow with the requester as the
    invitee (opening the RFC's discussion/contribution surface), then
    echoes a notification back to the requester.
  * ``POST /api/rfcs/{slug}/contribution-requests/{id}/decline`` — owner:
    decline; the request closes and the requester is notified.

"Pending" is scoped to a super-draft because that is the state with an
owner to route to, a contribution surface to open, and a row in
``cached_rfcs`` for the ``rfc_invitations`` FK the accept path reuses.
Pre-merge idea PRs are deliberately out of scope (no contribution
surface yet) — a documented future extension, mirroring the
conservative scoping in ``rfc_links.py``.
"""
from __future__ import annotations

import sqlite3
from typing import Any

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

from . import api_invitations, auth, db, notify

# Field caps — generous for free text, bounded so a request row (and the
# notification payload that carries it) can't be used to store unbounded
# blobs. Mirrors the order-of-magnitude of the propose/tag-suggest caps.
_WHO_MAX = 2000
_WHY_MAX = 4000
_USE_CASE_MAX = 4000
_TERM_MAX = 200


class ContributionRequestBody(BaseModel):
    # The term in the PR/comment text that surfaced the offer (the
    # super-draft's title/slug). Carried for the owner's context line.
    matched_term: str = Field(min_length=1, max_length=_TERM_MAX)
    who_i_am: str = Field(min_length=1, max_length=_WHO_MAX)
    why: str = Field(min_length=1, max_length=_WHY_MAX)
    use_case: str | None = Field(default=None, max_length=_USE_CASE_MAX)


def _require_super_draft(slug: str, viewer):
    """The contribute surface only operates on a *pending* RFC. 404 on
    unknown; 409 on a state that isn't a super-draft (active RFCs use the
    Part-1 link, not a contribute offer; withdrawn is closed). The §22.5
    visibility gate is subtractive: a gated project's entries 404 to
    non-members (§22.7)."""
    row = db.conn().execute(
        "SELECT slug, title, state, owners_json, proposed_by, (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"] != "super-draft":
        raise HTTPException(409, "RFC is not a pending super-draft")
    return row


def _require_request(slug: str, request_id: int):
    row = db.conn().execute(
        """
        SELECT id, rfc_slug, requester_user_id, matched_term, who_i_am, why,
               use_case, status
          FROM contribution_requests WHERE id = ? AND rfc_slug = ?
        """,
        (request_id, slug),
    ).fetchone()
    if row is None:
        raise HTTPException(404, "Contribution request not found")
    return row


def _viewer_relationship(viewer, slug: str) -> str | None:
    """Why this viewer can't *request* to contribute — or None if they can.
    Owners/admins already have the RFC; existing collaborators are already
    in. Both get a clear 409 rather than a useless self-request."""
    if auth.is_rfc_owner(viewer, slug) or auth.is_collection_superuser(viewer, auth.collection_of_rfc(slug)):
        return "You already own or administer this RFC."
    if auth.is_rfc_collaborator(viewer, slug):
        return "You're already a collaborator on this RFC."
    return None


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

    # ---------------------------------------------------------------
    # GET — what the contribute form needs to render + gate itself.
    # ---------------------------------------------------------------

    @router.get("/api/rfcs/{slug}/contribution-target")
    async def contribution_target(slug: str, request: Request) -> dict[str, Any]:
        row = db.conn().execute(
            "SELECT slug, title, state, owners_json, proposed_by, (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")
        viewer = auth.current_user(request)
        # §22.5 visibility gate (subtractive): gated → 404 to non-members.
        auth.require_project_readable(viewer, row["project_id"])

        from . import rfc_links  # local import: avoid a module import cycle

        owner = rfc_links._owner_display(db.conn(), row["owners_json"], row["proposed_by"])

        eligible = True
        reason: str | None = None
        already_requested = False
        if row["state"] != "super-draft":
            eligible, reason = False, "This RFC is no longer pending."
        elif viewer is None:
            eligible, reason = False, "Sign in to ask to contribute."
        elif viewer.permission_state != "granted":
            eligible, reason = False, "Your beta access request is in review."
        else:
            reason = _viewer_relationship(viewer, slug)
            if reason is not None:
                eligible = False
            else:
                already_requested = bool(
                    db.conn().execute(
                        """
                        SELECT 1 FROM contribution_requests
                         WHERE rfc_slug = ? AND requester_user_id = ? AND status = 'pending'
                         LIMIT 1
                        """,
                        (slug, viewer.user_id),
                    ).fetchone()
                )

        return {
            "slug": row["slug"],
            "title": row["title"],
            "owner": owner,
            "eligible": eligible and not already_requested,
            "reason": reason,
            "already_requested": already_requested,
        }

    # ---------------------------------------------------------------
    # POST — submit a contribute request.
    # ---------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/contribution-requests")
    async def create_contribution_request(
        slug: str, body: ContributionRequestBody, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        _require_super_draft(slug, viewer)

        reason = _viewer_relationship(viewer, slug)
        if reason is not None:
            raise HTTPException(409, reason)

        who_i_am = body.who_i_am.strip()
        why = body.why.strip()
        use_case = (body.use_case or "").strip() or None
        matched_term = body.matched_term.strip()
        if not who_i_am or not why:
            raise HTTPException(422, "Both 'who I am' and 'why' are required.")

        try:
            cur = db.conn().execute(
                """
                INSERT INTO contribution_requests
                    (rfc_slug, requester_user_id, matched_term, who_i_am, why, use_case)
                VALUES (?, ?, ?, ?, ?, ?)
                """,
                (slug, viewer.user_id, matched_term, who_i_am, why, use_case),
            )
        except sqlite3.IntegrityError:
            # The partial unique index — one open request per (RFC, user).
            raise HTTPException(409, "You already have a pending request to contribute to this RFC.")
        request_id = cur.lastrowid

        # One actionable notification per owner; stamp the first onto the
        # row as the inbox-action handle (any owner can act on the request).
        notif_ids = notify.fan_out_contribution_request(
            rfc_slug=slug,
            requester_user_id=viewer.user_id,
            request_id=request_id,
            matched_term=matched_term,
            who_i_am=who_i_am,
            why=why,
            use_case=use_case,
        )
        if notif_ids:
            db.conn().execute(
                "UPDATE contribution_requests SET notification_id = ? WHERE id = ?",
                (notif_ids[0], request_id),
            )

        return {"id": request_id, "rfc_slug": slug, "status": "pending"}

    # ---------------------------------------------------------------
    # POST — owner accepts → fire #12's invite flow.
    # ---------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/contribution-requests/{request_id}/accept")
    async def accept_contribution_request(
        slug: str, request_id: int, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        rfc = _require_super_draft(slug, viewer)
        if not auth.can_invite_to_rfc(viewer, slug):
            raise HTTPException(403, "Only the RFC's owner can act on contribution requests")

        req = _require_request(slug, request_id)
        if req["status"] != "pending":
            raise HTTPException(409, f"This request was already {req['status']}.")

        requester = db.conn().execute(
            "SELECT id, email FROM users WHERE id = ?", (req["requester_user_id"],)
        ).fetchone()
        if requester is None or not (requester["email"] or "").strip():
            raise HTTPException(422, "The requester has no email address on file to invite.")

        # Fire #12's owner-invite flow with the requester as the invitee.
        # If a pending contributor invitation already exists (the owner
        # invited them out-of-band first), reuse it rather than failing.
        try:
            invitation = api_invitations.issue_invitation(
                slug=slug,
                inviter_user_id=viewer.user_id,
                inviter_display=viewer.display_name or viewer.gitea_login or "An RFC owner",
                invitee_email=requester["email"],
                role_in_rfc="contributor",
                rfc_title=rfc["title"],
            )
            invitation_id = invitation["id"]
        except HTTPException as exc:
            if exc.status_code != 409:
                raise
            existing = db.conn().execute(
                """
                SELECT id FROM rfc_invitations
                 WHERE rfc_slug = ? AND invitee_email = ? COLLATE NOCASE
                   AND role_in_rfc = 'contributor' AND status = 'pending'
                 ORDER BY id DESC LIMIT 1
                """,
                (slug, requester["email"].strip()),
            ).fetchone()
            invitation_id = existing["id"] if existing else None

        db.conn().execute(
            """
            UPDATE contribution_requests
               SET status = 'accepted', decided_at = datetime('now'),
                   decided_by_user_id = ?, invitation_id = ?
             WHERE id = ?
            """,
            (viewer.user_id, invitation_id, request_id),
        )
        notify.notify_contribution_decided(
            rfc_slug=slug,
            requester_user_id=req["requester_user_id"],
            decider_user_id=viewer.user_id,
            request_id=request_id,
            accepted=True,
        )
        return {"ok": True, "status": "accepted", "invitation_id": invitation_id}

    # ---------------------------------------------------------------
    # POST — owner declines.
    # ---------------------------------------------------------------

    @router.post("/api/rfcs/{slug}/contribution-requests/{request_id}/decline")
    async def decline_contribution_request(
        slug: str, request_id: int, request: Request
    ) -> dict[str, Any]:
        viewer = auth.require_contributor(request)
        _require_super_draft(slug, viewer)
        if not auth.can_invite_to_rfc(viewer, slug):
            raise HTTPException(403, "Only the RFC's owner can act on contribution requests")

        req = _require_request(slug, request_id)
        if req["status"] != "pending":
            raise HTTPException(409, f"This request was already {req['status']}.")

        db.conn().execute(
            """
            UPDATE contribution_requests
               SET status = 'declined', decided_at = datetime('now'),
                   decided_by_user_id = ?
             WHERE id = ?
            """,
            (viewer.user_id, request_id),
        )
        notify.notify_contribution_decided(
            rfc_slug=slug,
            requester_user_id=req["requester_user_id"],
            decider_user_id=viewer.user_id,
            request_id=request_id,
            accepted=False,
        )
        return {"ok": True, "status": "declined"}

    return router
