================================================================================
        TAG App - Anonymous Ephemeral Chat (tap-a-tag inside a TagSpace)
                        Developer Documentation
================================================================================

1. INTRODUCTION
   Inside a TagSpace, members appear to each other only by an anonymous
   space_tag_code, never their real name. This feature lets one member tap
   another member's tag code to start a temporary 1:1 chat — but only if
   that member is still actively in the space right now, the chat stays
   anonymous by default (tag codes only, no auto-revealed identity), and
   closing it leaves nothing behind: the whole chat/session disappears, not
   just its messages.

   This is a different architecture from both direct calls (1:1 WebRTC +
   Firestore signaling) and group rooms (Metered SFU): the ephemeral chat
   has NO server-side persistence at all. The PHP backend only checks
   presence/permissions and sends one push notification; the entire chat
   lives and dies in a Firestore document the client owns completely.

2. SYSTEM OVERVIEW
   - PHP/MySQL: presence + block checks only, via the existing
     tag_space_sessions table. Nothing about the chat itself is stored.
   - Firestore: the actual chat — one document per chat, created/deleted
     entirely by the client. No backend code touches it.
   - FCM: one data-only push to notify the target that someone wants to
     chat, carrying only the caller's tag code (never their real identity).

3. API ENDPOINT

   POST /api/v1/tag-spaces/{uuid}/members/{spaceTagCode}/ephemeral-chat
     Requires JWT. Caller must be the space creator or have an active
     session in the space (same rule as the existing GET .../members/{code}
     DM-resolve endpoint).

     Success (200): { "chat_id": "<uuid>", "target_tag_code": "<code>" }
       The caller already knows target_tag_code (they tapped it) — it's
       echoed back for convenience. chat_id is what both sides use to open
       the Firestore document (see §4).

     Errors:
       404 "Tag space not found."
       403 "You must be a member to start a chat."
       404 "This person is not in the space at the moment."
            — returned both when the code is invalid AND when it belongs
              to a member who has since left (is_active=0) — the server
              deliberately doesn't distinguish these to avoid leaking
              whether a code ever existed.
       400 "You cannot start a chat with yourself."
       403 "Unable to start a chat with this person." (either side blocked)

   NOTE: the existing GET /api/v1/tag-spaces/{uuid}/members/{spaceTagCode}
   endpoint (full identity reveal, for the normal "start a real DM" flow)
   was ALSO fixed to require the target still be an active member — tapping
   a stale/left member's code now correctly 404s there too, instead of
   silently resolving them.

   GET /api/v1/tag-spaces/{uuid}/ephemeral-chat-invites
     Requires JWT, same membership rule as above. Lists pending (unexpired)
     invites addressed to the caller in this space — backs an in-app panel
     so an invite is discoverable even if its push was missed. The FCM
     push (§5) is now a courtesy notification only; this endpoint is the
     source of truth.
     Success (200): { "invites": [ { "chat_id", "from_tag_code",
       "created_at" }, ... ] } — never real identity, same rule as the
       chat itself. Invites expire 10 minutes after creation (this is a
       near-real-time signal, not a durable request) and are removed by a
       cron sweep (scripts/call_sweep.php) if never acted on.

   DELETE /api/v1/tag-spaces/{uuid}/ephemeral-chat-invites/{chatId}
     Requires JWT. Dismisses one of the caller's own pending invites (e.g.
     after opening the chat, or explicitly ignoring it) so it stops
     appearing in the panel. Idempotent — succeeds whether or not a
     matching row existed; scoped to the caller, so it cannot dismiss
     someone else's invite.
     Success (200): generic "Invite dismissed."

4. FIRESTORE CONTRACT (client-owned — no backend code touches this)
   Collection: ephemeral_chats/{chatId}
     Document fields:
       - participant_a_uid, participant_b_uid  (real user IDs — used ONLY
         to scope Firestore security rules so each side can read/write
         their own chat; the UI must NEVER render these as identity)
       - space_id       (the TagSpace uuid)
       - created_at
       - expires_at     (recommend now + 24h — see §6)

     Subcollection: messages/{messageId}
       - sender_uid
       - body
       - sent_at
       - type: "text" (default) | "contact_share"
           A "contact_share" message is the ONLY place real identity may
           appear — sender_uid's own full_name/avatar_url/public_tag_id,
           written only when that user explicitly taps "Share my contact".
           Until that happens, both sides only ever see each other's
           space_tag_code (already known client-side from the tap /
           from_tag_code push payload — see §5).

   Display rule: render participants by tag code only. Only render a real
   name/avatar if a contact_share message exists from that sender.

