package handler

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

	"github.com/flowy-live/llink/internal/auth"
	"github.com/flowy-live/llink/internal/depot"
	"github.com/flowy-live/llink/internal/human"
	"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"
)

type Handler struct {
	authSvc     auth.AuthService
	humanSvc    human.Service
	networkSvc  network.Service
	particleSvc particle.Service
	depotSvc    depot.Service
}

func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service) *Handler {
	return &Handler{
		authSvc:     authSvc,
		humanSvc:    humanSvc,
		networkSvc:  networkSvc,
		particleSvc: particleSvc,
		depotSvc:    depotSvc,
	}
}

// Response DTOs

type Human struct {
	// Id will be nil if this human is not registered
	Id          *string    `json:"id"`
	Email       string     `json:"email"`
	EmailPrefix string     `json:"email_prefix"`
	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"`
	OpenStreamCount    int       `json:"open_stream_count"`
	OpenStreamCapacity int       `json:"open_stream_capacity"`
	CreatedAt          time.Time `json:"created_at"`
}

type StreamParticle struct {
	Id             string          `json:"id"`
	Type           string          `json:"type"`
	Data           json.RawMessage `json:"data"`
	CreatedByEmail string          `json:"created_by_email"`
	Seen           bool            `json:"seen"`
	Acks           []*AckInfo      `json:"acks"`
	UpdatedAt      time.Time       `json:"updated_at"`
	CreatedAt      time.Time       `json:"created_at"`
}

type Particle struct {
	Id             string          `json:"id"`
	Type           string          `json:"type"`
	Data           json.RawMessage `json:"data"`
	CreatedByEmail string          `json:"created_by_email"`
	Visibility     string          `json:"visibility"`
	Members        []string        `json:"members,omitempty"`
	StreamStatus   *string         `json:"stream_status,omitempty"`
	Seen           bool            `json:"seen,omitempty"`
	Acks           []*AckInfo      `json:"acks,omitempty"`
	UnseenCount    *int            `json:"unseen_count,omitempty"`
	UpdatedAt      time.Time       `json:"updated_at"`
	CreatedAt      time.Time       `json:"created_at"`
}

type Stream struct {
	Id          string       `json:"id"`
	Name        string       `json:"name"`
	Description string       `json:"description"`
	Status      StreamStatus `json:"status"`
	// The emails of the members in this stream
	Members     []string          `json:"members,omitempty"`
	Particles   []*StreamParticle `json:"particles"`
	UnseenCount int               `json:"unseen_count"`
	UpdatedAt   time.Time         `json:"updated_at"`
	CreatedAt   time.Time         `json:"created_at"`
}

type StreamStatus string

const (
	STREAM_STATUS_OPEN        StreamStatus = "open"
	STREAM_STATUS_CLOSED      StreamStatus = "closed"
	STREAM_STATUS_UNSPECIFIED StreamStatus = "unspecified"
)

type NetworkWithStreams struct {
	Network
	Streams []*Stream `json:"streams"`
}

type StartData struct {
	Networks []*NetworkWithStreams `json:"networks"`
}

type AckInfo struct {
	Email   string    `json:"email"`
	AckedAt time.Time `json:"acked_at"`
}

type ParticleList struct {
	Particles  []Particle `json:"particles"`
	HasMore    bool       `json:"has_more"`
	NextCursor *string    `json:"next_cursor,omitempty"`
	PrevCursor *string    `json:"prev_cursor,omitempty"`
}

// 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"`
}

// Network Request DTOs

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

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

type SetOpenStreamCapacityRequest struct {
	Capacity int `json:"capacity"`
}

// Particle Request DTOs

type CreateStreamParticleRequest struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}

type CreateStreamRequest struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Visibility  string   `json:"visibility"`
	Members     []string `json:"members"`
}

type UpdateStreamRequest struct {
	Name        *string `json:"name"`
	Description *string `json:"description"`
}

type UpdateParticleRequest struct {
	Data json.RawMessage `json:"data"`
}

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

type MarkSeenBatchRequest struct {
	ParticleIDs []string `json:"particle_ids"`
}

// 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 creates a human account if not already existent and sends a sign-in 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
	}

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

	// Request sign-in code
	if err := h.authSvc.RequestSignInCode(r.Context(), req.Email); err != nil {
		slog.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)
}

