package message

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

type SidecarKind string

const (
	SidecarKindAttachment          SidecarKind = "attachment"
	SidecarKindSourceRef           SidecarKind = "source_ref"
	SidecarKindShareCard           SidecarKind = "share_card"
	SidecarKindReaction            SidecarKind = "reaction"
	SidecarKindPin                 SidecarKind = "pin"
	SidecarKindRecoveryPoint       SidecarKind = "recovery_point"
	SidecarKindDiagnostics         SidecarKind = "diagnostics"
	SidecarKindAppNotificationCard SidecarKind = "app_notification_card"
)

type SidecarReadRequest struct {
	WorkspaceID    string
	MessageID      string
	ViewerMemberID string
	ViewerType     string
	Kind           SidecarKind
}

type SidecarReadDecision struct {
	Allowed   bool
	Kind      SidecarKind
	MessageID string
	ScopeID   string
	Reason    string
	Message   *HistoryMessage
}

type SidecarReadBatchRequest struct {
	WorkspaceID string
	Kind        SidecarKind
	Items       []SidecarReadBatchItem
}

type SidecarReadBatchItem struct {
	RequestID      string
	MessageID      string
	ViewerMemberID string
}

// AuthorizeSidecarRead is the single message-owned authority gate for
// message-id-bound sidecars. Sidecar stores may load their own storage rows, but
// they must not decide visibility by scope/thread access or by copying
// owner-private ACL SQL.
func (service ScopeAPIService) AuthorizeSidecarRead(ctx context.Context, request SidecarReadRequest) (SidecarReadDecision, error) {
	kind := normalizeSidecarKind(request.Kind)
	if kind == "" {
		return SidecarReadDecision{}, invalidArgument("message sidecar kind is required")
	}
	messageID := strings.TrimSpace(request.MessageID)
	if strings.TrimSpace(request.WorkspaceID) == "" || messageID == "" || strings.TrimSpace(request.ViewerMemberID) == "" {
		return SidecarReadDecision{}, invalidArgument("message sidecar read requires workspace, message, and viewer")
	}
	result, err := service.CanReadMessage(ctx, ScopeAPIReadCheckRequest{
		WorkspaceID:    request.WorkspaceID,
		MessageID:      messageID,
		ViewerMemberID: request.ViewerMemberID,
		ViewerType:     firstNonEmpty(request.ViewerType, ScopeActorHuman),
		RequestKind:    sidecarVisibilityRequestKind(kind),
	})
	if err != nil {
		return SidecarReadDecision{}, err
	}
	decision := SidecarReadDecision{
		Allowed:   result.Allowed,
		Kind:      kind,
		MessageID: messageID,
		Reason:    strings.TrimSpace(result.Reason),
		Message:   result.Message,
	}
	if result.Message != nil {
		decision.ScopeID = strings.TrimSpace(result.Message.TargetKey)
	}
	if decision.Allowed && sidecarKindHiddenAfterRecall(kind) && result.Message != nil && MessageMetadataIsTombstone(result.Message.Metadata) {
		decision.Allowed = false
		if MessageMetadataIsRewound(result.Message.Metadata) {
			decision.Reason = "message_rewound"
		} else {
			decision.Reason = "message_recalled"
		}
		decision.Message = nil
	}
	return decision, nil
}

