package services

import (
	"context"
	"encoding/json"
	"fmt"

	"firebase.google.com/go/v4/messaging"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"github.com/sideshow/apns2"
	"github.com/sideshow/apns2/payload"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"

	"git.solsynth.dev/goatworks/turbine/pkg/ring/clients"
	"git.solsynth.dev/goatworks/turbine/pkg/ring/infra"
	"git.solsynth.dev/goatworks/turbine/pkg/ring/models"
	"git.solsynth.dev/goatworks/turbine/pkg/ring/websocket"
	pb "git.solsynth.dev/goatworks/turbine/pkg/shared/proto/gen"
)

// RingServiceServerImpl implements the RingServiceServer and RingHandlerServiceServer interfaces
type RingServiceServerImpl struct {
	pb.UnimplementedRingServiceServer
	pb.UnimplementedRingHandlerServiceServer
	WsManager *websocket.Manager // Add WebSocket Manager
}

// sendPushNotification helper function to send notifications via APNs or FCM
func sendPushNotification(ctx context.Context, notif *models.Notification, sub *models.NotificationPushSubscription) error {
	if sub.Provider == models.PushProviderApple {
		apnsClient := clients.GetAPNsClient()
		if apnsClient == nil {
			return fmt.Errorf("APNs client not initialized")
		}

		apnsPayload := payload.NewPayload().
			AlertTitle(*notif.Title).
			AlertBody(*notif.Content)

		if notif.Subtitle != nil {
			apnsPayload.AlertSubtitle(*notif.Subtitle)
		}

		if notif.Meta != nil {
			metaBytes, err := json.Marshal(notif.Meta)
			if err != nil {
				log.Error().Err(err).Msg("Failed to marshal notification meta for APNs")
			} else {
				apnsPayload.Custom("meta", string(metaBytes))
			}
		}

		notification := &apns2.Notification{
			DeviceToken: sub.DeviceToken,
			Topic:       notif.Topic,
			Payload:     apnsPayload,
		}

		res, err := apnsClient.PushWithContext(ctx, notification)
		if err != nil {
			return fmt.Errorf("failed to send APNs push notification: %w", err)
		}
		if res.Sent() {
			log.Info().Msgf("APNs notification sent successfully to device %s (token: %s)", sub.DeviceId, sub.DeviceToken)
		} else {
			log.Error().Msgf("APNs notification failed to send to device %s (token: %s) with reason: %s", sub.DeviceId, sub.DeviceToken, res.Reason)
			if res.StatusCode == 410 { // Expired token
				log.Warn().Msgf("APNs token for device %s expired. Deleting subscription.", sub.DeviceId)
				// TODO: Delete the subscription
			}
			return fmt.Errorf("APNs push notification failed: %s", res.Reason)
		}
	} else if sub.Provider == models.PushProviderGoogle {
		firebaseClient := clients.GetFirebaseClient()
		if firebaseClient == nil {
			return fmt.Errorf("Firebase client not initialized")
		}

		message := &messaging.Message{
			Token: sub.DeviceToken,
			Notification: &messaging.Notification{
				Title: *notif.Title,
				Body:  *notif.Content,
			},
			Data: make(map[string]string),
		}

		if notif.Meta != nil {
			for k, v := range notif.Meta {
				message.Data[k] = fmt.Sprintf("%v", v)
			}
		}

		res, err := firebaseClient.Send(ctx, message)
		if err != nil {
			return fmt.Errorf("failed to send Firebase push notification: %w", err)
		}
		log.Info().Msgf("Firebase notification sent successfully to device %s (token: %s): %s", sub.DeviceId, sub.DeviceToken, res)
	} else {
		return fmt.Errorf("unsupported push provider: %v", sub.Provider)
	}
	return nil
}

// SendEmail implements proto.RingServiceServer
func (s *RingServiceServerImpl) SendEmail(ctx context.Context, req *pb.SendEmailRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received SendEmail request: %+v", req.GetEmail())

	email := req.GetEmail()
	if email == nil {
		return nil, status.Errorf(codes.InvalidArgument, "email message is nil")
	}

	if email.ToAddress == "" || email.Subject == "" || email.Body == "" {
		return nil, status.Errorf(codes.InvalidArgument, "to_address, subject, and body cannot be empty")
	}

	err := clients.SendEmail(email.ToAddress, email.Subject, email.Body)
	if err != nil {
		log.Error().Err(err).Msg("Failed to send email")
		return nil, status.Errorf(codes.Internal, "failed to send email: %v", err)
	}

	log.Info().Msgf("Email sent successfully to %s", email.ToAddress)
	return &emptypb.Empty{}, nil
}

