package push

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"time"
)

type Service struct {
	repo       Repository
	client     *http.Client
	vapidPub   string
	vapidPriv  string
	fcmKey     string
}

func NewService(repo Repository) *Service {
	return &Service{
		repo:      repo,
		client:    &http.Client{Timeout: 6 * time.Second},
		vapidPub:  strings.TrimSpace(os.Getenv("VAPID_PUBLIC_KEY")),
		vapidPriv: strings.TrimSpace(os.Getenv("VAPID_PRIVATE_KEY")),
		fcmKey:    strings.TrimSpace(os.Getenv("FCM_SERVER_KEY")),
	}
}

func (s *Service) VapidPublicKey() string {
	return s.vapidPub
}

func (s *Service) Subscribe(ctx context.Context, userID int64, in SubscribeInput, userAgent string) (*Subscription, error) {
	if strings.TrimSpace(in.Endpoint) == "" {
		return nil, fmt.Errorf("endpoint is required")
	}
	app := strings.ToLower(strings.TrimSpace(in.App))
	if app == "" {
		app = "erp"
	}

	sub := &Subscription{
		UserID:      userID,
		Endpoint:    strings.TrimSpace(in.Endpoint),
		P256dh:      strings.TrimSpace(in.Keys.P256dh),
		Auth:        strings.TrimSpace(in.Keys.Auth),
		DeviceToken: strings.TrimSpace(in.DeviceToken),
		App:         app,
		UserAgent:   userAgent,
	}

	if err := s.repo.Save(ctx, sub); err != nil {
		return nil, err
	}
	return sub, nil
}

func (s *Service) Unsubscribe(ctx context.Context, userID int64, endpoint string) error {
	return s.repo.Delete(ctx, userID, strings.TrimSpace(endpoint))
}

func (s *Service) SendToUser(ctx context.Context, userID int64, payload Payload) int {
	subs, err := s.repo.FindByUser(ctx, userID)
	if err != nil {
		slog.Error("push: find subscriptions for user", "user_id", userID, "err", err)
		return 0
	}
	return s.dispatchBatch(ctx, subs, payload)
}

func (s *Service) SendToRoles(ctx context.Context, roles []string, payload Payload) int {
	subs, err := s.repo.FindByRoles(ctx, roles)
	if err != nil {
		slog.Error("push: find subscriptions for roles", "roles", roles, "err", err)
		return 0
	}
	return s.dispatchBatch(ctx, subs, payload)
}

func (s *Service) Broadcast(ctx context.Context, payload Payload) int {
	subs, err := s.repo.FindAll(ctx)
	if err != nil {
		slog.Error("push: find all subscriptions", "err", err)
		return 0
	}
	return s.dispatchBatch(ctx, subs, payload)
}

func (s *Service) dispatchBatch(ctx context.Context, subs []Subscription, payload Payload) int {
	if len(subs) == 0 {
		return 0
	}

	sent := 0
	bodyBytes, _ := json.Marshal(payload)

	for _, sub := range subs {
		// Run delivery with detached background context bounded to 6s
		go func(sub Subscription) {
			deliveryCtx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
			defer cancel()

			req, err := http.NewRequestWithContext(deliveryCtx, http.MethodPost, sub.Endpoint, bytes.NewReader(bodyBytes))
			if err != nil {
				return
			}

			req.Header.Set("Content-Type", "application/json")
			req.Header.Set("TTL", "120")
			req.Header.Set("Urgency", "high")

			// If Google FCM endpoint and server key exists
			if s.fcmKey != "" && strings.Contains(sub.Endpoint, "fcm.googleapis.com") {
				req.Header.Set("Authorization", "key="+s.fcmKey)
			}

			resp, err := s.client.Do(req)
			if err != nil {
				return
			}
			defer resp.Body.Close()

			// If subscription is unsubscribed or expired, cleanup stale record
			if resp.StatusCode == http.StatusGone || resp.StatusCode == http.StatusNotFound {
				_ = s.repo.DeleteByEndpoint(context.Background(), sub.Endpoint)
			}
		}(sub)
		sent++
	}

	return sent
}