// SignIn verifies the code and returns a session token
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
	}

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

	// Get the human
	hum, err := h.humanSvc.GetByEmail(r.Context(), req.Email)
	if err != nil {
		slog.Error("failed to get human after sign-in", "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)
}

// SignOut deletes the session from the token in headers
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 {
		slog.Error("failed to sign out", "error", err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

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

	networks, err := h.networkSvc.ListForEmail(r.Context(), email)
	if err != nil {
		http.Error(w, "unable to fetch networks", http.StatusInternalServerError)
		return
	}

	data := StartData{
		Networks: make([]*NetworkWithStreams, 0, len(networks)),
	}

	streamFilter := particle.ListFilter{
		Types: []particle.ParticleType{particle.TypeStream},
	}

	for _, net := range networks {
		networkDTO, err := h.networkToDTO(r.Context(), net)
		if err != nil {
			slog.Error("unable to convert db network to dto", "error", err, "network_id", net.ID)
			continue
		}

		nws := &NetworkWithStreams{
			Network: networkDTO,
		}

		// Fetch all top-level streams for this network
		streams, err := h.listAllParticles(r.Context(), net.ID, nil, email, streamFilter)
		if err != nil {
			slog.Error("unable to list streams for network", "error", err, "network_id", net.ID)
			data.Networks = append(data.Networks, nws)
			continue
		}

		// Collect stream IDs for unseen counts
		streamIDs := make([]string, len(streams))
		for i, s := range streams {
			streamIDs[i] = s.ID
		}

		// Get unseen counts and members for all streams in this network
		var unseenCounts map[string]int
		var streamMembersMap map[string][]string
		if len(streamIDs) > 0 {
			unseenCounts, err = h.particleSvc.GetUnseenCounts(r.Context(), net.ID, streamIDs, email)
			if err != nil {
				slog.Warn("failed to get unseen counts for streams", "error", err, "network_id", net.ID)
				unseenCounts = make(map[string]int)
			}

			streamMembersMap, err = h.particleSvc.GetMembersMap(r.Context(), streamIDs)
			if err != nil {
				slog.Warn("failed to get members map for streams", "error", err, "network_id", net.ID)
				streamMembersMap = make(map[string][]string)
			}
		}

		nws.Streams = make([]*Stream, 0, len(streams))
		for _, sp := range streams {
			// Parse stream metadata from particle data
			var streamData particle.StreamData
			if err := json.Unmarshal(sp.Data, &streamData); err != nil {
				slog.Warn("failed to parse stream data", "error", err, "particle_id", sp.ID)
			}

			status := parseStreamStatus(streamData.Status)

			stream := &Stream{
				Id:          sp.ID,
				Name:        streamData.Name,
				Description: utils.OptionalString(streamData.Description),
				Status:      status,
				Members:     h.getStreamMembers(r.Context(), sp, streamMembersMap),
				UnseenCount: unseenCounts[sp.ID],
				UpdatedAt:   sp.UpdatedAt,
				CreatedAt:   sp.CreatedAt,
			}

			// Fetch child particles for this stream
			children, err := h.listAllParticles(r.Context(), net.ID, &sp.ID, email, particle.ListFilter{})
			if err != nil {
				slog.Error("unable to list particles for stream", "error", err, "stream_id", sp.ID)
				nws.Streams = append(nws.Streams, stream)
				continue
			}

			// Collect child particle IDs for bulk enrichment
			childIDs := make([]string, len(children))
			for i, p := range children {
				childIDs[i] = p.ID
			}

			seenMap, err := h.particleSvc.GetSeenMap(r.Context(), childIDs, email)
			if err != nil {
				slog.Warn("failed to get seen map", "error", err)
				seenMap = make(map[string]bool)
			}

			acksMap, err := h.particleSvc.GetAcksMap(r.Context(), childIDs)
			if err != nil {
				slog.Warn("failed to get acks map", "error", err)
				acksMap = make(map[string][]particle.AckInfo)
			}

			// Build enriched particle DTOs
			stream.Particles = make([]*StreamParticle, 0, len(children))
			for _, p := range children {
				dto := h.streamParticleToDTO(r.Context(), p)

				if seen, ok := seenMap[p.ID]; ok {
					dto.Seen = seen
				}

				if acks, ok := acksMap[p.ID]; ok && len(acks) > 0 {
					dto.Acks = make([]*AckInfo, len(acks))
					for i, a := range acks {
						dto.Acks[i] = &AckInfo{Email: a.Email, AckedAt: a.AckedAt}
					}
				} else {
					dto.Acks = []*AckInfo{}
				}

				stream.Particles = append(stream.Particles, &dto)
			}

			nws.Streams = append(nws.Streams, stream)
		}

		data.Networks = append(data.Networks, nws)
	}

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

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

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

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

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

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

	streamData := &particle.StreamData{
		Name:        req.Name,
		Description: utils.CreateOptionalString(req.Description),
		Status:      string(particle.StreamStatusOpen),
	}
	data, err := json.Marshal(streamData)
	if err != nil {
		slog.Error("failed to marshal stream data", "error", err, "stream_name", req.Name)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
	visibilityMode, err := particle.ParseVisibilityMode(req.Visibility)
	if err != nil {
		slog.Error("invalid visibility mode", "error", err, "visibility", req.Visibility)
		http.Error(w, "invalid visibility mode", http.StatusBadRequest)
		return
	}
	params := particle.CreateInput{
		Type:       particle.TypeStream,
		NetworkID:  networkID,
		ParentID:   nil,
		Data:       data,
		Members:    req.Members,
		Visibility: visibilityMode,
	}
	created, err := h.particleSvc.Create(r.Context(), params, email)
	if err != nil {
		if errors.Is(err, particle.ErrMembersRequired) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		if errors.Is(err, particle.ErrCapacityExceeded) {
			http.Error(w, "stream capacity exceeded", http.StatusConflict)
			return
		}
		if errors.Is(err, particle.ErrInvalidData) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		slog.Error("failed to create stream particle", "error", err, "stream_name", req.Name, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Get members for the created stream
	membersMap, _ := h.particleSvc.GetMembersMap(r.Context(), []string{created.ID})

	var parsedData particle.StreamData
	_ = json.Unmarshal(created.Data, &parsedData)

	resp := Stream{
		Id:          created.ID,
		Name:        parsedData.Name,
		Description: utils.OptionalString(parsedData.Description),
		Status:      parseStreamStatus(parsedData.Status),
		Members:     h.getStreamMembers(r.Context(), created, membersMap),
		Particles:   []*StreamParticle{},
		UnseenCount: 0,
		UpdatedAt:   created.UpdatedAt,
		CreatedAt:   created.CreatedAt,
	}

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

// GetStream returns a stream with its child particles, members, and unseen count
func (h *Handler) GetStream(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	// Get the stream particle and verify access
	sp, err := h.particleSvc.GetByID(r.Context(), streamID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "stream not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get stream", "error", err, "stream_id", streamID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if sp.Type != particle.TypeStream {
		http.Error(w, "particle is not a stream", http.StatusBadRequest)
		return
	}

	// Parse stream data
	var streamData particle.StreamData
	if err := json.Unmarshal(sp.Data, &streamData); err != nil {
		slog.Warn("failed to parse stream data", "error", err, "stream_id", streamID)
	}

	// Get members
	membersMap, err := h.particleSvc.GetMembersMap(r.Context(), []string{streamID})
	if err != nil {
		slog.Warn("failed to get members for stream", "error", err, "stream_id", streamID)
		membersMap = make(map[string][]string)
	}

	// Get unseen count
	unseenCounts, err := h.particleSvc.GetUnseenCounts(r.Context(), sp.NetworkID, []string{streamID}, email)
	if err != nil {
		slog.Warn("failed to get unseen counts", "error", err, "stream_id", streamID)
		unseenCounts = make(map[string]int)
	}

	stream := &Stream{
		Id:          sp.ID,
		Name:        streamData.Name,
		Description: utils.OptionalString(streamData.Description),
		Status:      parseStreamStatus(streamData.Status),
		Members:     h.getStreamMembers(r.Context(), sp, membersMap),
		UnseenCount: unseenCounts[sp.ID],
		UpdatedAt:   sp.UpdatedAt,
		CreatedAt:   sp.CreatedAt,
	}

	// Fetch child particles
	children, err := h.listAllParticles(r.Context(), sp.NetworkID, &sp.ID, email, particle.ListFilter{})
	if err != nil {
		slog.Error("unable to list particles for stream", "error", err, "stream_id", sp.ID)
		stream.Particles = []*StreamParticle{}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(stream)
		return
	}

	// Collect child IDs for bulk enrichment
	childIDs := make([]string, len(children))
	for i, p := range children {
		childIDs[i] = p.ID
	}

	seenMap, err := h.particleSvc.GetSeenMap(r.Context(), childIDs, email)
	if err != nil {
		slog.Warn("failed to get seen map", "error", err)
		seenMap = make(map[string]bool)
	}

	acksMap, err := h.particleSvc.GetAcksMap(r.Context(), childIDs)
	if err != nil {
		slog.Warn("failed to get acks map", "error", err)
		acksMap = make(map[string][]particle.AckInfo)
	}

	stream.Particles = make([]*StreamParticle, 0, len(children))
	for _, p := range children {
		dto := h.streamParticleToDTO(r.Context(), p)

		if seen, ok := seenMap[p.ID]; ok {
			dto.Seen = seen
		}

		if acks, ok := acksMap[p.ID]; ok && len(acks) > 0 {
			dto.Acks = make([]*AckInfo, len(acks))
			for i, a := range acks {
				dto.Acks[i] = &AckInfo{Email: a.Email, AckedAt: a.AckedAt}
			}
		} else {
			dto.Acks = []*AckInfo{}
		}

		stream.Particles = append(stream.Particles, &dto)
	}

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

// UpdateStream updates a stream's name and/or description (not status)
func (h *Handler) UpdateStream(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	// Get the existing stream to preserve status and merge fields
	sp, err := h.particleSvc.GetByID(r.Context(), streamID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "stream not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get stream for update", "error", err, "stream_id", streamID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if sp.Type != particle.TypeStream {
		http.Error(w, "particle is not a stream", http.StatusBadRequest)
		return
	}

	// Parse existing data to preserve status
	var existing particle.StreamData
	if err := json.Unmarshal(sp.Data, &existing); err != nil {
		slog.Error("failed to parse existing stream data", "error", err, "stream_id", streamID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Merge: only update fields that were provided
	if req.Name != nil {
		if *req.Name == "" {
			http.Error(w, "name cannot be empty", http.StatusBadRequest)
			return
		}
		existing.Name = *req.Name
	}
	if req.Description != nil {
		existing.Description = utils.CreateOptionalString(*req.Description)
	}

	newData, err := json.Marshal(existing)
	if err != nil {
		slog.Error("failed to marshal updated stream data", "error", err, "stream_id", streamID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	updated, err := h.particleSvc.Update(r.Context(), streamID, newData, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "stream not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrInvalidData) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		slog.Error("failed to update stream", "error", err, "stream_id", streamID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Build response
	var parsedData particle.StreamData
	_ = json.Unmarshal(updated.Data, &parsedData)

	membersMap, _ := h.particleSvc.GetMembersMap(r.Context(), []string{updated.ID})

	unseenCounts, _ := h.particleSvc.GetUnseenCounts(r.Context(), updated.NetworkID, []string{updated.ID}, email)

	resp := Stream{
		Id:          updated.ID,
		Name:        parsedData.Name,
		Description: utils.OptionalString(parsedData.Description),
		Status:      parseStreamStatus(parsedData.Status),
		Members:     h.getStreamMembers(r.Context(), updated, membersMap),
		Particles:   []*StreamParticle{},
		UnseenCount: unseenCounts[updated.ID],
		UpdatedAt:   updated.UpdatedAt,
		CreatedAt:   updated.CreatedAt,
	}

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

// CreateStreamParticle creates a particle inside a stream
func (h *Handler) CreateStreamParticle(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	// Get the stream to find NetworkID and verify it's a stream
	stream, err := h.particleSvc.GetByID(r.Context(), streamID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "stream not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get stream for particle creation", "error", err, "stream_id", streamID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	if stream.Type != particle.TypeStream {
		http.Error(w, "particle is not a stream", http.StatusBadRequest)
		return
	}

	// Parse and validate particle type
	particleType, err := particle.ParseParticleType(req.Type)
	if err != nil {
		http.Error(w, "invalid particle type", http.StatusBadRequest)
		return
	}

	// Reject streams and folders as children
	if particleType == particle.TypeStream || particleType == particle.TypeFolder {
		http.Error(w, "cannot create streams or folders inside a stream", http.StatusBadRequest)
		return
	}

	// For media/file types, validate object_id exists in depot
	if req.Type == "media" || req.Type == "file" {
		var data struct {
			ObjectID string `json:"object_id"`
		}
		if err := json.Unmarshal(req.Data, &data); err == nil && data.ObjectID != "" {
			exists, err := h.depotSvc.Exists(r.Context(), data.ObjectID)
			if err != nil {
				slog.Error("failed to check depot object existence", "error", err, "object_id", data.ObjectID)
				http.Error(w, "internal server error", http.StatusInternalServerError)
				return
			}
			if !exists {
				http.Error(w, "object_id does not exist in depot", http.StatusBadRequest)
				return
			}
		}
	}

	// Service will force inherited visibility and nil members
	input := particle.CreateInput{
		Type:      particleType,
		NetworkID: stream.NetworkID,
		ParentID:  &streamID,
		Data:      req.Data,
	}

	created, err := h.particleSvc.Create(r.Context(), input, email)
	if err != nil {
		if errors.Is(err, particle.ErrInvalidType) || errors.Is(err, particle.ErrInvalidData) || errors.Is(err, particle.ErrInvalidParent) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to create stream particle", "error", err, "stream_id", streamID, "type", req.Type, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := h.streamParticleToDTO(r.Context(), created)
	resp.Acks = []*AckInfo{}

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

// GetCurrentHuman returns the authenticated human
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
		}
		slog.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)
}

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

// CreateNetwork creates a new network
func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(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, email)
	if err != nil {
		if errors.Is(err, network.ErrInvalidName) {
			http.Error(w, "name cannot be empty", http.StatusBadRequest)
			return
		}
		slog.Error("failed to create network", "error", err, "email", email, "name", req.Name)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp, err := h.networkToDTO(r.Context(), net)
	if err != nil {
		slog.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)
}

// ListNetworks retrieves networks for the authenticated human
func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networks, err := h.networkSvc.ListForEmail(r.Context(), email)
	if err != nil {
		slog.Error("failed to list networks", "error", err, "email", email)
		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 {
			slog.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)
}

// GetNetwork retrieves a specific network
func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(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
	}

	// Check membership
	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
	if err != nil {
		slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
		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
		}
		slog.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 {
		slog.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 adds members to a network
func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(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
	}

	// Check membership
	isMember, err := h.networkSvc.IsMember(r.Context(), networkID, email)
	if err != nil {
		slog.Error("failed to check network membership", "error", err, "network_id", networkID, "email", email)
		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
	}

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

	// Return updated network
	net, err := h.networkSvc.GetByID(r.Context(), networkID)
	if err != nil {
		slog.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 {
		slog.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 removes a member from a network
func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	networkID := r.PathValue("id")
	memberEmail := r.PathValue("email")
	if networkID == "" || memberEmail == "" {
		http.Error(w, "network id and member email are required", http.StatusBadRequest)
		return
	}

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

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

	w.WriteHeader(http.StatusNoContent)
}

// SetOpenStreamCapacity sets the open stream capacity for a network
func (h *Handler) SetOpenStreamCapacity(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(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
	}

	// Get network to check admin
	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
		}
		slog.Error("failed to get network for capacity update", "error", err, "network_id", networkID)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Only admin can set capacity
	if net.AdminEmail != email {
		http.Error(w, "only admin can set capacity", http.StatusForbidden)
		return
	}

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

	if err := h.networkSvc.SetOpenStreamCapacity(r.Context(), networkID, req.Capacity); err != nil {
		if errors.Is(err, network.ErrNotFound) {
			http.Error(w, "network not found", http.StatusNotFound)
			return
		}
		slog.Error("failed to set open stream capacity", "error", err, "network_id", networkID, "capacity", req.Capacity)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// ============================================================================
// Particle Handlers
// ============================================================================

// ListParticles returns particles in a network
func (h *Handler) ListParticles(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	// Parse query parameters
	parentID := r.URL.Query().Get("parent_id")
	var parentIDPtr *string
	if parentID != "" {
		parentIDPtr = &parentID
	}

	// Parse cursor
	var cursor *particle.Cursor
	cursorStr := r.URL.Query().Get("cursor")
	direction := r.URL.Query().Get("direction")
	if cursorStr != "" {
		cursor = &particle.Cursor{
			Position:  cursorStr,
			Direction: direction,
		}
		if cursor.Direction == "" {
			cursor.Direction = "after"
		}
	}

	// Parse type filter
	filter := particle.ListFilter{}
	typeFilter := r.URL.Query()["type"]
	for _, t := range typeFilter {
		pt, err := particle.ParseParticleType(t)
		if err != nil {
			http.Error(w, "invalid particle type: "+t, http.StatusBadRequest)
			return
		}
		filter.Types = append(filter.Types, pt)
	}

	list, err := h.particleSvc.List(r.Context(), networkID, parentIDPtr, email, filter, cursor, 50)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "parent not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to list particles", "error", err, "network_id", networkID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Extract particle IDs for enrichment
	particleIDs := make([]string, len(list.Particles))
	streamIDs := make([]string, 0)
	for i, p := range list.Particles {
		particleIDs[i] = p.ID
		if p.Type == particle.TypeStream {
			streamIDs = append(streamIDs, p.ID)
		}
	}

	// Get seen map for enrichment
	seenMap, err := h.particleSvc.GetSeenMap(r.Context(), particleIDs, email)
	if err != nil {
		slog.Warn("failed to get seen map", "error", err)
		seenMap = make(map[string]bool)
	}

	// Get acks map for enrichment
	acksMap, err := h.particleSvc.GetAcksMap(r.Context(), particleIDs)
	if err != nil {
		slog.Warn("failed to get acks map", "error", err)
		acksMap = make(map[string][]particle.AckInfo)
	}

	// Get members map for enrichment
	membersMap, err := h.particleSvc.GetMembersMap(r.Context(), particleIDs)
	if err != nil {
		slog.Warn("failed to get members map", "error", err)
		membersMap = make(map[string][]string)
	}

	// Get unseen counts for streams
	var unseenCounts map[string]int
	if len(streamIDs) > 0 {
		unseenCounts, err = h.particleSvc.GetUnseenCounts(r.Context(), networkID, streamIDs, email)
		if err != nil {
			slog.Warn("failed to get unseen counts", "error", err)
			unseenCounts = make(map[string]int)
		}
	}

	resp := ParticleList{
		Particles: make([]Particle, 0, len(list.Particles)),
		HasMore:   list.HasMore,
	}

	for _, p := range list.Particles {
		dto := h.particleToDTO(r.Context(), p)

		// Enrich with seen status
		seen, ok := seenMap[p.ID]
		if ok {
			dto.Seen = seen
		}

		// Enrich with acks
		if acks, ok := acksMap[p.ID]; ok && len(acks) > 0 {
			dto.Acks = make([]*AckInfo, len(acks))
			for i, a := range acks {
				dto.Acks[i] = &AckInfo{Email: a.Email, AckedAt: a.AckedAt}
			}
		}

		// Enrich with members
		if members, ok := membersMap[p.ID]; ok && len(members) > 0 {
			dto.Members = members
		}

		// Enrich with unseen count for streams
		if p.Type == particle.TypeStream {
			count := unseenCounts[p.ID]
			dto.UnseenCount = &count
		}

		resp.Particles = append(resp.Particles, dto)
	}

	if list.NextCursor != nil {
		encoded := list.NextCursor.Position + ":" + list.NextCursor.Direction
		resp.NextCursor = &encoded
	}
	if list.PrevCursor != nil {
		encoded := list.PrevCursor.Position + ":" + list.PrevCursor.Direction
		resp.PrevCursor = &encoded
	}

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

// GetParticle gets the details of a particle
func (h *Handler) GetParticle(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	p, err := h.particleSvc.GetByID(r.Context(), particleID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

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

	resp := h.particleToDTO(r.Context(), p)

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

// UpdateParticle updates the data of a particle
func (h *Handler) UpdateParticle(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	updated, err := h.particleSvc.Update(r.Context(), particleID, req.Data, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrInvalidData) {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		slog.Error("failed to update particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	resp := h.particleToDTO(r.Context(), updated)

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

// DeleteParticle deletes a particle and cascades to depot if applicable
func (h *Handler) DeleteParticle(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	// Get particle first to check for object_id (for cascade delete)
	p, err := h.particleSvc.GetByID(r.Context(), particleID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get particle for deletion", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Extract object_id if media/file
	objectID := extractObjectID(p)

	// Delete particle
	if err := h.particleSvc.Delete(r.Context(), particleID, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to delete particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	// Cascade delete depot object if applicable
	if objectID != "" {
		if err := h.depotSvc.Delete(r.Context(), objectID); err != nil {
			slog.Warn("failed to cascade delete depot object", "error", err, "object_id", objectID, "particle_id", particleID)
		}
	}

	w.WriteHeader(http.StatusNoContent)
}

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

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

	if err := h.particleSvc.OpenStream(r.Context(), particleID, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrNotAStream) {
			http.Error(w, "particle is not a stream", http.StatusBadRequest)
			return
		}
		if errors.Is(err, particle.ErrStreamAlreadyOpen) {
			http.Error(w, "stream is already open", http.StatusConflict)
			return
		}
		if errors.Is(err, particle.ErrCapacityExceeded) {
			http.Error(w, "stream capacity exceeded", http.StatusConflict)
			return
		}
		slog.Error("failed to open stream", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

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

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

	if err := h.particleSvc.CloseStream(r.Context(), particleID, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrNotAStream) {
			http.Error(w, "particle is not a stream", http.StatusBadRequest)
			return
		}
		if errors.Is(err, particle.ErrStreamAlreadyClosed) {
			http.Error(w, "stream is already closed", http.StatusConflict)
			return
		}
		slog.Error("failed to close stream", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// AddMembers adds members to a particle with custom visibility
func (h *Handler) AddMembers(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	if len(req.Emails) == 0 {
		http.Error(w, "emails are required", http.StatusBadRequest)
		return
	}

	if err := h.particleSvc.AddMembers(r.Context(), particleID, req.Emails, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrNotAContainer) {
			http.Error(w, "only streams can have members", http.StatusBadRequest)
			return
		}
		slog.Error("failed to add members to particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// RemoveMembers removes members from a particle with custom visibility
func (h *Handler) RemoveMembers(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	if len(req.Emails) == 0 {
		http.Error(w, "emails are required", http.StatusBadRequest)
		return
	}

	if err := h.particleSvc.RemoveMembers(r.Context(), particleID, req.Emails, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		if errors.Is(err, particle.ErrNotAContainer) {
			http.Error(w, "only streams can have members", http.StatusBadRequest)
			return
		}
		slog.Error("failed to remove members from particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// MarkSeen marks a particle as seen by the requester
func (h *Handler) MarkSeen(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	if err := h.particleSvc.MarkSeen(r.Context(), particleID, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to mark particle as seen", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// MarkSeenBatch marks multiple particles as seen by the requester
func (h *Handler) MarkSeenBatch(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	if len(req.ParticleIDs) == 0 {
		http.Error(w, "particle_ids are required", http.StatusBadRequest)
		return
	}

	if err := h.particleSvc.MarkSeenBatch(r.Context(), req.ParticleIDs, email); err != nil {
		slog.Error("failed to mark particles as seen", "error", err, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// AckParticle acknowledges a particle (public, permanent)
func (h *Handler) AckParticle(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

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

	if err := h.particleSvc.Ack(r.Context(), particleID, email); err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to ack particle", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

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

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

	p, err := h.particleSvc.GetByID(r.Context(), particleID, email)
	if err != nil {
		if errors.Is(err, particle.ErrNotFound) {
			http.Error(w, "particle not found", http.StatusNotFound)
			return
		}
		if errors.Is(err, particle.ErrAccessDenied) {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		slog.Error("failed to get particle for download", "error", err, "particle_id", particleID, "email", email)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	objectID := extractObjectID(p)
	if objectID == "" {
		http.Error(w, "particle has no downloadable content", http.StatusBadRequest)
		return
	}

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

	http.Redirect(w, r, downloadURL, http.StatusFound)
}

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

// PrepareUpload prepares an upload and returns a signed URL for direct upload to GCS
func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
	email, ok := middleware.EmailFromContext(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
	}

	// Check network membership
	isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, email)
	if err != nil {
		slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "email", email)
		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
		}
		slog.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)
}

// ConfirmUpload confirms that an upload has been completed
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
		}
		slog.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,
		CreatedAt:       obj.CreatedAt,
	}

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

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

const listAllBatchSize = 100

// listAllParticles fetches all particles matching the query by paginating in batches.
// This keeps the particle service pagination contract intact.
func (h *Handler) listAllParticles(ctx context.Context, networkID string, parentID *string, email string, filter particle.ListFilter) ([]*particle.Particle, error) {
	var all []*particle.Particle
	var cursor *particle.Cursor

	for {
		page, err := h.particleSvc.List(ctx, networkID, parentID, email, filter, cursor, listAllBatchSize)
		if err != nil {
			return nil, err
		}

		all = append(all, page.Particles...)

		if !page.HasMore || page.NextCursor == nil {
			break
		}
		cursor = page.NextCursor
	}

	return all, nil
}

// getStreamMembers returns the effective members for a stream.
// For custom visibility, returns particle_members. For network_all, returns all network members.
func (h *Handler) getStreamMembers(ctx context.Context, sp *particle.Particle, membersMap map[string][]string) []string {
	if sp.Visibility == particle.VisibilityCustom {
		return membersMap[sp.ID]
	}
	// network_all — return all network members
	net, err := h.networkSvc.GetByID(ctx, sp.NetworkID)
	if err != nil {
		slog.Warn("failed to get network for stream members", "error", err, "network_id", sp.NetworkID)
		return nil
	}
	return net.MemberEmails
}

func humanToDTO(h *human.Human) Human {
	return Human{
		Id:          utils.CreateOptionalString(h.ID),
		Email:       h.Email,
		EmailPrefix: h.EmailPrefix,
		CreatedAt:   &h.CreatedAt,
	}
}

func emailPrefix(email string) string {
	return strings.Split(email, "@")[0]
}
func (h *Handler) networkToDTO(ctx context.Context, n *network.Network) (Network, error) {
	adminHuman, err := h.humanSvc.GetByEmail(ctx, n.AdminEmail)
	if err != nil {
		return Network{}, err
	}

	// Get member humans
	humans := make([]Human, 0, len(n.MemberEmails))
	for _, email := range n.MemberEmails {
		hum, err := h.humanSvc.GetByEmail(ctx, email)
		if err != nil {
			if err == human.ErrNotFound {
				humans = append(humans, Human{
					Id:          nil,
					CreatedAt:   nil,
					Email:       email,
					EmailPrefix: emailPrefix(email),
				})
			}
			continue
		} else {
			humans = append(humans, humanToDTO(hum))
		}
	}

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

func (h *Handler) particleToDTO(ctx context.Context, p *particle.Particle) Particle {
	dto := Particle{
		Id:             p.ID,
		Type:           string(p.Type),
		CreatedByEmail: p.CreatedByEmail,
		Data:           p.Data,
		UpdatedAt:      p.UpdatedAt,
		CreatedAt:      p.CreatedAt,
		Visibility:     string(p.Visibility),
		Seen:           false,
		Acks:           []*AckInfo{},
	}

	return dto
}

func (h *Handler) streamParticleToDTO(ctx context.Context, p *particle.Particle) StreamParticle {
	dto := StreamParticle{
		Id:             p.ID,
		Type:           string(p.Type),
		CreatedByEmail: p.CreatedByEmail,
		Data:           p.Data,
		UpdatedAt:      p.UpdatedAt,
		CreatedAt:      p.CreatedAt,
	}

	return dto
}

func parseStreamStatus(s string) StreamStatus {
	switch s {
	case "open":
		return STREAM_STATUS_OPEN
	case "closed":
		return STREAM_STATUS_CLOSED
	default:
		return STREAM_STATUS_UNSPECIFIED
	}
}

func extractObjectID(p *particle.Particle) string {
	if p.Type != particle.TypeMedia && p.Type != particle.TypeFile {
		return ""
	}

	var data struct {
		ObjectID string `json:"object_id"`
	}
	if err := json.Unmarshal(p.Data, &data); err == nil {
		return data.ObjectID
	}
	return ""
}

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 ""
}