// PushWebSocketPacket implements proto.RingServiceServer
func (s *RingServiceServerImpl) PushWebSocketPacket(ctx context.Context, req *pb.PushWebSocketPacketRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received PushWebSocketPacket request for user %s: %+v", req.GetUserId(), req.GetPacket())

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	accountID, err := uuid.Parse(req.GetUserId())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user ID: %v", err)
	}

	packet := websocket.FromProtoValue(req.GetPacket())
	s.WsManager.SendPacketToAccount(accountID, packet)

	return &emptypb.Empty{}, nil
}

// PushWebSocketPacketToUsers implements proto.RingServiceServer
func (s *RingServiceServerImpl) PushWebSocketPacketToUsers(ctx context.Context, req *pb.PushWebSocketPacketToUsersRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received PushWebSocketPacketToUsers request for users %v: %+v", req.GetUserIds(), req.GetPacket())

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	packet := websocket.FromProtoValue(req.GetPacket())
	for _, userID := range req.GetUserIds() {
		accountID, err := uuid.Parse(userID)
		if err != nil {
			log.Warn().Err(err).Msgf("Invalid user ID in batch: %s, skipping", userID)
			continue
		}
		s.WsManager.SendPacketToAccount(accountID, packet)
	}

	return &emptypb.Empty{}, nil
}

// PushWebSocketPacketToDevice implements proto.RingServiceServer
func (s *RingServiceServerImpl) PushWebSocketPacketToDevice(ctx context.Context, req *pb.PushWebSocketPacketToDeviceRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received PushWebSocketPacketToDevice request for device %s: %+v", req.GetDeviceId(), req.GetPacket())

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	packet := websocket.FromProtoValue(req.GetPacket())
	s.WsManager.SendPacketToDevice(req.GetDeviceId(), packet)

	return &emptypb.Empty{}, nil
}

// PushWebSocketPacketToDevices implements proto.RingServiceServer
func (s *RingServiceServerImpl) PushWebSocketPacketToDevices(ctx context.Context, req *pb.PushWebSocketPacketToDevicesRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received PushWebSocketPacketToDevices request for devices %v: %+v", req.GetDeviceIds(), req.GetPacket())

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	packet := websocket.FromProtoValue(req.GetPacket())
	for _, deviceID := range req.GetDeviceIds() {
		s.WsManager.SendPacketToDevice(deviceID, packet)
	}

	return &emptypb.Empty{}, nil
}

// SendPushNotificationToUser implements proto.RingServiceServer
func (s *RingServiceServerImpl) SendPushNotificationToUser(ctx context.Context, req *pb.SendPushNotificationToUserRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received SendPushNotificationToUser request for user %s: %+v", req.GetUserId(), req.GetNotification())

	// 1. Parse incoming notification and save to DB
	accountID, err := uuid.Parse(req.GetUserId())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid account ID: %v", err)
	}

	notif := &models.Notification{
		Topic:     req.GetNotification().GetTopic(),
		Title:     &req.GetNotification().Title,
		Content:   &req.GetNotification().Body,
		AccountId: accountID,
	}

	if req.GetNotification().Subtitle != "" {
		subtitle := req.GetNotification().Subtitle
		notif.Subtitle = &subtitle
	}

	if req.GetNotification().Meta != nil {
		var metaMap map[string]any
		if err := json.Unmarshal(req.GetNotification().GetMeta(), &metaMap); err != nil {
			log.Warn().Err(err).Msg("Failed to unmarshal notification meta, sending without it")
		} else {
			notif.Meta = metaMap
		}
	}

	if err := infra.Db.WithContext(ctx).Create(notif).Error; err != nil {
		log.Error().Err(err).Msg("Failed to save notification to database")
		return nil, status.Errorf(codes.Internal, "failed to save notification: %v", err)
	}
	log.Info().Msgf("Notification saved to DB: %s", notif.Id.String())

	// 2. Retrieve push subscriptions for the user
	var subscriptions []models.NotificationPushSubscription
	if err := infra.Db.WithContext(ctx).Where("account_id = ?", req.GetUserId()).Find(&subscriptions).Error; err != nil {
		log.Error().Err(err).Msgf("Failed to retrieve subscriptions for account %s", req.GetUserId())
		return nil, status.Errorf(codes.Internal, "failed to retrieve subscriptions: %v", err)
	}

	if len(subscriptions) == 0 {
		log.Info().Msgf("No active push subscriptions found for user %s", req.GetUserId())
		return &emptypb.Empty{}, nil
	}

	// 3. Send notification to each subscription
	for _, sub := range subscriptions {
		sub := sub // Create a local copy for the goroutine
		go func() {
			if err := sendPushNotification(ctx, notif, &sub); err != nil {
				log.Error().Err(err).Msgf("Failed to send push notification to device %s", sub.DeviceId)
			}
		}()
	}

	return &emptypb.Empty{}, nil
}

