package unread

import (
	"context"
	"fmt"
	"strings"
	"time"
)

type taskBarrierSnapshotStore interface {
	UpsertTaskBarrierSnapshots(ctx context.Context, snapshots []Snapshot) error
}

// PersistTaskUnreadSnapshots is the durable completion barrier for one
// targeted Task delivery. The outbox must not wake Agents or become delivered
// until the exact frozen Human recipients' owner-local unread values survive a
// process restart.
//
// Snapshot persistence is serialized with the background flusher so an older
// captured batch from this process cannot commit after this barrier and
// rewrite its regular compatibility columns first. The barrier deliberately
// leaves dirty markers intact; the periodic flusher remains their single
// cleanup owner, but only the dedicated barrier store seam may advance the
// protected Task aggregate.
func (s *Service) PersistTaskUnreadSnapshots(
	ctx context.Context,
	workspaceID string,
	scopeID string,
	memberIDs []string,
) error {
	if s == nil || s.runtime == nil || s.store == nil {
		return fmt.Errorf("unread runtime snapshot persistence is not configured")
	}
	s.snapshotPersistMu.Lock()
	defer s.snapshotPersistMu.Unlock()

	rows, err := s.runtime.taskSnapshotRowsForMembers(
		workspaceID,
		scopeID,
		memberIDs,
		time.Now().UTC(),
	)
	if err != nil {
		return err
	}
	if len(rows) == 0 {
		return nil
	}
	barrierStore, ok := s.store.(taskBarrierSnapshotStore)
	if !ok {
		return fmt.Errorf("persist targeted Task unread snapshots: store does not support the Task barrier")
	}
	if err := barrierStore.UpsertTaskBarrierSnapshots(ctx, rows); err != nil {
		return fmt.Errorf("persist targeted Task unread snapshots: %w", err)
	}
	return nil
}

func (s *Service) enqueueTaskSnapshotHydrateIfNeeded(workspaceID, memberID, scopeID string) {
	if s == nil || s.store == nil || s.runtime == nil ||
		!s.runtime.taskSnapshotNeedsHydrate(workspaceID, memberID, scopeID) {
		return
	}
	s.enqueue(hydrateJob{
		kind:        hydrateJobSnapshot,
		workspaceID: workspaceID,
		memberID:    memberID,
		scopeIDs:    []string{scopeID},
	})
}

func frontierCursorsCoverLatest(metadata FrontierMetadata) bool {
	return frontierMessageCursorCoversLatest(metadata) && frontierActivityCursorCoversLatest(metadata)
}

func frontierMessageCursorCoversLatest(metadata FrontierMetadata) bool {
	return metadata.LatestMessageSeq <= 0 || metadata.LastMessageReadSeq >= metadata.LatestMessageSeq
}

func frontierActivityCursorCoversLatest(metadata FrontierMetadata) bool {
	return metadata.LatestActivitySeq <= 0 || metadata.LastActivityReadSeq >= metadata.LatestActivitySeq
}

func frontierMetadataEligibleForUnread(metadata FrontierMetadata) bool {
	// Task unread is event-recipient based. A later Unwatch must not erase an
	// unread direct mention or assignment event, so Task snapshot hydration
	// validates only active membership/visibility and deliberately ignores the
	// current notification setting.
	if metadata.ScopeType != ScopeTypeTask {
		switch strings.TrimSpace(metadata.NotificationState) {
		case "", "active", "quiet":
		default:
			return false
		}
	}
	switch strings.TrimSpace(metadata.MembershipVisibility) {
	case "", "public", "dm":
		return true
	default:
		return false
	}
}
