package services

import (
	"context"
	"crypto/tls"
	"fmt"
	"log/slog"
	"net"
	"net/smtp"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"

	"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
)

// NotificationService handles notification CRUD, deadline reminders, and email sending.
type NotificationService struct {
	db     *sqlx.DB
	stopCh chan struct{}
	wg     sync.WaitGroup
}

// NewNotificationService creates a new notification service.
func NewNotificationService(db *sqlx.DB) *NotificationService {
	return &NotificationService{
		db:     db,
		stopCh: make(chan struct{}),
	}
}

// Start launches the background reminder checker (every hour) and daily digest (8am).
func (s *NotificationService) Start() {
	s.wg.Add(1)
	go s.backgroundLoop()
}

// Stop gracefully shuts down background workers.
func (s *NotificationService) Stop() {
	close(s.stopCh)
	s.wg.Wait()
}

func (s *NotificationService) backgroundLoop() {
	defer s.wg.Done()

	// Check reminders on startup
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	s.CheckDeadlineReminders(ctx)
	cancel()

	reminderTicker := time.NewTicker(1 * time.Hour)
	defer reminderTicker.Stop()

	// Digest ticker: check every 15 minutes, send at 8am
	digestTicker := time.NewTicker(15 * time.Minute)
	defer digestTicker.Stop()

	var lastDigestDate string

	for {
		select {
		case <-s.stopCh:
			return
		case <-reminderTicker.C:
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
			s.CheckDeadlineReminders(ctx)
			cancel()
		case now := <-digestTicker.C:
			today := now.Format("2006-01-02")
			hour := now.Hour()
			if hour >= 8 && lastDigestDate != today {
				lastDigestDate = today
				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
				s.SendDailyDigests(ctx)
				cancel()
			}
		}
	}
}

// CheckDeadlineReminders finds deadlines due in N days matching user preferences and creates notifications.
func (s *NotificationService) CheckDeadlineReminders(ctx context.Context) {
	slog.Info("checking deadline reminders")

	// Get all user preferences with email enabled
	var prefs []models.NotificationPreferences
	err := s.db.SelectContext(ctx, &prefs,
		`SELECT user_id, tenant_id, deadline_reminder_days, email_enabled, daily_digest, created_at, updated_at
		 FROM notification_preferences`)
	if err != nil {
		slog.Error("failed to load notification preferences", "error", err)
		return
	}

	if len(prefs) == 0 {
		return
	}

	// Collect all unique reminder day values across all users
	daySet := make(map[int64]bool)
	for _, p := range prefs {
		for _, d := range p.DeadlineReminderDays {
			daySet[d] = true
		}
	}
	if len(daySet) == 0 {
		return
	}

	// Build array of target dates
	today := time.Now().Truncate(24 * time.Hour)
	var targetDates []string
	dayToDate := make(map[string]int64)
	for d := range daySet {
		target := today.AddDate(0, 0, int(d))
		dateStr := target.Format("2006-01-02")
		targetDates = append(targetDates, dateStr)
		dayToDate[dateStr] = d
	}

	// Also check overdue deadlines
	todayStr := today.Format("2006-01-02")

	// Find pending deadlines matching target dates
	type deadlineRow struct {
		models.Deadline
		CaseTitle  string `db:"case_title"`
		CaseNumber string `db:"case_number"`
	}

	// Reminder deadlines (due in N days)
	var reminderDeadlines []deadlineRow
	query, args, err := sqlx.In(
		`SELECT d.*, c.title AS case_title, c.case_number
		 FROM deadlines d
		 JOIN cases c ON c.id = d.case_id
		 WHERE d.status = 'pending' AND d.due_date IN (?)`,
		targetDates)
	if err == nil {
		query = s.db.Rebind(query)
		err = s.db.SelectContext(ctx, &reminderDeadlines, query, args...)
	}
	if err != nil {
		slog.Error("failed to query reminder deadlines", "error", err)
	}

	// Overdue deadlines
	var overdueDeadlines []deadlineRow
	err = s.db.SelectContext(ctx, &overdueDeadlines,
		`SELECT d.*, c.title AS case_title, c.case_number
		 FROM deadlines d
		 JOIN cases c ON c.id = d.case_id
		 WHERE d.status = 'pending' AND d.due_date < $1`, todayStr)
	if err != nil {
		slog.Error("failed to query overdue deadlines", "error", err)
	}

	// Create notifications for each user based on their tenant and preferences
	for _, pref := range prefs {
		// Reminder notifications
		for _, dl := range reminderDeadlines {
			if dl.TenantID != pref.TenantID {
				continue
			}
			daysUntil := dayToDate[dl.DueDate]
			// Check if this user cares about this many days
			if !containsDay(pref.DeadlineReminderDays, daysUntil) {
				continue
			}

			title := fmt.Sprintf("Frist in %d Tagen: %s", daysUntil, dl.Title)
			body := fmt.Sprintf("Akte %s — %s\nFällig am %s", dl.CaseNumber, dl.CaseTitle, dl.DueDate)
			entityType := "deadline"

			s.CreateNotification(ctx, CreateNotificationInput{
				TenantID:   pref.TenantID,
				UserID:     pref.UserID,
				Type:       "deadline_reminder",
				EntityType: &entityType,
				EntityID:   &dl.ID,
				Title:      title,
				Body:       &body,
				SendEmail:  pref.EmailEnabled && !pref.DailyDigest,
			})
		}

		// Overdue notifications
		for _, dl := range overdueDeadlines {
			if dl.TenantID != pref.TenantID {
				continue
			}

			title := fmt.Sprintf("Frist überfällig: %s", dl.Title)
			body := fmt.Sprintf("Akte %s — %s\nFällig seit %s", dl.CaseNumber, dl.CaseTitle, dl.DueDate)
			entityType := "deadline"

			s.CreateNotification(ctx, CreateNotificationInput{
				TenantID:   pref.TenantID,
				UserID:     pref.UserID,
				Type:       "deadline_overdue",
				EntityType: &entityType,
				EntityID:   &dl.ID,
				Title:      title,
				Body:       &body,
				SendEmail:  pref.EmailEnabled && !pref.DailyDigest,
			})
		}
	}
}

