package message

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

const (
	ScopeVisibilityRequestHistory       = "history"
	ScopeVisibilityRequestSearch        = "search"
	ScopeVisibilityRequestRootContext   = "root_context"
	ScopeVisibilityRequestSourceRef     = "source_ref"
	ScopeVisibilityRequestAttachment    = "attachment"
	ScopeVisibilityRequestDiagnostics   = "diagnostics"
	ScopeVisibilityReasonMembership     = "membership_interval"
	ScopeVisibilityReasonPrivateOutput  = "private_own_output"
	ScopeVisibilityReasonAuthorInterval = "author_interval"
	ScopeVisibilityReasonPrivateGrant   = "private_grant_interval"
)

// VisibilityFilter owns ScopeAPI visible-row pagination. Production read-row
// authority lives in SQL (scopeReadableMessagePredicateSQL); this Go layer only
// maps SQL-visible rows, deduplicates, paginates, and fails closed if a
// repository row does not carry the required authority metadata.
type VisibilityFilter struct{}

func NewVisibilityFilter() VisibilityFilter {
	return VisibilityFilter{}
}

type VisibilityFilterRequest struct {
	ViewerMemberID      string
	ViewerType          string
	ScopeID             string
	RequestKind         string
	BeforeSeq           int64
	Limit               int
	CandidateBatchLimit int
	MaxCandidateBatches int
}

type VisibilityFilterResult struct {
	Messages                 []VisibleMessageRef
	NextCursor               int64
	NextSearchCursor         MessageSearchCursor
	HasMore                  bool
	CandidateBudgetExhausted bool
}

type VisibleMessageRef struct {
	Message    ScopeMessageRecord
	Reason     string
	Membership *ScopeMembershipRecord
	GrantID    string
	SearchRank int
}

func (filter VisibilityFilter) ListVisibleMessages(ctx context.Context, store MessageStore, request VisibilityFilterRequest) (VisibilityFilterResult, error) {
	viewerMemberID := strings.TrimSpace(request.ViewerMemberID)
	scopeID := strings.TrimSpace(request.ScopeID)
	if viewerMemberID == "" || scopeID == "" {
		return VisibilityFilterResult{}, fmt.Errorf("visibility filter requires viewer member and scope")
	}
	limit := request.Limit
	if limit <= 0 {
		limit = 50
	}
	result := VisibilityFilterResult{
		Messages: make([]VisibleMessageRef, 0, limit),
	}
	cursor := request.BeforeSeq
	batchLimit := visibilityCandidateBatchLimit(limit, request.CandidateBatchLimit)
	maxBatches := visibilityMaxCandidateBatches(request.MaxCandidateBatches)
	seenMessageIDs := make(map[string]struct{}, limit)
	for batch := 0; batch < maxBatches && len(result.Messages) < limit; batch++ {
		candidates, err := store.ListVisibilityCandidatesBeforeSeq(ctx, scopeID, viewerMemberID, cursor, batchLimit)
		if err != nil {
			return VisibilityFilterResult{}, err
		}
		if len(candidates) == 0 {
			break
		}
		lastScannedSeq := int64(0)
		for _, candidate := range candidates {
			lastScannedSeq = candidate.Message.Seq
			cursor = lastScannedSeq
			messageID := strings.TrimSpace(candidate.Message.ID)
			if _, ok := seenMessageIDs[messageID]; ok && messageID != "" {
				continue
			}
			if len(result.Messages) >= limit {
				continue
			}
			visible, ref := filter.visibleCandidate(candidate, request)
			if !visible {
				continue
			}
			if messageID != "" {
				seenMessageIDs[messageID] = struct{}{}
			}
			result.Messages = append(result.Messages, ref)
			if len(result.Messages) >= limit {
				result.HasMore = true
				break
			}
		}
		result.NextCursor = lastScannedSeq
		if len(candidates) < batchLimit {
			break
		}
		if batch == maxBatches-1 && len(result.Messages) < limit {
			result.CandidateBudgetExhausted = true
			result.HasMore = true
		}
	}
	return result, nil
}

func (filter VisibilityFilter) FilterCandidates(candidates []ScopeMessageVisibilityCandidate, request VisibilityFilterRequest) VisibilityFilterResult {
	result := VisibilityFilterResult{
		Messages: make([]VisibleMessageRef, 0, len(candidates)),
	}
	seenMessageIDs := make(map[string]struct{}, len(candidates))
	for _, candidate := range candidates {
		messageID := strings.TrimSpace(candidate.Message.ID)
		if _, ok := seenMessageIDs[messageID]; ok && messageID != "" {
			continue
		}
		visible, ref := filter.visibleCandidate(candidate, request)
		if !visible {
			continue
		}
		if messageID != "" {
			seenMessageIDs[messageID] = struct{}{}
		}
		result.Messages = append(result.Messages, ref)
		if result.NextCursor == 0 || candidate.Message.Seq < result.NextCursor {
			result.NextCursor = candidate.Message.Seq
		}
	}
	return result
}

func (filter VisibilityFilter) visibleCandidate(candidate ScopeMessageVisibilityCandidate, request VisibilityFilterRequest) (bool, VisibleMessageRef) {
	viewerMemberID := strings.TrimSpace(request.ViewerMemberID)
	if strings.TrimSpace(candidate.Membership.MemberID) != viewerMemberID {
		return false, VisibleMessageRef{}
	}
	reason := strings.TrimSpace(candidate.Reason)
	if !knownScopeVisibilityReason(reason) {
		return false, VisibleMessageRef{}
	}
	grantID := strings.TrimSpace(candidate.GrantID)
	if reason == ScopeVisibilityReasonPrivateGrant && grantID == "" {
		return false, VisibleMessageRef{}
	}
	membership := candidate.Membership
	ref := VisibleMessageRef{
		Message:    candidate.Message,
		Reason:     reason,
		Membership: &membership,
		SearchRank: candidate.SearchRank,
	}
	if reason == ScopeVisibilityReasonPrivateGrant {
		ref.GrantID = grantID
	}
	return true, ref
}

func knownScopeVisibilityReason(reason string) bool {
	switch strings.TrimSpace(reason) {
	case ScopeVisibilityReasonMembership,
		ScopeVisibilityReasonPrivateOutput,
		ScopeVisibilityReasonAuthorInterval,
		ScopeVisibilityReasonPrivateGrant:
		return true
	default:
		return false
	}
}

func visibilityCandidateBatchLimit(pageLimit int, override int) int {
	if override > 0 {
		if override > 500 {
			return 500
		}
		return override
	}
	batchLimit := pageLimit * 4
	if batchLimit < 50 {
		batchLimit = 50
	}
	if batchLimit > 500 {
		batchLimit = 500
	}
	return batchLimit
}

func visibilityMaxCandidateBatches(override int) int {
	if override > 0 {
		if override > 20 {
			return 20
		}
		return override
	}
	return 4
}

func membershipCoversMessage(membership ScopeMembershipRecord, seq int64) bool {
	if strings.TrimSpace(membership.ScopeID) == "" || strings.TrimSpace(membership.MemberID) == "" || membership.RemovedAt != nil {
		return false
	}
	if membership.EffectiveFromSeq > 0 && seq < membership.EffectiveFromSeq {
		return false
	}
	if membership.EffectiveToSeq != nil && seq > *membership.EffectiveToSeq {
		return false
	}
	return true
}
