package collaboration

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/tim/cairnquire/apps/server/internal/auth"
	"github.com/tim/cairnquire/apps/server/internal/realtime"
)

type EmailSender interface {
	Send(ctx context.Context, to []string, subject, body string) error
}

type NoOpEmailSender struct{}

func (n *NoOpEmailSender) Send(ctx context.Context, to []string, subject, body string) error {
	slog.Info("email stub", "to", to, "subject", subject, "body_len", len(body))
	return nil
}

type Service struct {
	repo   *Repository
	email  EmailSender
	hub    *realtime.Hub
	logger *slog.Logger
}

func NewService(repo *Repository, email EmailSender, hub *realtime.Hub, logger *slog.Logger) *Service {
	if email == nil {
		email = &NoOpEmailSender{}
	}
	return &Service{
		repo:   repo,
		email:  email,
		hub:    hub,
		logger: logger,
	}
}

// Comments

func (s *Service) CreateComment(ctx context.Context, principal auth.Principal, input CreateCommentInput) (*Comment, error) {
	if principal.UserID == "" {
		return nil, fmt.Errorf("authentication required")
	}
	if input.Content == "" {
		return nil, fmt.Errorf("content is required")
	}
	if input.DocumentID == "" {
		return nil, fmt.Errorf("document_id is required")
	}

	comment := Comment{
		ID:          randomID("comment"),
		DocumentID:  input.DocumentID,
		VersionHash: input.VersionHash,
		AuthorID:    principal.UserID,
		Content:     input.Content,
		CreatedAt:   time.Now().UTC(),
	}
	if input.ParentID != nil && *input.ParentID != "" {
		comment.ParentID = input.ParentID
	}
	if input.AnchorHash != nil && *input.AnchorHash != "" {
		comment.AnchorHash = input.AnchorHash
	}
	if input.AnchorLine != nil && *input.AnchorLine > 0 {
		comment.AnchorLine = input.AnchorLine
	}

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

	// Create notifications for watchers
	if err := s.notifyWatchersOfComment(ctx, comment); err != nil {
		s.logger.Warn("failed to notify watchers of comment", "error", err)
	}

	// Broadcast to connected clients
	s.broadcastNotification(principal.UserID)

	return s.repo.GetComment(ctx, comment.ID)
}

func (s *Service) ListComments(ctx context.Context, documentID string) ([]Comment, error) {
	return s.repo.ListCommentsByDocument(ctx, documentID)
}

func (s *Service) ListCommentsByAnchor(ctx context.Context, documentID string, anchorHash string) ([]Comment, error) {
	return s.repo.ListCommentsByAnchor(ctx, documentID, anchorHash)
}

func (s *Service) GetComment(ctx context.Context, commentID string) (*Comment, error) {
	return s.repo.GetComment(ctx, commentID)
}

func (s *Service) ResolveComment(ctx context.Context, principal auth.Principal, commentID string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.ResolveComment(ctx, commentID, principal.UserID)
}

// Notifications

func (s *Service) ListNotifications(ctx context.Context, principal auth.Principal, unreadOnly bool, limit int) ([]Notification, error) {
	if principal.UserID == "" {
		return nil, fmt.Errorf("authentication required")
	}
	return s.repo.ListNotifications(ctx, principal.UserID, unreadOnly, limit)
}

func (s *Service) GetBellStatus(ctx context.Context, principal auth.Principal) (*NotificationBell, error) {
	if principal.UserID == "" {
		return &NotificationBell{UnreadCount: 0, Items: []Notification{}}, nil
	}
	count, err := s.repo.CountUnreadNotifications(ctx, principal.UserID)
	if err != nil {
		return nil, err
	}
	items, err := s.repo.ListNotifications(ctx, principal.UserID, false, 20)
	if err != nil {
		return nil, err
	}
	return &NotificationBell{
		UnreadCount: count,
		Items:       items,
	}, nil
}

func (s *Service) MarkNotificationRead(ctx context.Context, principal auth.Principal, notificationID string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.MarkNotificationRead(ctx, notificationID, principal.UserID)
}