// SendDailyDigests compiles pending notifications into one email per user.
func (s *NotificationService) SendDailyDigests(ctx context.Context) {
	slog.Info("sending daily digests")

	// Find users with daily_digest enabled
	var prefs []models.NotificationPreferences
	err := s.db.SelectContext(ctx, &prefs,
		`SELECT user_id, tenant_id, deadline_reminder_days, email_enabled, daily_digest, created_at, updated_at
		 FROM notification_preferences
		 WHERE daily_digest = true AND email_enabled = true`)
	if err != nil {
		slog.Error("failed to load digest preferences", "error", err)
		return
	}

	for _, pref := range prefs {
		// Get unsent notifications for this user from the last 24 hours
		var notifications []models.Notification
		err := s.db.SelectContext(ctx, &notifications,
			`SELECT id, tenant_id, user_id, type, entity_type, entity_id, title, body, sent_at, read_at, created_at
			 FROM notifications
			 WHERE user_id = $1 AND tenant_id = $2 AND sent_at IS NULL
			   AND created_at > now() - interval '24 hours'
			 ORDER BY created_at DESC`,
			pref.UserID, pref.TenantID)
		if err != nil {
			slog.Error("failed to load unsent notifications", "error", err, "user_id", pref.UserID)
			continue
		}

		if len(notifications) == 0 {
			continue
		}

		// Get user email
		email := s.getUserEmail(ctx, pref.UserID)
		if email == "" {
			continue
		}

		// Build digest
		var lines []string
		lines = append(lines, fmt.Sprintf("Guten Morgen! Hier ist Ihre Tagesübersicht mit %d Benachrichtigungen:\n", len(notifications)))
		for _, n := range notifications {
			body := ""
			if n.Body != nil {
				body = " — " + *n.Body
			}
			lines = append(lines, fmt.Sprintf("• %s%s", n.Title, body))
		}
		lines = append(lines, "\n---\nKanzlAI Kanzleimanagement")

		subject := fmt.Sprintf("KanzlAI Tagesübersicht — %d Benachrichtigungen", len(notifications))
		bodyText := strings.Join(lines, "\n")

		if err := SendEmail(email, subject, bodyText); err != nil {
			slog.Error("failed to send digest email", "error", err, "user_id", pref.UserID)
			continue
		}

		// Mark all as sent
		ids := make([]uuid.UUID, len(notifications))
		for i, n := range notifications {
			ids[i] = n.ID
		}
		query, args, err := sqlx.In(
			`UPDATE notifications SET sent_at = now() WHERE id IN (?)`, ids)
		if err == nil {
			query = s.db.Rebind(query)
			_, err = s.db.ExecContext(ctx, query, args...)
		}
		if err != nil {
			slog.Error("failed to mark digest notifications sent", "error", err)
		}

		slog.Info("sent daily digest", "user_id", pref.UserID, "count", len(notifications))
	}
}

// CreateNotificationInput holds the data for creating a notification.
type CreateNotificationInput struct {
	TenantID   uuid.UUID
	UserID     uuid.UUID
	Type       string
	EntityType *string
	EntityID   *uuid.UUID
	Title      string
	Body       *string
	SendEmail  bool
}