// SendPushNotificationToUsers implements proto.RingServiceServer
func (s *RingServiceServerImpl) SendPushNotificationToUsers(ctx context.Context, req *pb.SendPushNotificationToUsersRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received SendPushNotificationToUsers request for users %v: %+v", req.GetUserIds(), req.GetNotification())

	// 1. Parse incoming notification (one notification for all users)
	// We'll create separate DB entries for each user if needed, or link to a single notification.
	// For simplicity, let's assume we save one notification per user for now.
	// A better approach for many users might be a single notification record linked to multiple user-notification bridges.

	// Extract common notification details
	notificationProto := req.GetNotification()
	metaMap := make(map[string]any)
	if notificationProto.Meta != nil {
		if err := json.Unmarshal(notificationProto.GetMeta(), &metaMap); err != nil {
			log.Warn().Err(err).Msg("Failed to unmarshal notification meta for batch, sending without it")
		}
	}

	for _, userID := range req.GetUserIds() {
		accountID, err := uuid.Parse(userID)
		if err != nil {
			log.Warn().Err(err).Msgf("Invalid account ID in batch: %s, skipping", userID)
			continue
		}

		notif := &models.Notification{
			Topic:     notificationProto.GetTopic(),
			Title:     &notificationProto.Title,
			Content:   &notificationProto.Body,
			Meta:      metaMap,
			AccountId: accountID,
		}

		if notificationProto.Subtitle != "" {
			subtitle := notificationProto.Subtitle
			notif.Subtitle = &subtitle
		}

		if err := infra.Db.WithContext(ctx).Create(notif).Error; err != nil {
			log.Error().Err(err).Msgf("Failed to save notification to database for user %s: %v", userID, err)
			continue
		}
		log.Info().Msgf("Notification saved to DB for user %s: %s", userID, notif.Id.String())

		// Retrieve push subscriptions for the current user
		var subscriptions []models.NotificationPushSubscription
		if err := infra.Db.WithContext(ctx).Where("account_id = ?", userID).Find(&subscriptions).Error; err != nil {
			log.Error().Err(err).Msgf("Failed to retrieve subscriptions for account %s in batch: %v", userID, err)
			continue
		}

		if len(subscriptions) == 0 {
			log.Info().Msgf("No active push subscriptions found for user %s in batch", userID)
			continue
		}

		// Send notification to each subscription for the current user
		for _, sub := range subscriptions {
			sub := sub // Create a local copy for the goroutine
			go func() {
				if err := sendPushNotification(ctx, notif, &sub); err != nil {
					log.Error().Err(err).Msgf("Failed to send push notification to device %s for user %s", sub.DeviceId, userID)
				}
			}()
		}
	}

	return &emptypb.Empty{}, nil
}

