package handler

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/flowy-live/llink/internal/utils/flog"

	"cloud.google.com/go/firestore"
	"github.com/livekit/protocol/webhook"

	"github.com/flowy-live/llink/internal/auth"
	"github.com/flowy-live/llink/internal/billing"
	"github.com/flowy-live/llink/internal/depot"
	"github.com/flowy-live/llink/internal/human"
	"github.com/flowy-live/llink/internal/human/pushnotify"
	"github.com/flowy-live/llink/internal/livekit"
	"github.com/flowy-live/llink/internal/middleware"
	"github.com/flowy-live/llink/internal/network"
	"github.com/flowy-live/llink/internal/particle"
	"github.com/flowy-live/llink/internal/utils"
	"github.com/flowy-live/llink/internal/waitlist"
)

type Handler struct {
	authSvc         auth.AuthService
	humanSvc        human.Service
	networkSvc      network.Service
	particleSvc     particle.Service
	depotSvc        depot.Service
	waitlistSvc     waitlist.Service
	billingSvc      billing.Service
	pushTokenSvc    pushnotify.Service
	livekitClient   livekit.Client
	firestoreClient *firestore.Client
}

func NewHandler(
	authSvc auth.AuthService,
	humanSvc human.Service,
	networkSvc network.Service,
	particleSvc particle.Service,
	depotSvc depot.Service,
	waitlistSvc waitlist.Service,
	billingSvc billing.Service,
	pushTokenSvc pushnotify.Service,
	livekitClient livekit.Client,
	firestoreClient *firestore.Client,
) *Handler {
	return &Handler{
		authSvc:         authSvc,
		humanSvc:        humanSvc,
		networkSvc:      networkSvc,
		particleSvc:     particleSvc,
		depotSvc:        depotSvc,
		waitlistSvc:     waitlistSvc,
		billingSvc:      billingSvc,
		pushTokenSvc:    pushTokenSvc,
		livekitClient:   livekitClient,
		firestoreClient: firestoreClient,
	}
}

// Response DTOs

type Human struct {
	Id                        string    `json:"id"`
	Email                     string    `json:"email"`
	EmailPrefix               string    `json:"email_prefix"`
	EmailNotificationsEnabled bool      `json:"email_notifications_enabled"`
	AvatarObjectID            *string   `json:"avatar_object_id"`
	CreatedAt                 time.Time `json:"created_at"`
}

type Network struct {
	Id         string    `json:"id"`
	Name       string    `json:"name"`
	AdminHuman Human     `json:"admin_human"`
	Humans     []Human   `json:"humans"`
	CreatedAt  time.Time `json:"created_at"`
}

// Auth Request/Response DTOs

type RequestSignInCodeRequest struct {
	Email string `json:"email"`
}

type SignInRequest struct {
	Email string `json:"email"`
	Code  string `json:"code"`
}

type SignInResponse struct {
	Human Human  `json:"human"`
	Token string `json:"token"`
}

type FirebaseTokenResponse struct {
	Token string `json:"token"`
}

// Network Request DTOs

type CreateNetworkRequest struct {
	Name string `json:"name"`
}

type AddMembersToNetworkRequest struct {
	EmailAddresses []string `json:"email_addresses"`
}

type MembersRequest struct {
	Emails []string `json:"emails"`
}

type Invitation struct {
	NetworkId   string    `json:"network_id"`
	NetworkName string    `json:"network_name"`
	Email       string    `json:"email"`
	CreatedAt   time.Time `json:"created_at"`
}

type AcceptInvitationRequest struct {
	NetworkId string `json:"network_id"`
}

type RevokeInvitationRequest struct {
	Email string `json:"email"`
}

// LiveKit DTOs

type GetLivekitTokenRequest struct {
	NetworkId string `json:"network_id"`
	StreamId  string `json:"stream_id"`
}

type GetLivekitTokenResponse struct {
	Token     string `json:"token"`
	ServerUrl string `json:"server_url"`
}

// Depot DTOs

type PrepareUploadRequest struct {
	NetworkId     string `json:"network_id"`
	Name          string `json:"name"`
	ContentType   string `json:"content_type"`
	ContentLength int64  `json:"content_length"`
}

type PrepareUploadResponse struct {
	ObjectID      string            `json:"object_id"`
	UploadURL     string            `json:"upload_url"`
	UploadHeaders map[string]string `json:"upload_headers"`
}