func (s *Service) MarkAllNotificationsRead(ctx context.Context, principal auth.Principal) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.MarkAllNotificationsRead(ctx, principal.UserID)
}

// Watchers

func (s *Service) WatchDocument(ctx context.Context, principal auth.Principal, documentID string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.AddWatcher(ctx, Watcher{
		UserID:     principal.UserID,
		DocumentID: &documentID,
		CreatedAt:  time.Now().UTC(),
	})
}

func (s *Service) UnwatchDocument(ctx context.Context, principal auth.Principal, documentID string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.RemoveWatcher(ctx, principal.UserID, &documentID, nil)
}

func (s *Service) WatchFolder(ctx context.Context, principal auth.Principal, folderPath string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.AddWatcher(ctx, Watcher{
		UserID:     principal.UserID,
		FolderPath: &folderPath,
		CreatedAt:  time.Now().UTC(),
	})
}

func (s *Service) UnwatchFolder(ctx context.Context, principal auth.Principal, folderPath string) error {
	if principal.UserID == "" {
		return fmt.Errorf("authentication required")
	}
	return s.repo.RemoveWatcher(ctx, principal.UserID, nil, &folderPath)
}

func (s *Service) IsWatching(ctx context.Context, principal auth.Principal, documentID string) (bool, error) {
	if principal.UserID == "" {
		return false, nil
	}
	return s.repo.IsWatching(ctx, principal.UserID, documentID)
}

// Document change notifications

func (s *Service) NotifyDocumentChanged(ctx context.Context, documentID string, documentPath string, actorName string) error {
	watchers, err := s.repo.ListWatchersByDocument(ctx, documentID)
	if err != nil {
		return fmt.Errorf("list document watchers: %w", err)
	}
	folderWatchers, err := s.repo.ListWatchersByFolder(ctx, documentPath)
	if err != nil {
		return fmt.Errorf("list folder watchers: %w", err)
	}

	seen := make(map[string]bool)
	for _, w := range append(watchers, folderWatchers...) {
		if seen[w.UserID] {
			continue
		}
		seen[w.UserID] = true

		msg := fmt.Sprintf("%s updated %s", actorName, documentPath)
		notification := Notification{
			ID:           randomID("notif"),
			UserID:       w.UserID,
			Type:         "file_changed",
			ResourceType: "document",
			ResourceID:   documentID,
			Message:      msg,
			CreatedAt:    time.Now().UTC(),
		}
		if err := s.repo.CreateNotification(ctx, notification); err != nil {
			s.logger.Warn("create notification", "error", err)
			continue
		}
	}

	// Broadcast bell update to all connected clients
	s.broadcastNotification("")

	return nil
}

func (s *Service) notifyWatchersOfComment(ctx context.Context, comment Comment) error {
	watchers, err := s.repo.ListWatchersByDocument(ctx, comment.DocumentID)
	if err != nil {
		return err
	}

	for _, w := range watchers {
		if w.UserID == comment.AuthorID {
			continue
		}
		msg := fmt.Sprintf("New comment on document")
		if comment.AnchorHash != nil {
			msg = fmt.Sprintf("New comment on a section of the document")
		}
		notification := Notification{
			ID:           randomID("notif"),
			UserID:       w.UserID,
			Type:         "comment",
			ResourceType: "document",
			ResourceID:   comment.DocumentID,
			Message:      msg,
			CreatedAt:    time.Now().UTC(),
		}
		if err := s.repo.CreateNotification(ctx, notification); err != nil {
			s.logger.Warn("create comment notification", "error", err)
		}
	}
	return nil
}

func (s *Service) broadcastNotification(userID string) {
	if s.hub == nil {
		return
	}
	s.hub.Broadcast(realtime.Event{
		Type: "notification",
		Data: map[string]string{"userId": userID},
	})
}

func (s *Service) SetEmailSender(email EmailSender) {
	if email != nil {
		s.email = email
	}
}
