package pusher

import (
	"context"
	"net/http"

	"github.com/google/uuid"
	"nhooyr.io/websocket"

	pbpusher "github.com/flowy-live/llink/genproto/llink/pusher"
	"github.com/flowy-live/llink/internal/auth"
	"github.com/flowy-live/llink/internal/utils/flog"
)

// Server handles WebSocket upgrades and gRPC presence queries.
type Server struct {
	pbpusher.UnimplementedPusherServiceServer

	ctx     context.Context // server-scoped; cancelling closes all WebSockets gracefully
	hub     *Hub
	bridge  *RedisBridge
	authSvc auth.SessionReader
}

func NewServer(ctx context.Context, hub *Hub, bridge *RedisBridge, authSvc auth.SessionReader) *Server {
	return &Server{
		ctx:     ctx,
		hub:     hub,
		bridge:  bridge,
		authSvc: authSvc,
	}
}

func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
	// Token rides in the query string — WebSocket upgrades can't carry custom headers.
	token := r.URL.Query().Get("token")
	if token == "" {
		http.Error(w, "token required", http.StatusUnauthorized)
		return
	}

	session, err := s.authSvc.GetSession(r.Context(), token)
	if err != nil {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
		// CORS is enforced at the gateway.
		InsecureSkipVerify: true,
	})
	if err != nil {
		flog.Error("websocket accept failed", "error", err)
		return
	}

	connID := uuid.New().String()
	conn := newConn(connID, session.HumanId, ws)

	flog.Info("websocket connected", "connId", connID, "humanId", session.HumanId)

	// Use the server context, not r.Context(): after upgrade the HTTP request
	// context can be cancelled by load balancers and nhooyr/websocket would
	// then permanently close the conn.
	ctx, cancel := context.WithCancel(s.ctx)
	defer cancel()

	// Auto-subscribe to the presence channel so this user appears online.
	s.hub.Subscribe(conn, "_presence:"+session.HumanId)

	go conn.WritePump(ctx)
	conn.ReadPump(ctx, s.hub)

	flog.Info("websocket disconnected", "connId", connID, "humanId", session.HumanId)
}

func (s *Server) GetOnlineHumanIds(ctx context.Context, _ *pbpusher.GetOnlineHumanIdsRequest) (*pbpusher.GetOnlineHumanIdsResponse, error) {
	humanIDs, err := s.bridge.GetAllConnectedHumanIDs(ctx)
	if err != nil {
		return nil, err
	}
	return &pbpusher.GetOnlineHumanIdsResponse{HumanIds: humanIDs}, nil
}

func (s *Server) IsOnline(ctx context.Context, req *pbpusher.IsOnlineRequest) (*pbpusher.IsOnlineResponse, error) {
	allOnline, err := s.bridge.GetAllConnectedHumanIDs(ctx)
	if err != nil {
		return nil, err
	}
	onlineSet := make(map[string]bool, len(allOnline))
	for _, id := range allOnline {
		onlineSet[id] = true
	}
	result := make(map[string]bool, len(req.HumanIds))
	for _, id := range req.HumanIds {
		result[id] = onlineSet[id]
	}
	return &pbpusher.IsOnlineResponse{Online: result}, nil
}

func (s *Server) GetChannelPresence(ctx context.Context, req *pbpusher.GetChannelPresenceRequest) (*pbpusher.GetChannelPresenceResponse, error) {
	presence, err := s.bridge.GetPresence(ctx, req.ChannelIds)
	if err != nil {
		return nil, err
	}
	resp := &pbpusher.GetChannelPresenceResponse{
		Presences: make(map[string]*pbpusher.ChannelPresence, len(presence)),
	}
	for chID, humanIDs := range presence {
		resp.Presences[chID] = &pbpusher.ChannelPresence{
			HumanIds: humanIDs,
		}
	}
	return resp, nil
}
