package notifications

import (
	"context"
	"encoding/json"
	"log"
	"sync"
	"time"

	"switchboard-core/events"
	"switchboard-core/models"
	"switchboard-core/store"
)

// ── Package-level singleton ─────────────────
// Allows notification sources (KB ingester, group handler, etc.)
// to access the service without threading it through constructors.

var defaultService *Service

// SetDefault registers the global notification service instance.
// Called once at startup from main.go.
func SetDefault(svc *Service) {
	defaultService = svc
}

// Default returns the global notification service instance, or nil if
// not configured (e.g. in unmanaged mode without a database).
func Default() *Service {
	return defaultService
}

// Service handles notification creation, persistence, and real-time delivery.
// All notification sources should go through this service — never insert directly.
type Service struct {
	store     store.NotificationStore
	prefStore store.NotificationPreferenceStore
	userStore store.UserStore
	hub       *events.Hub

	// Email transport (nil = email disabled)
	email        *EmailTransport
	instanceName string

	// Retention cleanup
	retentionDays int
	stopCleanup   chan struct{}
	cleanupWg     sync.WaitGroup
}

// NewService creates a notification service bound to the given store and WebSocket hub.
func NewService(s store.NotificationStore, hub *events.Hub) *Service {
	return &Service{
		store:         s,
		hub:           hub,
		instanceName:  "Switchboard Core",
		retentionDays: 90,
		stopCleanup:   make(chan struct{}),
	}
}

// WithPrefs attaches the preference store for per-user delivery control.
func (s *Service) WithPrefs(ps store.NotificationPreferenceStore) *Service {
	s.prefStore = ps
	return s
}

// WithUsers attaches the user store for email address lookup.
func (s *Service) WithUsers(us store.UserStore) *Service {
	s.userStore = us
	return s
}

// WithEmail enables email transport.
func (s *Service) WithEmail(transport *EmailTransport, instanceName string) *Service {
	s.email = transport
	if instanceName != "" {
		s.instanceName = instanceName
	}
	return s
}

// WithRetention sets the retention period in days. Notifications older than
// this are pruned by the background cleanup goroutine. Default: 90 days.
func (s *Service) WithRetention(days int) *Service {
	if days > 0 {
		s.retentionDays = days
	}
	return s
}

// Notify routes a notification through the user's delivery preferences:
// in-app (persist + WebSocket) and/or email. Falls back to system defaults
// if no user preferences are set.
func (s *Service) Notify(ctx context.Context, n *models.Notification) error {
	prefs := s.resolvePrefs(ctx, n.UserID, n.Type)

	// In-app: persist + WebSocket push
	if prefs.InApp {
		if err := s.store.Create(ctx, n); err != nil {
			return err
		}

		if s.hub != nil {
			payload, _ := json.Marshal(n)
			s.hub.PublishToUser(n.UserID, events.Event{
				Label:   "notification.new",
				Payload: payload,
				Ts:      time.Now().UnixMilli(),
			})
		}
	}

	// Email: async send (don't block the caller)
	if prefs.Email && s.email != nil && s.userStore != nil {
		user, err := s.userStore.GetByID(ctx, n.UserID)
		if err == nil && user != nil && user.Email != "" {
			subject := SubjectForNotification(n, s.instanceName)
			htmlBody := RenderHTML(n, s.instanceName)
			textBody := RenderText(n, s.instanceName)
			s.email.SendAsync(user.Email, subject, htmlBody, textBody)
		}
	}

	return nil
}

// NotifyMany fans out a notification template to multiple users.
// Each user gets their own copy. Errors are logged but don't fail the batch.
func (s *Service) NotifyMany(ctx context.Context, userIDs []string, template models.Notification) {
	for _, uid := range userIDs {
		n := template
		n.ID = "" // let store generate
		n.UserID = uid
		if err := s.Notify(ctx, &n); err != nil {
			log.Printf("[notifications] failed to notify user %s: %v", uid, err)
		}
	}
}

// resolvePrefs returns the effective delivery preferences for a user + type.
// Resolution chain: specific type → user '*' default → system default.
func (s *Service) resolvePrefs(ctx context.Context, userID, notifType string) models.ResolvedPreference {
	if s.prefStore == nil {
		return models.SystemDefaultPreference
	}

	// 1. Specific type preference
	pref, err := s.prefStore.Get(ctx, userID, notifType)
	if err == nil && pref != nil {
		return models.ResolvedPreference{InApp: pref.InApp, Email: pref.Email}
	}

	// 2. User's wildcard default
	pref, err = s.prefStore.Get(ctx, userID, "*")
	if err == nil && pref != nil {
		return models.ResolvedPreference{InApp: pref.InApp, Email: pref.Email}
	}

	// 3. System default
	return models.SystemDefaultPreference
}

// StartCleanup launches a background goroutine that prunes old notifications daily.
func (s *Service) StartCleanup() {
	s.cleanupWg.Add(1)
	go func() {
		defer s.cleanupWg.Done()
		ticker := time.NewTicker(24 * time.Hour)
		defer ticker.Stop()

		// Run once on startup (after a short delay to avoid contention)
		time.Sleep(5 * time.Minute)
		s.runCleanup()

		for {
			select {
			case <-ticker.C:
				s.runCleanup()
			case <-s.stopCleanup:
				return
			}
		}
	}()
}

// StopCleanup signals the cleanup goroutine to stop and waits for it.
func (s *Service) StopCleanup() {
	close(s.stopCleanup)
	s.cleanupWg.Wait()
}

func (s *Service) runCleanup() {
	cutoff := time.Now().AddDate(0, 0, -s.retentionDays)
	deleted, err := s.store.DeleteOlderThan(context.Background(), cutoff)
	if err != nil {
		log.Printf("[notifications] cleanup error: %v", err)
		return
	}
	if deleted > 0 {
		log.Printf("[notifications] cleaned up %d notifications older than %d days", deleted, s.retentionDays)
	}
}