// AuthorizeSidecarReadBatch is the bounded message-owned read authority for
// sidecar callers that already have many message_id-bound resources. It keeps
// sidecar packages from copying private/public/grant ACL SQL while also avoiding
// per-resource CanReadMessage loops on task/context surfaces.
func (service ScopeAPIService) AuthorizeSidecarReadBatch(ctx context.Context, request SidecarReadBatchRequest) (map[string]SidecarReadDecision, error) {
	if err := service.requireDB(); err != nil {
		return nil, err
	}
	kind := normalizeSidecarKind(request.Kind)
	if kind == "" {
		return nil, invalidArgument("message sidecar kind is required")
	}
	workspaceID := strings.TrimSpace(request.WorkspaceID)
	if workspaceID == "" {
		return nil, invalidArgument("message sidecar read batch requires workspace")
	}
	items := normalizeSidecarReadBatchItems(request.Items)
	if len(items) == 0 {
		return map[string]SidecarReadDecision{}, nil
	}
	recallPredicate := ""
	if sidecarKindHiddenAfterRecall(kind) {
		recallPredicate = `
      AND ` + MessageNotLifecycleTombstoneSQL("m") + `
`
	}
	requestsCTE, requestArgs, _, err := buildSidecarReadBatchRequestsCTE("requested_sidecars", items, 2)
	if err != nil {
		return nil, err
	}
	query := fmt.Sprintf(`
WITH %s,
allowed AS (
    SELECT
        requested.request_id,
        m.id AS message_id,
        m.scope_id,
        `+scopeReadableMessageReasonSQL+` AS reason
    FROM requested_sidecars requested
    JOIN scope_messages m
      ON m.workspace_id = $1
     AND m.id = requested.message_id
    JOIN scopes message_scope
      ON message_scope.id = m.scope_id
     AND message_scope.workspace_id = m.workspace_id
     AND message_scope.status = 'active'
    `+scopeReadableMembershipJoinSQL("requested.viewer_member_id")+`
`+scopePrivateAuthorityGrantJoinsSQL+`
    WHERE `+scopeReadableMessagePredicateSQL+recallPredicate+`
)
SELECT request_id, message_id, scope_id, reason
FROM allowed
`, requestsCTE)
	rows, err := service.db.Query(ctx, query, append([]any{workspaceID}, requestArgs...)...)
	if err != nil {
		return nil, fmt.Errorf("authorize message sidecar read batch: %w", err)
	}
	defer rows.Close()

	decisions := make(map[string]SidecarReadDecision, len(items))
	for rows.Next() {
		var requestID, messageID, scopeID, reason string
		if err := rows.Scan(&requestID, &messageID, &scopeID, &reason); err != nil {
			return nil, fmt.Errorf("scan message sidecar read batch: %w", err)
		}
		requestID = strings.TrimSpace(requestID)
		if requestID == "" {
			continue
		}
		reason = strings.TrimSpace(reason)
		if !knownScopeVisibilityReason(reason) {
			continue
		}
		if _, exists := decisions[requestID]; exists {
			continue
		}
		decisions[requestID] = SidecarReadDecision{
			Allowed:   true,
			Kind:      kind,
			MessageID: strings.TrimSpace(messageID),
			ScopeID:   strings.TrimSpace(scopeID),
			Reason:    reason,
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate message sidecar read batch: %w", err)
	}
	return decisions, nil
}

func sidecarKindHiddenAfterRecall(kind SidecarKind) bool {
	switch normalizeSidecarKind(kind) {
	case SidecarKindDiagnostics, SidecarKindRecoveryPoint:
		return false
	case "":
		return false
	default:
		return true
	}
}

func normalizeSidecarReadBatchItems(items []SidecarReadBatchItem) []SidecarReadBatchItem {
	normalized := make([]SidecarReadBatchItem, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for _, item := range items {
		candidate := SidecarReadBatchItem{
			RequestID:      strings.TrimSpace(item.RequestID),
			MessageID:      strings.TrimSpace(item.MessageID),
			ViewerMemberID: strings.TrimSpace(item.ViewerMemberID),
		}
		if candidate.RequestID == "" || candidate.MessageID == "" || candidate.ViewerMemberID == "" {
			continue
		}
		key := strings.Join([]string{candidate.RequestID, candidate.MessageID, candidate.ViewerMemberID}, "\x00")
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, candidate)
	}
	return normalized
}

func buildSidecarReadBatchRequestsCTE(name string, items []SidecarReadBatchItem, startParam int) (string, []any, int, error) {
	trimmedName := strings.TrimSpace(name)
	if trimmedName == "" {
		return "", nil, startParam, fmt.Errorf("cte name is required")
	}
	if len(items) == 0 {
		return "", nil, startParam, fmt.Errorf("sidecar read batch items are empty")
	}
	if startParam <= 0 {
		return "", nil, startParam, fmt.Errorf("invalid start parameter %d", startParam)
	}
	var builder strings.Builder
	builder.WriteString(trimmedName)
	builder.WriteString(" AS (\n")
	args := make([]any, 0, len(items)*3)
	param := startParam
	for index, item := range items {
		if index > 0 {
			builder.WriteString("\n    UNION ALL\n")
		}
		builder.WriteString("    SELECT $")
		builder.WriteString(strconv.Itoa(param))
		builder.WriteString(" AS request_id, $")
		builder.WriteString(strconv.Itoa(param + 1))
		builder.WriteString(" AS message_id, $")
		builder.WriteString(strconv.Itoa(param + 2))
		builder.WriteString(" AS viewer_member_id")
		args = append(args, item.RequestID, item.MessageID, item.ViewerMemberID)
		param += 3
	}
	builder.WriteString("\n)")
	return builder.String(), args, param, nil
}

func normalizeSidecarKind(kind SidecarKind) SidecarKind {
	switch SidecarKind(strings.TrimSpace(string(kind))) {
	case SidecarKindAttachment:
		return SidecarKindAttachment
	case SidecarKindSourceRef:
		return SidecarKindSourceRef
	case SidecarKindShareCard:
		return SidecarKindShareCard
	case SidecarKindReaction:
		return SidecarKindReaction
	case SidecarKindPin:
		return SidecarKindPin
	case SidecarKindRecoveryPoint:
		return SidecarKindRecoveryPoint
	case SidecarKindDiagnostics:
		return SidecarKindDiagnostics
	case SidecarKindAppNotificationCard:
		return SidecarKindAppNotificationCard
	default:
		return ""
	}
}

func sidecarVisibilityRequestKind(kind SidecarKind) string {
	switch normalizeSidecarKind(kind) {
	case SidecarKindAttachment:
		return ScopeVisibilityRequestAttachment
	case SidecarKindDiagnostics, SidecarKindRecoveryPoint:
		return ScopeVisibilityRequestDiagnostics
	case SidecarKindSourceRef, SidecarKindShareCard:
		return ScopeVisibilityRequestSourceRef
	case SidecarKindReaction, SidecarKindPin, SidecarKindAppNotificationCard:
		return ScopeVisibilityRequestHistory
	default:
		return ScopeVisibilityRequestHistory
	}
}

func (kind SidecarKind) Validate() error {
	if normalizeSidecarKind(kind) == "" {
		return fmt.Errorf("unknown message sidecar kind %q", strings.TrimSpace(string(kind)))
	}
	return nil
}