type DepotObject struct {
	ID              string    `json:"id"`
	Name            string    `json:"name"`
	ContentType     string    `json:"content_type"`
	ContentLength   int64     `json:"content_length"`
	ContainsContent bool      `json:"contains_content"`
	DownloadURL     string    `json:"download_url,omitempty"`
	CreatedAt       time.Time `json:"created_at"`
}

// ============================================================================
// Auth Handlers
// ============================================================================

// RequestSignInCode auto-creates the human if missing, then emails a one-time code.
func (h *Handler) RequestSignInCode(w http.ResponseWriter, r *http.Request) {
	var req RequestSignInCodeRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.Email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	_, err := h.humanSvc.GetOrCreateByEmail(r.Context(), req.Email)
	if err != nil {
		flog.Error("failed to get or create human", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if err := h.authSvc.RequestSignInCode(r.Context(), req.Email); err != nil {
		flog.Error("failed to request sign-in code", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
	var req SignInRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.Email == "" || req.Code == "" {
		http.Error(w, "email and code are required", http.StatusBadRequest)
		return
	}

	// humanId is captured into the session so later requests don't re-resolve email → id.
	hum, err := h.humanSvc.GetByEmail(r.Context(), req.Email)
	if err != nil {
		if errors.Is(err, human.ErrNotFound) {
			http.Error(w, "human not found", http.StatusNotFound)
			return
		}
		flog.Error("failed to get human for sign-in", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	token, err := h.authSvc.VerifySignInCode(r.Context(), req.Email, req.Code, hum.ID)
	if err != nil {
		if errors.Is(err, auth.ErrInvalidCode) {
			http.Error(w, "invalid code", http.StatusUnauthorized)
			return
		}
		flog.Error("failed to verify sign-in code", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := SignInResponse{
		Human: humanToDTO(hum),
		Token: token,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// FirebaseToken mints a custom token so the client can signInWithCustomToken
// and have request.auth.uid populated in Firestore security rules.
func (h *Handler) FirebaseToken(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	token, err := h.authSvc.MintFirebaseCustomToken(r.Context(), humanId)
	if err != nil {
		flog.Error("failed to mint Firebase custom token", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(FirebaseTokenResponse{Token: token})
}

func (h *Handler) SignOut(w http.ResponseWriter, r *http.Request) {
	token := extractBearerToken(r)
	if token == "" {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	if err := h.authSvc.SignOut(r.Context(), token); err != nil {
		flog.Error("failed to sign out", "error", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) GetCurrentHuman(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	hum, err := h.humanSvc.GetByEmail(r.Context(), email)
	if err != nil {
		if errors.Is(err, human.ErrNotFound) {
			http.Error(w, "human not found", http.StatusNotFound)
			return
		}
		flog.Error("failed to get current human", "error", err, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	dto := humanToDTO(hum)
	json.NewEncoder(w).Encode(dto)
}

type UpdateSettingsRequest struct {
	EmailNotificationsEnabled *bool `json:"email_notifications_enabled"`
}

func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req UpdateSettingsRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.EmailNotificationsEnabled != nil {
		if err := h.humanSvc.UpdateEmailNotificationsEnabled(r.Context(), humanId, *req.EmailNotificationsEnabled); err != nil {
			flog.Error("failed to update email notifications setting", "error", err, "humanId", humanId)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	human, err := h.humanSvc.GetByID(r.Context(), humanId)
	if err != nil {
		flog.Error("failed to get human by id", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	err = h.humanSvc.DeleteAvatar(r.Context(), humanId)
	if err != nil {
		flog.Error("failed to delete avatar from human", "error", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if human.AvatarObjectID != nil {
		err = h.depotSvc.Delete(r.Context(), utils.OptionalString(human.AvatarObjectID))
		if err != nil {
			flog.Error("failed to delete object", "error", err, "objectID", human.AvatarObjectID)
		}
	}

	w.WriteHeader(http.StatusNoContent)
	return
}

func (h *Handler) UpdateAvatar(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	// 5MB limit = 5 * 1024 * 1024 bytes
	const maxBodySize = 5 << 20

	r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)

	object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
		Prefix:      "avatars",
		Name:        fmt.Sprintf("%s-avatar", humanId),
		ContentType: r.Header.Get("Content-Type"),
	}, r.Body)
	if err != nil {
		var maxErr *http.MaxBytesError
		if errors.As(err, &maxErr) {
			http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
			return
		}
		flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
	if err != nil {
		flog.Error("failed to update human avatar", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)

		// best effort
		err = h.depotSvc.Delete(r.Context(), object.ID)
		if err != nil {
			flog.Error("best-effort delete of object failed", "error", err)
		}

		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// ============================================================================
// Network Handlers
// ============================================================================

func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req CreateNetworkRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	net, err := h.networkSvc.Create(r.Context(), req.Name, humanId)
	if err != nil {
		if errors.Is(err, network.ErrInvalidName) {
			http.Error(w, "name cannot be empty", http.StatusBadRequest)
			return
		}
		flog.Error("failed to create network", "error", err, "humanId", humanId, "name", req.Name)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp, err := h.networkToDTO(r.Context(), net)
	if err != nil {
		flog.Error("failed to convert network to DTO", "error", err, "network_id", net.ID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(resp)
}

func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networks, err := h.networkSvc.ListForHuman(r.Context(), humanId)
	if err != nil {
		flog.Error("failed to list networks", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := make([]Network, 0, len(networks))
	for _, net := range networks {
		dto, err := h.networkToDTO(r.Context(), net)
		if err != nil {
			flog.Warn("failed to convert network to DTO in list", "error", err, "network_id", net.ID)
			continue
		}
		resp = append(resp, dto)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networkID := r.PathValue("id")
	if networkID == "" {
		http.Error(w, "network id is required", http.StatusBadRequest)
		return
	}

	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
	if err != nil {
		flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if !isMember {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	net, err := h.networkSvc.GetByID(r.Context(), networkID)
	if err != nil {
		if errors.Is(err, network.ErrNotFound) {
			http.Error(w, "network not found", http.StatusNotFound)
			return
		}
		flog.Error("failed to get network", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp, err := h.networkToDTO(r.Context(), net)
	if err != nil {
		flog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// AddMembersToNetwork routes registered users into membership and emails an
// invitation to the rest.
func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networkID := r.PathValue("id")
	if networkID == "" {
		http.Error(w, "network id is required", http.StatusBadRequest)
		return
	}

	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
	if err != nil {
		flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if !isMember {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	var req AddMembersToNetworkRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if len(req.EmailAddresses) == 0 {
		http.Error(w, "email addresses are required", http.StatusBadRequest)
		return
	}

	var memberHumanIds []string
	var inviteEmails []string
	for _, email := range req.EmailAddresses {
		normalized, err := utils.NormalizeEmail(email)
		if err != nil {
			http.Error(w, "invalid email: "+email, http.StatusBadRequest)
			return
		}
		hum, err := h.humanSvc.GetByEmail(r.Context(), normalized)
		if err != nil {
			if errors.Is(err, human.ErrNotFound) {
				inviteEmails = append(inviteEmails, normalized)
				continue
			}
			flog.Error("failed to look up human by email", "error", err, "email", normalized)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
		memberHumanIds = append(memberHumanIds, hum.ID)
	}

	if len(memberHumanIds) > 0 {
		if err := h.networkSvc.AddMembers(r.Context(), networkID, memberHumanIds); err != nil {
			flog.Error("failed to add members to network", "error", err, "network_id", networkID)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
	}

	if len(inviteEmails) > 0 {
		if err := h.networkSvc.InviteByEmail(r.Context(), networkID, inviteEmails); err != nil {
			flog.Error("failed to invite members to network", "error", err, "network_id", networkID)
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}
	}

	net, err := h.networkSvc.GetByID(r.Context(), networkID)
	if err != nil {
		flog.Error("failed to get network after adding members", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp, err := h.networkToDTO(r.Context(), net)
	if err != nil {
		flog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// RemoveMemberFromNetwork is admin-only. Admins cannot remove themselves
// (would orphan networks.admin_human_id); removing a non-member is a no-op (204).
func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request) {
	net, _, ok := h.loadNetworkForAdmin(w, r)
	if !ok {
		return
	}

	memberHumanId := r.PathValue("humanId")
	if memberHumanId == "" {
		http.Error(w, "member humanId is required", http.StatusBadRequest)
		return
	}

	if memberHumanId == net.AdminHumanId {
		http.Error(w, "admin cannot remove themselves", http.StatusConflict)
		return
	}

	if err := h.networkSvc.RemoveMember(r.Context(), net.ID, memberHumanId); err != nil {
		flog.Error("failed to remove member from network", "error", err, "network_id", net.ID, "memberHumanId", memberHumanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ListInvitationsForNetwork(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networkID := r.PathValue("id")
	if networkID == "" {
		http.Error(w, "network id is required", http.StatusBadRequest)
		return
	}

	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
	if err != nil {
		flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if !isMember {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	invitations, err := h.networkSvc.ListInvitationsForNetwork(r.Context(), networkID)
	if err != nil {
		flog.Error("failed to list invitations", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := make([]Invitation, 0, len(invitations))
	for _, inv := range invitations {
		resp = append(resp, Invitation{
			NetworkId:   inv.NetworkID,
			NetworkName: inv.NetworkName,
			Email:       inv.Email,
			CreatedAt:   inv.CreatedAt,
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func (h *Handler) ListMyInvitations(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	invitations, err := h.networkSvc.ListInvitationsForEmail(r.Context(), email)
	if err != nil {
		flog.Error("failed to list invitations for email", "error", err, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := make([]Invitation, 0, len(invitations))
	for _, inv := range invitations {
		resp = append(resp, Invitation{
			NetworkId:   inv.NetworkID,
			NetworkName: inv.NetworkName,
			Email:       inv.Email,
			CreatedAt:   inv.CreatedAt,
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req AcceptInvitationRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.NetworkId == "" {
		http.Error(w, "network_id is required", http.StatusBadRequest)
		return
	}

	if err := h.networkSvc.AcceptInvitation(r.Context(), req.NetworkId, email, humanId); err != nil {
		flog.Error("failed to accept invitation", "error", err, "network_id", req.NetworkId, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) RevokeInvitation(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networkID := r.PathValue("id")
	if networkID == "" {
		http.Error(w, "network id is required", http.StatusBadRequest)
		return
	}

	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
	if err != nil {
		flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if !isMember {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	var req RevokeInvitationRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.Email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	if err := h.networkSvc.RevokeInvitation(r.Context(), networkID, req.Email); err != nil {
		flog.Error("failed to revoke invitation", "error", err, "network_id", networkID, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// GetObjectDownloadUrl returns a fresh signed URL for media/file particles.
func (h *Handler) GetObjectDownloadUrl(w http.ResponseWriter, r *http.Request) {
	_, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	// TODO: integrate firebase to fetch particle, and verify visibility for this particle's media

	objectID := r.PathValue("id")

	downloadURL, err := h.depotSvc.GetDownloadURL(r.Context(), objectID)
	if err != nil {
		flog.Error("failed to get download URL", "error", err, "object_id", objectID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"url": downloadURL})
}

// ============================================================================
// Depot Handlers
// ============================================================================

// PrepareUpload returns a signed URL for direct upload to GCS.
func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req PrepareUploadRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.NetworkId == "" {
		http.Error(w, "network_id is required", http.StatusBadRequest)
		return
	}

	isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, humanId)
	if err != nil {
		flog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	if !isMember {
		http.Error(w, "access denied", http.StatusForbidden)
		return
	}

	input := depot.PrepareUploadInput{
		Prefix:        req.NetworkId,
		Name:          req.Name,
		ContentType:   req.ContentType,
		ContentLength: req.ContentLength,
	}

	result, err := h.depotSvc.PrepareUpload(r.Context(), input)
	if err != nil {
		if errors.Is(err, depot.ErrInvalidInput) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		flog.Error("failed to prepare upload", "error", err, "network_id", req.NetworkId, "name", req.Name)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := PrepareUploadResponse{
		ObjectID:      result.ObjectID,
		UploadURL:     result.UploadURL,
		UploadHeaders: result.UploadHeaders,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) {
	_, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	objectID := r.PathValue("id")
	if objectID == "" {
		http.Error(w, "object id is required", http.StatusBadRequest)
		return
	}

	obj, err := h.depotSvc.ConfirmUpload(r.Context(), objectID)
	if err != nil {
		if errors.Is(err, depot.ErrNotFound) {
			http.Error(w, "object not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, depot.ErrInvalidInput) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		flog.Error("failed to confirm upload", "error", err, "object_id", objectID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := DepotObject{
		ID:              obj.ID,
		Name:            obj.Name,
		ContentType:     obj.ContentType,
		ContentLength:   obj.ContentLength,
		ContainsContent: obj.ContainsContent,
		DownloadURL:     "",
		CreatedAt:       obj.CreatedAt,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// ============================================================================
// Waitlist DTOs
// ============================================================================

type AddToWaitlistRequest struct {
	Email    string            `json:"email"`
	Metadata map[string]string `json:"metadata"`
}

type WaitlistEntryResponse struct {
	Id        int               `json:"id"`
	Email     string            `json:"email"`
	Metadata  map[string]string `json:"metadata"`
	CreatedAt time.Time         `json:"created_at"`
	InvitedAt *time.Time        `json:"invited_at,omitempty"`
}

type InviteWaitlistEntrantRequest struct {
	Email string `json:"email"`
}

// ============================================================================
// Waitlist Handlers
// ============================================================================

// AddToWaitlist is public — no auth required.
func (h *Handler) AddToWaitlist(w http.ResponseWriter, r *http.Request) {
	var req AddToWaitlistRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.Email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	err := h.waitlistSvc.AddToWaitlist(r.Context(), req.Email, req.Metadata)
	if err != nil {
		if errors.Is(err, waitlist.AlreadyInWaitlistError) {
			http.Error(w, "already in the waitlist", http.StatusConflict)
			return
		}
		flog.Error("failed to add to waitlist", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
}

// GetWaitlist is admin-only.
func (h *Handler) GetWaitlist(w http.ResponseWriter, r *http.Request) {
	if !middleware.IsAdminFromContext(r.Context()) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	filterParam := r.URL.Query().Get("filter")
	filter := waitlist.GetWaitlistFilterAll
	switch filterParam {
	case "invited":
		filter = waitlist.GetWaitlistFilterInvitedOnly
	case "uninvited":
		filter = waitlist.GetWaitlistFilterUninvitedOnly
	}

	entries, err := h.waitlistSvc.GetWaitlist(r.Context(), filter)
	if err != nil {
		flog.Error("failed to get waitlist", "error", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := make([]WaitlistEntryResponse, 0, len(entries))
	for _, e := range entries {
		resp = append(resp, waitlistEntryToDTO(e))
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

// GetWaitlistEntry is admin-only.
func (h *Handler) GetWaitlistEntry(w http.ResponseWriter, r *http.Request) {
	if !middleware.IsAdminFromContext(r.Context()) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	email := r.PathValue("email")
	if email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	entry, err := h.waitlistSvc.GetWaitlistEntryByEmail(r.Context(), email)
	if err != nil {
		if errors.Is(err, waitlist.EntryNotFoundError) {
			http.Error(w, "entry not found", http.StatusNotFound)
			return
		}
		flog.Error("failed to get waitlist entry", "error", err, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(waitlistEntryToDTO(entry))
}

// InviteWaitlistEntrant is admin-only.
func (h *Handler) InviteWaitlistEntrant(w http.ResponseWriter, r *http.Request) {
	if !middleware.IsAdminFromContext(r.Context()) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	var req InviteWaitlistEntrantRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.Email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	if err := h.waitlistSvc.MarkWaitlistEntryInvited(r.Context(), req.Email); err != nil {
		if errors.Is(err, waitlist.EntryNotFoundError) {
			http.Error(w, "entry not found", http.StatusNotFound)
			return
		}
		flog.Error("failed to invite waitlist entrant", "error", err, "email", req.Email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func waitlistEntryToDTO(e *waitlist.WaitlistEntry) WaitlistEntryResponse {
	return WaitlistEntryResponse{
		Id:        e.Id,
		Email:     e.Email,
		Metadata:  e.Metadata,
		CreatedAt: e.CreatedAt,
		InvitedAt: e.InvitedAt,
	}
}

// ============================================================================
// Helper Functions
// ============================================================================

func humanToDTO(h *human.Human) Human {
	return Human{
		Id:                        h.ID,
		Email:                     h.Email,
		EmailPrefix:               h.EmailPrefix,
		EmailNotificationsEnabled: h.EmailNotificationsEnabled,
		AvatarObjectID:            h.AvatarObjectID,
		CreatedAt:                 h.CreatedAt,
	}
}

func (h *Handler) networkToDTO(ctx context.Context, n *network.Network) (Network, error) {
	adminHuman, err := h.humanSvc.GetByID(ctx, n.AdminHumanId)
	if err != nil {
		return Network{}, err
	}

	humans := make([]Human, 0, len(n.MemberHumanIds))
	for _, memberHumanId := range n.MemberHumanIds {
		hum, err := h.humanSvc.GetByID(ctx, memberHumanId)
		if err != nil {
			flog.Warn("failed to look up network member", "humanId", memberHumanId, "error", err)
			continue
		}
		humans = append(humans, humanToDTO(hum))
	}

	return Network{
		Id:         n.ID,
		Name:       n.Name,
		AdminHuman: humanToDTO(adminHuman),
		Humans:     humans,
		CreatedAt:  n.CreatedAt,
	}, nil
}

// ============================================================================
// LiveKit Handlers
// ============================================================================

func (h *Handler) GetLivekitToken(w http.ResponseWriter, r *http.Request) {
	humanId, ok := middleware.HumanIdFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	humanEmail, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req GetLivekitTokenRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request body", http.StatusBadRequest)
		return
	}

	if req.NetworkId == "" {
		http.Error(w, "network_id is required", http.StatusBadRequest)
		return
	}
	if req.StreamId == "" {
		http.Error(w, "stream_id is required", http.StatusBadRequest)
		return
	}

	// Encode both IDs in the room name so the webhook handler can resolve them.
	roomName := req.NetworkId + "/" + req.StreamId

	token, err := h.livekitClient.GetJoinToken(roomName, humanId, humanEmail)
	if err != nil {
		flog.Error("failed to generate livekit token", "error", err, "humanId", humanId, "roomName", roomName)
		http.Error(w, "failed to generate token", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(GetLivekitTokenResponse{Token: token, ServerUrl: h.livekitClient.ServerUrl()})
}

// HandleLivekitWebhook verifies the webhook signature (not user auth) and
// reconciles huddle_active_participants on the stream particle in Firestore.
func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) {
	event, err := webhook.ReceiveWebhookEvent(r, h.livekitClient.KeyProvider())
	if err != nil {
		flog.Error("failed to verify livekit webhook", "error", err)
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	eventType := event.GetEvent()
	flog.Info("received livekit webhook", "event", eventType, "room", event.GetRoom().GetName())

	switch eventType {
	case "participant_joined", "participant_left", "room_finished":
		// fall through
	default:
		w.WriteHeader(http.StatusOK)
		return
	}

	roomName := event.GetRoom().GetName()
	parts := strings.SplitN(roomName, "/", 2)
	if len(parts) != 2 {
		flog.Error("invalid room name format", "room", roomName)
		http.Error(w, "invalid room name", http.StatusBadRequest)
		return
	}
	networkId, streamId := parts[0], parts[1]
	docPath := fmt.Sprintf("networks/%s/children/%s", networkId, streamId)
	docRef := h.firestoreClient.Doc(docPath)
	ctx := r.Context()

	var participantIds []string

	if eventType == "room_finished" {
		participantIds = []string{}
	} else {
		// Authoritative list avoids drift from missed/out-of-order webhooks.
		participants, err := h.livekitClient.ListParticipants(ctx, roomName)
		if err != nil {
			flog.Error("failed to list participants", "error", err, "room", roomName)
			// 200 to suppress LiveKit retries.
			w.WriteHeader(http.StatusOK)
			return
		}

		participantIds = make([]string, 0, len(participants))
		for _, p := range participants {
			participantIds = append(participantIds, p.Identity)
		}
	}

	_, err = docRef.Update(ctx, []firestore.Update{
		{Path: "huddle_active_participants", Value: participantIds},
	})
	if err != nil {
		flog.Error("failed to update huddle participants in firestore", "error", err, "path", docPath)
	}

	w.WriteHeader(http.StatusOK)
}

func extractBearerToken(r *http.Request) string {
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" {
		return ""
	}

	const prefix = "Bearer "
	if len(authHeader) > len(prefix) && authHeader[:len(prefix)] == prefix {
		return authHeader[len(prefix):]
	}
	return ""
}