// UnsubscribePushNotifications implements proto.RingServiceServer
func (s *RingServiceServerImpl) UnsubscribePushNotifications(ctx context.Context, req *pb.UnsubscribePushNotificationsRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received UnsubscribePushNotifications request for device %s", req.GetDeviceId())

	if req.GetDeviceId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "device_id cannot be empty")
	}

	result := infra.Db.WithContext(ctx).Where("device_id = ?", req.GetDeviceId()).Delete(&models.NotificationPushSubscription{})
	if result.Error != nil {
		log.Error().Err(result.Error).Msgf("Failed to unsubscribe device %s", req.GetDeviceId())
		return nil, status.Errorf(codes.Internal, "failed to unsubscribe: %v", result.Error)
	}

	if result.RowsAffected == 0 {
		log.Info().Msgf("No subscription found for device %s to unsubscribe", req.GetDeviceId())
	} else {
		log.Info().Msgf("Successfully unsubscribed device %s", req.GetDeviceId())
	}

	return &emptypb.Empty{}, nil
}

// GetWebsocketConnectionStatus implements proto.RingServiceServer
func (s *RingServiceServerImpl) GetWebsocketConnectionStatus(ctx context.Context, req *pb.GetWebsocketConnectionStatusRequest) (*pb.GetWebsocketConnectionStatusResponse, error) {
	log.Info().Msgf("Received GetWebsocketConnectionStatus request: %+v", req)

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	isConnected := false
	switch id := req.GetId().(type) {
	case *pb.GetWebsocketConnectionStatusRequest_DeviceId:
		isConnected = s.WsManager.GetDeviceIsConnected(id.DeviceId)
	case *pb.GetWebsocketConnectionStatusRequest_UserId:
		accountID, err := uuid.Parse(id.UserId)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid user ID: %v", err)
		}
		isConnected = s.WsManager.GetAccountIsConnected(accountID)
	default:
		return nil, status.Errorf(codes.InvalidArgument, "either device_id or user_id must be provided")
	}

	return &pb.GetWebsocketConnectionStatusResponse{IsConnected: isConnected}, nil
}

// GetWebsocketConnectionStatusBatch implements proto.RingServiceServer
func (s *RingServiceServerImpl) GetWebsocketConnectionStatusBatch(ctx context.Context, req *pb.GetWebsocketConnectionStatusBatchRequest) (*pb.GetWebsocketConnectionStatusBatchResponse, error) {
	log.Info().Msgf("Received GetWebsocketConnectionStatusBatch request: %+v", req)

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	response := &pb.GetWebsocketConnectionStatusBatchResponse{
		IsConnected: make(map[string]bool),
	}

	for _, userID := range req.GetUsersId() {
		accountID, err := uuid.Parse(userID)
		if err != nil {
			log.Warn().Err(err).Msgf("Invalid user ID in batch: %s, skipping", userID)
			response.IsConnected[userID] = false // Indicate connection status as false for invalid IDs
			continue
		}
		response.IsConnected[userID] = s.WsManager.GetAccountIsConnected(accountID)
	}

	return response, nil
}

// ReceiveWebSocketPacket implements proto.RingHandlerServiceServer
func (s *RingServiceServerImpl) ReceiveWebSocketPacket(ctx context.Context, req *pb.ReceiveWebSocketPacketRequest) (*emptypb.Empty, error) {
	log.Info().Msgf("Received ReceiveWebSocketPacket request: %+v", req)

	if s.WsManager == nil {
		return nil, status.Errorf(codes.Unavailable, "WebSocket manager not initialized")
	}

	packet := websocket.FromProtoValue(req.GetPacket())

	// The C# HandlePacket expects current user and device ID.
	// For this gRPC endpoint, we can use the account and device_id from the request.
	if req.GetAccount() == nil || req.GetAccount().GetId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "account information missing in request")
	}
	if req.GetDeviceId() == "" {
		return nil, status.Errorf(codes.InvalidArgument, "device_id missing in request")
	}

	// Assuming the request comes from a trusted source, we can use the provided account.
	// We don't have a direct *websocket.Conn here, so we can't send error responses back on a direct WebSocket.
	// Errors will be logged and returned as gRPC errors.
	err := s.WsManager.HandlePacket(req.GetAccount(), req.GetDeviceId(), packet, nil) // Pass nil for *websocket.Conn
	if err != nil {
		log.Error().Err(err).Msg("Failed to handle received WebSocket packet via gRPC")
		return nil, status.Errorf(codes.Internal, "failed to process packet: %v", err)
	}

	return &emptypb.Empty{}, nil
}