// CreateNotification stores a notification in the DB and optionally sends an email.
func (s *NotificationService) CreateNotification(ctx context.Context, input CreateNotificationInput) (*models.Notification, error) {
	// Dedup: check if we already sent this notification today
	if input.EntityID != nil {
		var count int
		err := s.db.GetContext(ctx, &count,
			`SELECT COUNT(*) FROM notifications
			 WHERE user_id = $1 AND entity_id = $2 AND type = $3
			   AND created_at::date = CURRENT_DATE`,
			input.UserID, input.EntityID, input.Type)
		if err == nil && count > 0 {
			return nil, nil // Already notified today
		}
	}

	var n models.Notification
	err := s.db.QueryRowxContext(ctx,
		`INSERT INTO notifications (tenant_id, user_id, type, entity_type, entity_id, title, body)
		 VALUES ($1, $2, $3, $4, $5, $6, $7)
		 RETURNING id, tenant_id, user_id, type, entity_type, entity_id, title, body, sent_at, read_at, created_at`,
		input.TenantID, input.UserID, input.Type, input.EntityType, input.EntityID,
		input.Title, input.Body).StructScan(&n)
	if err != nil {
		slog.Error("failed to create notification", "error", err)
		return nil, fmt.Errorf("create notification: %w", err)
	}

	// Send email immediately if requested (non-digest users)
	if input.SendEmail {
		email := s.getUserEmail(ctx, input.UserID)
		if email != "" {
			go func() {
				if err := SendEmail(email, input.Title, derefStr(input.Body)); err != nil {
					slog.Error("failed to send notification email", "error", err, "user_id", input.UserID)
				} else {
					// Mark as sent
					_, _ = s.db.Exec(`UPDATE notifications SET sent_at = now() WHERE id = $1`, n.ID)
				}
			}()
		}
	}

	return &n, nil
}

// ListForUser returns notifications for a user in a tenant, paginated.
func (s *NotificationService) ListForUser(ctx context.Context, tenantID, userID uuid.UUID, limit, offset int) ([]models.Notification, int, error) {
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}

	var total int
	err := s.db.GetContext(ctx, &total,
		`SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND tenant_id = $2`,
		userID, tenantID)
	if err != nil {
		return nil, 0, fmt.Errorf("count notifications: %w", err)
	}

	var notifications []models.Notification
	err = s.db.SelectContext(ctx, &notifications,
		`SELECT id, tenant_id, user_id, type, entity_type, entity_id, title, body, sent_at, read_at, created_at
		 FROM notifications
		 WHERE user_id = $1 AND tenant_id = $2
		 ORDER BY created_at DESC
		 LIMIT $3 OFFSET $4`,
		userID, tenantID, limit, offset)
	if err != nil {
		return nil, 0, fmt.Errorf("list notifications: %w", err)
	}

	return notifications, total, nil
}

// UnreadCount returns the number of unread notifications for a user.
func (s *NotificationService) UnreadCount(ctx context.Context, tenantID, userID uuid.UUID) (int, error) {
	var count int
	err := s.db.GetContext(ctx, &count,
		`SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND tenant_id = $2 AND read_at IS NULL`,
		userID, tenantID)
	return count, err
}

// MarkRead marks a single notification as read.
func (s *NotificationService) MarkRead(ctx context.Context, tenantID, userID, notificationID uuid.UUID) error {
	result, err := s.db.ExecContext(ctx,
		`UPDATE notifications SET read_at = now()
		 WHERE id = $1 AND user_id = $2 AND tenant_id = $3 AND read_at IS NULL`,
		notificationID, userID, tenantID)
	if err != nil {
		return fmt.Errorf("mark notification read: %w", err)
	}
	rows, _ := result.RowsAffected()
	if rows == 0 {
		return fmt.Errorf("notification not found or already read")
	}
	return nil
}

// MarkAllRead marks all notifications as read for a user.
func (s *NotificationService) MarkAllRead(ctx context.Context, tenantID, userID uuid.UUID) error {
	_, err := s.db.ExecContext(ctx,
		`UPDATE notifications SET read_at = now()
		 WHERE user_id = $1 AND tenant_id = $2 AND read_at IS NULL`,
		userID, tenantID)
	return err
}

// GetPreferences returns notification preferences for a user, creating defaults if needed.
func (s *NotificationService) GetPreferences(ctx context.Context, tenantID, userID uuid.UUID) (*models.NotificationPreferences, error) {
	var pref models.NotificationPreferences
	err := s.db.GetContext(ctx, &pref,
		`SELECT user_id, tenant_id, deadline_reminder_days, email_enabled, daily_digest, created_at, updated_at
		 FROM notification_preferences
		 WHERE user_id = $1 AND tenant_id = $2`,
		userID, tenantID)
	if err != nil {
		// Return defaults if no preferences set
		return &models.NotificationPreferences{
			UserID:               userID,
			TenantID:             tenantID,
			DeadlineReminderDays: pq.Int64Array{7, 3, 1},
			EmailEnabled:         true,
			DailyDigest:          false,
		}, nil
	}
	return &pref, nil
}

