TAG API — TAG SPACE MANAGEMENT
================================
Base URL : https://tag.nsamaandcompany.com/tag_api
Auth     : Authorization: Bearer <token>  required on all endpoints

Covers: space editing, avatar upload, unread count, member management,
        and join requests for private spaces.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LIST SPACES (with filter)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces                         [AUTH]
  Query params (optional):
    filter = "joined"    — only spaces you are a member of
    filter = "discover"  — only spaces you have NOT joined
    (omit filter for all active spaces)
  Returns: [ { id, uuid, title, description, cover_image_url, avatar_url,
               visibility, max_members, active_members, posts_count,
               is_creator, is_member, created_at } ]


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EDIT A SPACE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PATCH /api/v1/tag-spaces/{uuid}                 [AUTH]
  Edit space details. Creator only.
  Path param: uuid — the space's UUID
  Body (JSON, all fields optional — provide at least one):
    {
      "title":       "New title",
      "description": "Updated description",
      "visibility":  "public" | "private",
      "max_members": 50
    }
  Response 200: updated space object
    { id, uuid, title, description, cover_image_url, avatar_url,
      visibility, max_members }
  Errors:
    403 — caller is not the creator
    404 — space not found
    422 — no fields provided | invalid visibility value


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SPACE AVATAR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

POST /api/v1/tag-spaces/{uuid}/avatar           [AUTH]
  Upload or replace the space's circular avatar image. Creator only.
  Path param: uuid — the space's UUID
  Body: multipart/form-data   field: avatar (image file, max 5 MB)
  Response 200: { "avatar_url": "https://..." }
  Errors:
    403 — caller is not the creator
    404 — space not found
    422 — no file provided | invalid file type or size


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
UNREAD CHAT COUNT (ACROSS ALL SPACES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces/unread-count            [AUTH]
  Returns total unread chat messages across all spaces you have joined.
  Use this for the spaces tab badge.
  Response 200:
    {
      "total_unread": 12,
      "spaces": [
        {
          "uuid":         "abc-123-...",
          "title":        "Dev Chat",
          "avatar_url":   "https://...",
          "unread_count": 5
        }
      ]
    }
  Note: Only spaces with unread_count > 0 appear in the spaces array.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MEMBER LIST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces/{uuid}/members          [AUTH]
  List all active members of a space.
  Caller must be an active member OR the space creator.
  Returns: [
    {
      "space_tag_code": "1a2b3c4d5e6f",
      "joined_at":      "2026-06-01T10:00:00Z",
      "is_creator":     false
    }
  ]
  Note: Members are identified only by their anonymous space_tag_code,
        not by real name or user_id.
  Errors:
    403 — you are not a member or creator
    404 — space not found


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RESOLVE MEMBER  (deanonymise a tag code)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GET  /api/v1/tag-spaces/{uuid}/members/{code}   [AUTH]
  Resolve an anonymous space_tag_code to the real user behind it.
  Use this to open a DM with someone from the chat.
  Path params:
    uuid — the space's UUID
    code — the member's space_tag_code (e.g. "1a2b3c4d5e6f")
  Caller must be an active member or the space creator.
  Response 200:
    { user_id, public_tag_id, full_name, avatar_url }
  Errors:
    403 — you are not a member or creator
    404 — space not found | member not found


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REMOVE MEMBER  (creator only)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DELETE /api/v1/tag-spaces/{uuid}/members/{code} [AUTH]
  Remove a member from the space by their space_tag_code.
  Creator only. Creator cannot remove themselves.
  Path params:
    uuid — the space's UUID
    code — the member's space_tag_code
  Body: (none)
  Response 200: { "message": "Member removed from space." }
  Side effect: writes a Firebase RTDB event at
    chats/spaces/{uuid}/member_removed
    so other clients can react in real-time (e.g. hide that user's messages).
  Errors:
    403 — caller is not the creator
    422 — cannot remove yourself
    404 — space not found | member not found in this space


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JOIN REQUESTS  (private spaces only)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Flow:
  1. User sends a join request  →  owner approves or rejects
  2. On accept: user can now call POST /join without a join_code

--- REQUESTER SIDE ---

POST /api/v1/tag-spaces/{uuid}/join-requests    [AUTH]
  Send a request to join a private space.
  Body: (none)
  Response 201: { "request_id": 12 }
  FCM push to space owner: type = "space_join_request"
    Payload: { type, request_id, space_uuid }
  Errors:
    422 — space is public (join directly) | already a member
         | already pending | already accepted
    404 — space not found

DELETE /api/v1/tag-spaces/{uuid}/join-requests  [AUTH]
  Cancel your own pending join request for a space.
  Body: (none)
  Response 200: { "message": "Join request cancelled." }
  Errors:
    404 — no pending request found


--- OWNER SIDE ---

GET  /api/v1/tag-spaces/{uuid}/join-requests    [AUTH]
  List all PENDING join requests for a space.
  Creator only.
  Returns: [
    {
      "id": 12,
      "created_at": "2026-06-01T10:00:00Z",
      "requester": {
        "user_id": 5,
        "full_name": "John Doe",
        "public_tag_id": "johndoe",
        "avatar_url": "https://..."
      }
    }
  ]
  Errors:
    403 — caller is not the creator

PUT  /api/v1/tag-spaces/{uuid}/join-requests/{id}/accept  [AUTH]
  Accept a pending join request. Requester is notified and can now
  call POST /join without providing a join_code.
  Path param: id — the join request id
  Response 200: { "request_id": 12 }
  FCM push to requester: type = "space_join_request_accepted"
    Payload: { type, request_id, space_uuid }
  Errors:
    403 — caller is not the creator
    404 — space not found | request not found in this space
    422 — request already accepted or rejected

PUT  /api/v1/tag-spaces/{uuid}/join-requests/{id}/reject  [AUTH]
  Reject a pending join request. Requester is notified.
  Path param: id — the join request id
  Response 200: { "request_id": 12 }
  FCM push to requester: type = "space_join_request_rejected"
    Payload: { type, request_id, space_uuid }
  Errors:
    403 — caller is not the creator
    404 — space not found | request not found in this space
    422 — request already accepted or rejected


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FCM PUSH TYPES — SPACE JOIN REQUESTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

type = "space_join_request"
  Owner receives this when someone requests to join.
  Payload: { type, request_id, space_uuid }

type = "space_join_request_accepted"
  Requester receives this when their request is accepted.
  Payload: { type, request_id, space_uuid }
  On tap → navigate to space and prompt user to call POST /join.

type = "space_join_request_rejected"
  Requester receives this when their request is rejected.
  Payload: { type, request_id, space_uuid }