5. PUSH NOTIFICATION PAYLOAD (ephemeral_chat_invite)
   Data-only FCM push to the target when someone starts a chat with them:
   {
     "type": "ephemeral_chat_invite",
     "chat_id": "<uuid>",
     "space_id": "<tag space uuid>",
     "from_tag_code": "<caller's own space_tag_code in this space>"
   }
   Deliberately excludes the caller's real name/avatar — the target's app
   should open the ephemeral chat screen showing the caller only by
   from_tag_code, same anonymity as the caller sees on their side.

   This push is now a courtesy, real-time nudge only — the same invite is
   also persisted for 10 minutes and retrievable via GET .../ephemeral-
   chat-invites (§3) for an in-app panel, so a missed/disabled push
   doesn't mean a missed invite.

6. CLOSING THE CHAT (true ephemerality)
   There is no backend "close"/"leave" endpoint for this feature — the
   client closes a chat by deleting the ephemeral_chats/{chatId} document
   and its messages subcollection directly via the Firestore SDK. Because
   Firestore listeners fire on deletion, the other participant's screen
   updates automatically when either side closes — no extra push needed.

   As a safety net against a crashed/killed client that never explicitly
   closes, configure a native Firestore TTL policy on the expires_at field
   for the ephemeral_chats collection (Firebase Console → Firestore →
   TTL policies). This is an operational setup step, not code — flagging
   it so it doesn't get missed before this ships.

7. GRADUATING TO A REAL CONVERSATION
   If both sides want to keep talking after a contact_share, use the
   EXISTING conversation endpoint with one new optional field:

   POST /api/v1/conversations
   Body: { "recipient_id": <int, the real user_id revealed via
           contact_share>, "space_uuid": "<tag space uuid>" (optional) }

   When space_uuid is present and resolves to a real space, the created
   conversation is tagged source_type='space_dm' with source_space_id set
   (private_conversations table already has these columns) — useful later
   for product analytics on how many real contacts originate from anonymous
   space encounters. Omitting space_uuid behaves exactly as before
   (source_type='direct').

8. FLUTTER INTEGRATION NOTES
   - Tapping a tag code: call POST .../ephemeral-chat instead of the old
     GET .../members/{code} resolve-and-DM flow when the intent is "chat
     anonymously first." Use GET .../members/{code} only for flows that
     are meant to reveal identity immediately (existing behavior,
     unchanged apart from the presence fix).
   - On 404 "This person is not in the space at the moment.", show that
     message directly rather than a generic error — it's already
     user-facing copy.
   - Use cloud_firestore directly for the chat itself, same package
     already used for direct-call signaling, just a different collection.
   - Implement "Share my contact" as a button that writes one
     contact_share message containing the user's own already-known local
     profile data — no API call needed, it's a Firestore write.
   - Implement "close chat" as a Firestore document delete (and ideally
     delete the messages subcollection first, since Firestore doesn't
     cascade-delete subcollections automatically).

9. TESTING
   API testing with Postman/cURL:
   - Obtain JWT via /auth/login for two users both active in the same
     space.
   - POST .../members/{activeUserCode}/ephemeral-chat -> 200 + chat_id;
     confirm target receives the ephemeral_chat_invite push.
   - Have the target leave the space, then repeat -> 404 "not in the space
     at the moment."
   - Attempt against your own tag code -> 400.
   - Block the target first, then attempt -> 403.
   - Confirm GET .../members/{code} (existing endpoint) still succeeds for
     an active member (no regression) and now also 404s for a left member.
   - POST /conversations with space_uuid set -> verify source_type and
     source_space_id land correctly; omit it -> still defaults to 'direct'.
   - After starting a chat, GET .../ephemeral-chat-invites as the target ->
     confirm the invite appears with from_tag_code only.
   - DELETE .../ephemeral-chat-invites/{chatId} as the target -> confirm
     it's gone on the next GET; as a different user -> confirms it's a
     no-op, doesn't remove someone else's invite.
   - Backdate a row's expires_at directly in the DB, run
     scripts/call_sweep.php, confirm the row is removed and logged.

   Integration test:
   - Two real devices in the same space, tap each other's tag, exchange a
     few messages, one side taps "share contact", confirm only THAT side's
     identity becomes visible to the other, then close and confirm the
     Firestore document is fully gone (not just cleared) on both devices.

================================================================================
                         End of Documentation
================================================================================