// UpdatePreferences upserts notification preferences for a user.
func (s *NotificationService) UpdatePreferences(ctx context.Context, tenantID, userID uuid.UUID, input UpdatePreferencesInput) (*models.NotificationPreferences, error) {
	var pref models.NotificationPreferences
	err := s.db.QueryRowxContext(ctx,
		`INSERT INTO notification_preferences (user_id, tenant_id, deadline_reminder_days, email_enabled, daily_digest)
		 VALUES ($1, $2, $3, $4, $5)
		 ON CONFLICT (user_id, tenant_id)
		 DO UPDATE SET deadline_reminder_days = $3, email_enabled = $4, daily_digest = $5, updated_at = now()
		 RETURNING user_id, tenant_id, deadline_reminder_days, email_enabled, daily_digest, created_at, updated_at`,
		userID, tenantID, pq.Int64Array(input.DeadlineReminderDays), input.EmailEnabled, input.DailyDigest).StructScan(&pref)
	if err != nil {
		return nil, fmt.Errorf("update preferences: %w", err)
	}
	return &pref, nil
}

// UpdatePreferencesInput holds the data for updating notification preferences.
type UpdatePreferencesInput struct {
	DeadlineReminderDays []int64 `json:"deadline_reminder_days"`
	EmailEnabled         bool    `json:"email_enabled"`
	DailyDigest          bool    `json:"daily_digest"`
}

// SendEmail sends an email via direct SMTP over TLS.
// Requires SMTP_HOST, SMTP_USER, SMTP_PASS env vars. Falls back to no-op if unconfigured.
func SendEmail(to, subject, body string) error {
	host := os.Getenv("SMTP_HOST")
	port := os.Getenv("SMTP_PORT")
	user := os.Getenv("SMTP_USER")
	pass := os.Getenv("SMTP_PASS")
	from := os.Getenv("MAIL_FROM")

	if port == "" {
		port = "465"
	}
	if from == "" {
		from = "mgmt@msbls.de"
	}

	if host == "" || user == "" || pass == "" {
		slog.Warn("SMTP not configured, skipping email", "to", to, "subject", subject)
		return nil
	}

	// Build RFC 2822 message
	msg := fmt.Sprintf("From: \"KanzlAI-mGMT\" <%s>\r\n"+
		"To: %s\r\n"+
		"Subject: [KanzlAI] %s\r\n"+
		"MIME-Version: 1.0\r\n"+
		"Content-Type: text/plain; charset=utf-8\r\n"+
		"Date: %s\r\n"+
		"\r\n%s",
		from, to, subject,
		time.Now().Format(time.RFC1123Z),
		body)

	addr := net.JoinHostPort(host, port)

	// Connect with implicit TLS (port 465)
	tlsConfig := &tls.Config{ServerName: host}
	conn, err := tls.Dial("tcp", addr, tlsConfig)
	if err != nil {
		return fmt.Errorf("smtp tls dial: %w", err)
	}

	client, err := smtp.NewClient(conn, host)
	if err != nil {
		conn.Close()
		return fmt.Errorf("smtp new client: %w", err)
	}
	defer client.Close()

	// Authenticate
	auth := smtp.PlainAuth("", user, pass, host)
	if err := client.Auth(auth); err != nil {
		return fmt.Errorf("smtp auth: %w", err)
	}

	// Send
	if err := client.Mail(from); err != nil {
		return fmt.Errorf("smtp mail from: %w", err)
	}
	if err := client.Rcpt(to); err != nil {
		return fmt.Errorf("smtp rcpt to: %w", err)
	}

	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("smtp data: %w", err)
	}
	if _, err := w.Write([]byte(msg)); err != nil {
		return fmt.Errorf("smtp write: %w", err)
	}
	if err := w.Close(); err != nil {
		return fmt.Errorf("smtp close data: %w", err)
	}

	if err := client.Quit(); err != nil {
		slog.Warn("smtp quit error (non-fatal)", "error", err)
	}

	slog.Info("email sent via SMTP", "from", from, "to", to, "subject", subject)
	return nil
}

// getUserEmail looks up the email for a user from Supabase auth.users.
func (s *NotificationService) getUserEmail(ctx context.Context, userID uuid.UUID) string {
	var email string
	err := s.db.GetContext(ctx, &email,
		`SELECT email FROM auth.users WHERE id = $1`, userID)
	if err != nil {
		slog.Error("failed to get user email", "error", err, "user_id", userID)
		return ""
	}
	return email
}

func containsDay(arr pq.Int64Array, day int64) bool {
	for _, d := range arr {
		if d == day {
			return true
		}
	}
	return false
}

func derefStr(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}
