package handlers

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/models"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/repository"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/services"
	"gitlab.com/patrickbritton3/sojorn/go-backend/pkg/utils"
)

type PostHandler struct {
	postRepo            *repository.PostRepository
	userRepo            *repository.UserRepository
	feedService         *services.FeedService
	assetService        *services.AssetService
	notificationService *services.NotificationService
	moderationService   *services.ModerationService
	contentFilter       *services.ContentFilter
	openRouterService   *services.OpenRouterService
	linkPreviewService  *services.LinkPreviewService
	localAIService      *services.LocalAIService
	videoProcessor      *services.VideoProcessor
}

func NewPostHandler(postRepo *repository.PostRepository, userRepo *repository.UserRepository, feedService *services.FeedService, assetService *services.AssetService, notificationService *services.NotificationService, moderationService *services.ModerationService, contentFilter *services.ContentFilter, openRouterService *services.OpenRouterService, linkPreviewService *services.LinkPreviewService, localAIService *services.LocalAIService, s3Client *s3.Client, videoBucket, vidDomain string) *PostHandler {
	return &PostHandler{
		postRepo:            postRepo,
		userRepo:            userRepo,
		feedService:         feedService,
		assetService:        assetService,
		notificationService: notificationService,
		moderationService:   moderationService,
		contentFilter:       contentFilter,
		openRouterService:   openRouterService,
		linkPreviewService:  linkPreviewService,
		localAIService:      localAIService,
		videoProcessor:      services.NewVideoProcessor(s3Client, videoBucket, vidDomain),
	}
}

// enrichLinkPreviews populates link_preview fields on a slice of posts via batch query.
func (h *PostHandler) enrichLinkPreviews(ctx context.Context, posts []models.Post) {
	if h.linkPreviewService == nil || len(posts) == 0 {
		return
	}
	ids := make([]string, len(posts))
	for i, p := range posts {
		ids[i] = p.ID.String()
	}
	previews, err := h.linkPreviewService.EnrichPostsWithLinkPreviews(ctx, ids)
	if err != nil || len(previews) == 0 {
		return
	}
	for i := range posts {
		if lp, ok := previews[posts[i].ID.String()]; ok {
			posts[i].LinkPreviewURL = &lp.URL
			posts[i].LinkPreviewTitle = &lp.Title
			posts[i].LinkPreviewDescription = &lp.Description
			signed := h.assetService.SignImageURL(lp.ImageURL)
			posts[i].LinkPreviewImageURL = &signed
			posts[i].LinkPreviewSiteName = &lp.SiteName
		}
	}
}

// enrichSinglePostLinkPreview populates link_preview fields on a single post.
func (h *PostHandler) enrichSinglePostLinkPreview(ctx context.Context, post *models.Post) {
	if h.linkPreviewService == nil || post == nil {
		return
	}
	previews, err := h.linkPreviewService.EnrichPostsWithLinkPreviews(ctx, []string{post.ID.String()})
	if err != nil || len(previews) == 0 {
		return
	}
	if lp, ok := previews[post.ID.String()]; ok {
		post.LinkPreviewURL = &lp.URL
		post.LinkPreviewTitle = &lp.Title
		post.LinkPreviewDescription = &lp.Description
		signed := h.assetService.SignImageURL(lp.ImageURL)
		post.LinkPreviewImageURL = &signed
		post.LinkPreviewSiteName = &lp.SiteName
	}
}

func (h *PostHandler) CreateComment(c *gin.Context) {
	userIDStr, _ := c.Get("user_id")
	userID, _ := uuid.Parse(userIDStr.(string))
	postID := c.Param("id")

	var req struct {
		Body string `json:"body" binding:"required,max=500"`
	}

	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	parentUUID, err := uuid.Parse(postID)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
		return
	}

	// Layer 0: Hard blocklist check — reject immediately, never save
	if h.contentFilter != nil {
		result := h.contentFilter.CheckContent(req.Body)
		if result.Blocked {
			strikeCount, consequence, _ := h.contentFilter.RecordStrikeWithIP(c.Request.Context(), userID, result.Category, req.Body, c.ClientIP())
			c.JSON(http.StatusUnprocessableEntity, gin.H{
				"error":       result.Message,
				"blocked":     true,
				"category":    result.Category,
				"strikes":     strikeCount,
				"consequence": consequence,
			})
			return
		}
	}

	tags := utils.ExtractHashtags(req.Body)
	tone := "neutral"
	cis := 0.8

	// AI Moderation — runs whichever engine the admin selected in ai_moderation_config
	var cachedScores *services.ThreePoisonsScore
	var cachedReason string
	commentStatus := "active"
	ctx := c.Request.Context()

	{
		selectedEngine := ""
		if h.openRouterService != nil {
			cfg, cfgErr := h.openRouterService.GetModerationConfig(ctx, "text")
			if cfgErr == nil && cfg != nil && cfg.Enabled && len(cfg.Engines) > 0 {
				selectedEngine = cfg.Engines[0]
			}
		}

		switch selectedEngine {
		case "local_ai":
			if h.localAIService != nil && req.Body != "" {
				localResult, localErr := h.localAIService.ModerateText(ctx, req.Body)
				if localErr != nil {
					log.Debug().Err(localErr).Msg("Local AI moderation unavailable for comment")
				} else if localResult != nil && !localResult.Allowed {
					commentStatus = "removed"
					log.Warn().Str("reason", localResult.Reason).Msg("Comment flagged by local AI")
				}
			}
		case "openai":
			if h.moderationService != nil {
				scores, reason, modErr := h.moderationService.AnalyzeContent(ctx, req.Body, []string{})
				if modErr == nil {
					cachedScores = scores
					cachedReason = reason
					cis = (scores.Hate + scores.Greed + scores.Delusion) / 3.0
					cis = 1.0 - cis
					tone = reason
					if reason != "" {
						commentStatus = "pending_moderation"
					}
				}
			}
		case "openrouter":
			if h.openRouterService != nil && req.Body != "" {
				textResult, textErr := h.openRouterService.ModerateText(ctx, req.Body)
				if textErr == nil && textResult != nil {
					if textResult.Action == "flag" {
						commentStatus = "removed"
					} else if textResult.Action == "nsfw" {
						commentStatus = "pending_moderation"
					}
					if textResult.Hate > 0 || textResult.Greed > 0 || textResult.Delusion > 0 {
						orCis := 1.0 - (textResult.Hate+textResult.Greed+textResult.Delusion)/3.0
						cis = orCis
					}
				}
			}
		default:
			log.Debug().Msg("No moderation engine configured in admin settings")
		}
	}

	post := &models.Post{
		AuthorID:       userID,
		Body:           req.Body,
		Status:         commentStatus,
		ToneLabel:      &tone,
		CISScore:       &cis,
		BodyFormat:     "plain",
		Tags:           tags,
		IsBeacon:       false,
		IsActiveBeacon: false,
		AllowChain:     true,
		Visibility:     "public",
		ChainParentID:  &parentUUID,
	}

	if err := h.postRepo.CreatePost(c.Request.Context(), post); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create comment", "details": err.Error()})
		return
	}

	comment := &models.Comment{
		ID:        post.ID,
		PostID:    postID,
		AuthorID:  post.AuthorID,
		Body:      post.Body,
		Status:    post.Status,
		CreatedAt: post.CreatedAt,
	}

	// Flag comment if needed — reuse cached scores
	if h.moderationService != nil && post.Status == "pending_moderation" && cachedScores != nil {
		_ = h.moderationService.FlagComment(c.Request.Context(), post.ID, cachedScores, cachedReason)
	}

	// Log AI moderation decision for comment — async
	if h.moderationService != nil {
		postID := post.ID
		postStatus := post.Status
		reqBody := req.Body
		go func() {
			decision := "pass"
			if postStatus == "pending_moderation" {
				decision = "flag"
			}
			invCis := 1.0 - cis
			scores := &services.ThreePoisonsScore{Hate: invCis, Greed: 0, Delusion: 0}
			h.moderationService.LogAIDecision(context.Background(), "comment", postID, userID, reqBody, scores, nil, decision, tone, "", nil)
		}()
	}

	// Get post details for notification
	rootPost, err := h.postRepo.GetPostByID(c.Request.Context(), postID, userIDStr.(string))
	if err == nil && rootPost.AuthorID.String() != userIDStr.(string) {
		// Get actor details
		actor, err := h.userRepo.GetProfileByID(c.Request.Context(), userIDStr.(string))
		if err == nil && h.notificationService != nil {
			// Determine post type for proper deep linking
			postType := "standard"
			if rootPost.IsBeacon {
				postType = "beacon"
			} else if rootPost.VideoURL != nil && *rootPost.VideoURL != "" {
				postType = "quip"
			}

			commentIDStr := comment.ID.String()
			metadata := map[string]interface{}{
				"actor_name": actor.DisplayName,
				"post_id":    postID,
				"post_type":  postType,
			}
			go h.notificationService.CreateNotification(
				context.Background(),
				rootPost.AuthorID.String(),
				userIDStr.(string),
				"comment",
				&postID,
				&commentIDStr,
				metadata,
			)
		}
	}

	c.JSON(http.StatusCreated, gin.H{"comment": comment})
}

func (h *PostHandler) GetNearbyBeacons(c *gin.Context) {
	lat := utils.GetQueryFloat(c, "lat", 0)
	long := utils.GetQueryFloat(c, "long", 0)
	radius := utils.GetQueryInt(c, "radius", 16000)

	beacons, err := h.postRepo.GetNearbyBeacons(c.Request.Context(), lat, long, radius)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch nearby beacons", "details": err.Error()})
		return
	}

	// Transform to beacon-specific JSON with correct field names for Flutter client
	results := make([]gin.H, 0, len(beacons))
	for _, b := range beacons {
		item := gin.H{
			"id":                  b.ID,
			"body":                b.Body,
			"author_id":           b.AuthorID,
			"beacon_type":         b.BeaconType,
			"confidence_score":    b.Confidence,
			"is_active_beacon":    b.IsActiveBeacon,
			"created_at":          b.CreatedAt,
			"image_url":           b.ImageURL,
			"tags":                b.Tags,
			"beacon_lat":          b.Lat,
			"beacon_long":         b.Long,
			"severity":            b.Severity,
			"incident_status":     b.IncidentStatus,
			"radius":              b.Radius,
			"vouch_count":         b.LikeCount,    // mapped from vouch subquery
			"report_count":        b.CommentCount, // mapped from report subquery
			"verification_count":  b.LikeCount,    // vouches = verification
			"status_color":        beaconStatusColor(b.Confidence),
			"author_handle":       "Anonymous",
			"author_display_name": "Anonymous",
		}
		results = append(results, item)
	}

	c.JSON(http.StatusOK, gin.H{"beacons": results})
}

// CreateBeacon creates an anonymous beacon pin on the map.
// The author_id is stored for abuse tracking but NEVER exposed in responses.
// AI moderation runs for text + images — flagged beacons stay visible but go to admin review.
// Does NOT create a feed post.
func (h *PostHandler) CreateBeacon(c *gin.Context) {
	userIDStr, _ := c.Get("user_id")
	userID, _ := uuid.Parse(userIDStr.(string))

	var req struct {
		Body       string  `json:"body" binding:"required"`
		BeaconType string  `json:"beacon_type" binding:"required"`
		Lat        float64 `json:"lat" binding:"required"`
		Long       float64 `json:"long" binding:"required"`
		Severity   string  `json:"severity"`
		ImageURL   *string `json:"image_url"`
		TTLHours   *int    `json:"ttl_hours"`
	}

	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "details": err.Error()})
		return
	}

	if req.Lat < -90 || req.Lat > 90 || req.Long < -180 || req.Long > 180 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid coordinates"})
		return
	}

	severity := "medium"
	if req.Severity != "" {
		severity = req.Severity
	}

	var expiresAt *time.Time
	if req.TTLHours != nil && *req.TTLHours > 0 {
		t := time.Now().Add(time.Duration(*req.TTLHours) * time.Hour)
		expiresAt = &t
	}

	beaconType := req.BeaconType
	lat := req.Lat
	long := req.Long

	post := &models.Post{
		AuthorID:       userID,
		Body:           req.Body,
		Status:         "active",
		BodyFormat:     "plain",
		Tags:           []string{},
		IsBeacon:       true,
		BeaconType:     &beaconType,
		Lat:            &lat,
		Long:           &long,
		Severity:       severity,
		IncidentStatus: "active",
		Radius:         500,
		Confidence:     0.5,
		IsActiveBeacon: true,
		AllowChain:     false,
		Visibility:     "public",
		ExpiresAt:      expiresAt,
		ImageURL:       req.ImageURL,
	}

	// AI Moderation — text + image. Flagged beacons stay active but get flagged for admin review.
	modFlagged := false
	if h.moderationService != nil {
		mediaURLs := []string{}
		if req.ImageURL != nil && *req.ImageURL != "" {
			mediaURLs = append(mediaURLs, *req.ImageURL)
		}
		scores, reason, err := h.moderationService.AnalyzeContent(c.Request.Context(), req.Body, mediaURLs)
		if err == nil && reason != "" {
			modFlagged = true
			cis := 1.0 - (scores.Hate+scores.Greed+scores.Delusion)/3.0
			post.CISScore = &cis
			post.ToneLabel = &reason
			log.Warn().Str("reason", reason).Float64("cis", cis).Msg("Beacon flagged by moderation — stays active for admin review")
		}
	}
	// Local AI moderation (llama-guard, on-server, free, fast)
	if h.localAIService != nil && req.Body != "" {
		localResult, localErr := h.localAIService.ModerateText(c.Request.Context(), req.Body)
		if localErr != nil {
			log.Debug().Err(localErr).Msg("Local AI moderation unavailable for beacon, falling through")
		} else if localResult != nil && !localResult.Allowed {
			post.Status = "removed"
			modFlagged = true
			log.Warn().Str("reason", localResult.Reason).Msg("Beacon flagged by local AI")
		}
	}
	if h.openRouterService != nil {
		// Try beacon-specific config first, fall back to generic
		textResult, textErr := h.openRouterService.ModerateWithType(c.Request.Context(), "beacon_text", req.Body, nil)
		if textResult == nil || textErr != nil {
			textResult, _ = h.openRouterService.ModerateText(c.Request.Context(), req.Body)
		}
		if textResult != nil {
			if textResult.Action == "flag" {
				post.Status = "removed"
				modFlagged = true
				log.Warn().Msg("Beacon removed by OpenRouter — not allowed content")
			} else if textResult.Action == "nsfw" {
				post.IsNSFW = true
				modFlagged = true
			}
		}
		if req.ImageURL != nil && *req.ImageURL != "" && post.Status == "active" {
			imgResult, imgErr := h.openRouterService.ModerateWithType(c.Request.Context(), "beacon_image", "", []string{*req.ImageURL})
			if imgResult == nil || imgErr != nil {
				imgResult, _ = h.openRouterService.ModerateImage(c.Request.Context(), *req.ImageURL)
			}
			if imgResult != nil {
				if imgResult.Action == "flag" {
					post.Status = "removed"
					modFlagged = true
				} else if imgResult.Action == "nsfw" {
					post.IsNSFW = true
					modFlagged = true
				}
			}
		}
	}

	if err := h.postRepo.CreatePost(c.Request.Context(), post); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create beacon", "details": err.Error()})
		return
	}

	// Flag for admin review if moderation triggered (beacon stays active unless "removed")
	if modFlagged && h.moderationService != nil && post.Status != "removed" {
		_ = h.moderationService.FlagPost(c.Request.Context(), post.ID, nil, "beacon_moderation_flag")
	}

	log.Info().
		Str("beacon_id", post.ID.String()).
		Str("beacon_type", beaconType).
		Str("status", post.Status).
		Bool("mod_flagged", modFlagged).
		Float64("lat", lat).
		Float64("long", long).
		Msg("Beacon created anonymously")

	// Return anonymous beacon data — no author info
	c.JSON(http.StatusCreated, gin.H{
		"beacon": gin.H{
			"id":               post.ID,
			"body":             post.Body,
			"beacon_type":      beaconType,
			"beacon_lat":       lat,
			"beacon_long":      long,
			"severity":         severity,
			"confidence_score": post.Confidence,
			"is_active_beacon": true,
			"incident_status":  "active",
			"radius":           500,
			"status_color":     beaconStatusColor(post.Confidence),
			"image_url":        post.ImageURL,
			"created_at":       post.CreatedAt,
			"vouch_count":      0,
			"report_count":     0,
		},
	})
}

// beaconStatusColor returns green/yellow/red based on confidence score.
func beaconStatusColor(confidence float64) string {
	if confidence > 0.7 {
		return "green"
	} else if confidence >= 0.3 {
		return "yellow"
	}
	return "red"
}

func (h *PostHandler) CreatePost(c *gin.Context) {
	userIDStr, _ := c.Get("user_id")
	userID, _ := uuid.Parse(userIDStr.(string))

	var req struct {
		CategoryID    *string  `json:"category_id"`
		Body          string   `json:"body" binding:"required,max=500"`
		ImageURL      *string  `json:"image_url"`
		VideoURL      *string  `json:"video_url"`
		Thumbnail     *string  `json:"thumbnail_url"`
		DurationMS    *int     `json:"duration_ms"`
		AllowChain    *bool    `json:"allow_chain"`
		ChainParentID *string  `json:"chain_parent_id"`
		IsBeacon      bool     `json:"is_beacon"`
		BeaconType    *string  `json:"beacon_type"`
		Severity      *string  `json:"severity"`
		BeaconLat     *float64 `json:"beacon_lat"`
		BeaconLong    *float64 `json:"beacon_long"`
		TTLHours      *int     `json:"ttl_hours"`
		IsNSFW        bool     `json:"is_nsfw"`
		NSFWReason    string   `json:"nsfw_reason"`
		Visibility    string   `json:"visibility"`
		OverlayJSON   *string  `json:"overlay_json"`
	}

	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Layer 0: Hard blocklist check — reject immediately, never save
	if h.contentFilter != nil {
		result := h.contentFilter.CheckContent(req.Body)
		if result.Blocked {
			strikeCount, consequence, _ := h.contentFilter.RecordStrikeWithIP(c.Request.Context(), userID, result.Category, req.Body, c.ClientIP())
			c.JSON(http.StatusUnprocessableEntity, gin.H{
				"error":       result.Message,
				"blocked":     true,
				"category":    result.Category,
				"strikes":     strikeCount,
				"consequence": consequence,
			})
			return
		}
	}

	// 1. Check rate limit (Simplification)
	trustState, err := h.userRepo.GetTrustState(c.Request.Context(), userID.String())
	if err == nil && trustState.PostsToday >= 50 { // Example hard limit
		c.JSON(http.StatusTooManyRequests, gin.H{"error": "Daily post limit reached"})
		return
	}

	// 2. Extract tags
	tags := utils.ExtractHashtags(req.Body)

	// 3. Mock Tone Check (In production, this would call a service or AI model)
	tone := "neutral"
	cis := 0.8

	// 4. Resolve TTL
	var expiresAt *time.Time
	if req.TTLHours != nil && *req.TTLHours > 0 {
		t := time.Now().Add(time.Duration(*req.TTLHours) * time.Hour)
		expiresAt = &t
	}

	duration := 0
	if req.DurationMS != nil {
		duration = *req.DurationMS
	}

	allowChain := !req.IsBeacon
	if req.AllowChain != nil {
		allowChain = *req.AllowChain
	}

	if req.ChainParentID != nil && *req.ChainParentID != "" {
		log.Info().
			Str("chain_parent_id", *req.ChainParentID).
			Bool("allow_chain", allowChain).
			Msg("CreatePost with chain parent")
	} else {
		log.Info().
			Bool("allow_chain", allowChain).
			Msg("CreatePost without chain parent")
	}

	severity := "medium"
	if req.Severity != nil && *req.Severity != "" {
		severity = *req.Severity
	}

	post := &models.Post{
		AuthorID:       userID,
		Body:           req.Body,
		Status:         "active",
		ToneLabel:      &tone,
		CISScore:       &cis,
		ImageURL:       req.ImageURL,
		VideoURL:       req.VideoURL,
		ThumbnailURL:   req.Thumbnail,
		DurationMS:     duration,
		BodyFormat:     "plain",
		Tags:           tags,
		IsBeacon:       req.IsBeacon,
		BeaconType:     req.BeaconType,
		Severity:       severity,
		IncidentStatus: "active",
		Radius:         500,
		Confidence:     0.5, // Initial confidence
		IsActiveBeacon: req.IsBeacon,
		AllowChain:     allowChain,
		Visibility: func() string {
			if req.Visibility == "neighborhood" {
				return "neighborhood"
			}
			return "public"
		}(),
		ExpiresAt:   expiresAt,
		IsNSFW:      req.IsNSFW,
		NSFWReason:  req.NSFWReason,
		Lat:         req.BeaconLat,
		Long:        req.BeaconLong,
		OverlayJSON: req.OverlayJSON,
	}

	if req.CategoryID != nil {
		catID, _ := uuid.Parse(*req.CategoryID)
		post.CategoryID = &catID
	}

	if req.ChainParentID != nil && *req.ChainParentID != "" {
		parentID, err := uuid.Parse(*req.ChainParentID)
		if err == nil {
			post.ChainParentID = &parentID
		}
	}

	// 5. AI Moderation — runs whichever engine the admin selected in ai_moderation_config
	userSelfLabeledNSFW := req.IsNSFW
	orDecision := ""
	var cachedScores *services.ThreePoisonsScore
	var cachedReason string
	ctx := c.Request.Context()

	{
		// Read admin-selected engine from ai_moderation_config
		selectedEngine := "" // empty = no AI moderation configured
		if h.openRouterService != nil {
			cfg, cfgErr := h.openRouterService.GetModerationConfig(ctx, "text")
			if cfgErr == nil && cfg != nil && cfg.Enabled && len(cfg.Engines) > 0 {
				selectedEngine = cfg.Engines[0]
			}
		}

		switch selectedEngine {
		case "local_ai":
			if h.localAIService != nil && req.Body != "" {
				localResult, localErr := h.localAIService.ModerateText(ctx, req.Body)
				if localErr != nil {
					log.Debug().Err(localErr).Msg("Local AI moderation unavailable")
				} else if localResult != nil && !localResult.Allowed {
					post.Status = "removed"
					log.Warn().Str("reason", localResult.Reason).Str("severity", localResult.Severity).Msg("Post flagged by local AI")
				}
			}

		case "openai":
			if h.moderationService != nil {
				mediaURLs := []string{}
				if req.ImageURL != nil && *req.ImageURL != "" {
					mediaURLs = append(mediaURLs, *req.ImageURL)
				}
				if req.VideoURL != nil && *req.VideoURL != "" {
					mediaURLs = append(mediaURLs, *req.VideoURL)
				}
				if req.Thumbnail != nil && *req.Thumbnail != "" {
					mediaURLs = append(mediaURLs, *req.Thumbnail)
				}
				scores, reason, err := h.moderationService.AnalyzeContent(ctx, req.Body, mediaURLs)
				if err == nil {
					cachedScores = scores
					cachedReason = reason
					cis = (scores.Hate + scores.Greed + scores.Delusion) / 3.0
					cis = 1.0 - cis
					post.CISScore = &cis
					post.ToneLabel = &reason
					if reason != "" {
						post.Status = "pending_moderation"
					}
				}
			}

		case "google":
			if h.moderationService != nil && h.moderationService.HasGoogleVision() {
				mediaURLs := []string{}
				if req.ImageURL != nil && *req.ImageURL != "" {
					mediaURLs = append(mediaURLs, *req.ImageURL)
				}
				if req.Thumbnail != nil && *req.Thumbnail != "" {
					mediaURLs = append(mediaURLs, *req.Thumbnail)
				}
				if len(mediaURLs) > 0 {
					scores, err := h.moderationService.AnalyzeMediaWithGoogleVision(ctx, mediaURLs)
					if err != nil {
						log.Warn().Err(err).Msg("Google Vision moderation failed")
					} else {
						cachedScores = scores
						cis = (scores.Hate + scores.Greed + scores.Delusion) / 3.0
						cis = 1.0 - cis
						post.CISScore = &cis
						if scores.Hate > 0.5 || scores.Delusion > 0.5 {
							cachedReason = "Google Vision flagged image content"
							post.Status = "removed"
							log.Warn().Float64("hate", scores.Hate).Float64("delusion", scores.Delusion).Msg("Post flagged by Google Vision")
						} else if scores.Hate > 0.25 || scores.Delusion > 0.25 {
							post.IsNSFW = true
							post.NSFWReason = "Google Vision detected potentially sensitive image content"
						}
					}
				}
			}

		case "openrouter":
			if h.openRouterService != nil {
				// Text moderation
				if req.Body != "" {
					textResult, textErr := h.openRouterService.ModerateText(ctx, req.Body)
					if textErr == nil && textResult != nil {
						log.Info().Str("action", textResult.Action).Msg("OpenRouter text moderation")
						if textResult.Action == "flag" {
							orDecision = "flag"
							post.Status = "removed"
						} else if textResult.Action == "nsfw" {
							orDecision = "nsfw"
							post.IsNSFW = true
							if textResult.NSFWReason != "" {
								post.NSFWReason = textResult.NSFWReason
							}
						}
						if textResult.Hate > 0 || textResult.Greed > 0 || textResult.Delusion > 0 {
							orCis := 1.0 - (textResult.Hate+textResult.Greed+textResult.Delusion)/3.0
							post.CISScore = &orCis
						}
					}
				}
				// Image moderation (only if text didn't already flag)
				if post.Status != "removed" && req.ImageURL != nil && *req.ImageURL != "" {
					imgResult, imgErr := h.openRouterService.ModerateImage(ctx, *req.ImageURL)
					if imgErr == nil && imgResult != nil {
						log.Info().Str("action", imgResult.Action).Msg("OpenRouter image moderation")
						if imgResult.Action == "flag" {
							orDecision = "flag"
							post.Status = "removed"
						} else if imgResult.Action == "nsfw" && orDecision != "flag" {
							orDecision = "nsfw"
							post.IsNSFW = true
							if imgResult.NSFWReason != "" {
								post.NSFWReason = imgResult.NSFWReason
							}
						}
					}
				}
				// Enhanced video moderation with frame extraction
				if post.Status != "removed" && req.VideoURL != nil && *req.VideoURL != "" {
					// First check thumbnail moderation
					if req.Thumbnail != nil && *req.Thumbnail != "" {
						vidResult, vidErr := h.openRouterService.ModerateImage(ctx, *req.Thumbnail)
						if vidErr == nil && vidResult != nil {
							log.Info().Str("action", vidResult.Action).Msg("OpenRouter video thumbnail moderation")
							if vidResult.Action == "flag" {
								orDecision = "flag"
								post.Status = "removed"
							} else if vidResult.Action == "nsfw" && orDecision != "flag" {
								orDecision = "nsfw"
								post.IsNSFW = true
								if vidResult.NSFWReason != "" {
									post.NSFWReason = vidResult.NSFWReason
								}
							}
						}
					}

					// Extract and analyze video frames for deeper moderation
					if post.Status != "removed" && h.videoProcessor != nil {
						frameURLs, err := h.videoProcessor.ExtractFrames(ctx, *req.VideoURL, 3)
						if err == nil && len(frameURLs) > 0 {
							// Analyze extracted frames with Azure OpenAI Vision
							if h.moderationService != nil {
								_, frameReason, frameErr := h.moderationService.AnalyzeContent(ctx, "Video frame analysis", frameURLs)
								if frameErr == nil && frameReason != "" {
									log.Info().Str("reason", frameReason).Msg("Video frame analysis completed")
									if strings.Contains(strings.ToLower(frameReason), "flag") || strings.Contains(strings.ToLower(frameReason), "remove") {
										orDecision = "flag"
										post.Status = "removed"
									} else if strings.Contains(strings.ToLower(frameReason), "nsfw") && orDecision != "flag" {
										orDecision = "nsfw"
										post.IsNSFW = true
										post.NSFWReason = "Video content flagged by frame analysis"
									}
								}
							}
						} else {
							log.Debug().Err(err).Msg("Failed to extract video frames for moderation")
						}
					}
				}
			}

		default:
			log.Debug().Msg("No moderation engine configured in admin settings")
		}
	}

	// Create post
	err = h.postRepo.CreatePost(c.Request.Context(), post)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post", "details": err.Error()})
		return
	}

	// Handle Flags — reuse cached scores from parallel moderation (no duplicate API call)
	if h.moderationService != nil && (post.Status == "pending_moderation" || post.Status == "removed") {
		if cachedScores != nil {
			_ = h.moderationService.FlagPost(c.Request.Context(), post.ID, cachedScores, cachedReason)
		}
	}

	// NSFW auto-reclassify: AI says NSFW but user didn't self-label → send warning
	if post.IsNSFW && !userSelfLabeledNSFW && h.notificationService != nil {
		go func() {
			ctx := context.Background()
			h.notificationService.NotifyNSFWWarning(ctx, userID.String(), post.ID.String())
			log.Info().Str("post_id", post.ID.String()).Str("author_id", userID.String()).Msg("NSFW warning sent — post auto-labeled")
		}()
	}

	// NOT ALLOWED: AI flagged → post removed, create violation, send appeal notification + email
	if post.Status == "removed" && orDecision == "flag" {
		go func() {
			ctx := context.Background()

			// Send in-app notification
			if h.notificationService != nil {
				h.notificationService.NotifyContentRemoved(ctx, userID.String(), post.ID.String())
			}

			// Create moderation violation record
			if h.moderationService != nil {
				h.moderationService.FlagPost(ctx, post.ID, &services.ThreePoisonsScore{Hate: 1.0}, "not_allowed")
			}

			// Send appeal email — get email from users table, display name from profiles
			var userEmail string
			h.postRepo.Pool().QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, userID).Scan(&userEmail)
			profile, _ := h.userRepo.GetProfileByID(ctx, userID.String())
			if userEmail != "" {
				displayName := "there"
				if profile != nil && profile.DisplayName != nil {
					displayName = *profile.DisplayName
				}
				snippet := req.Body
				if len(snippet) > 100 {
					snippet = snippet[:100] + "..."
				}
				appealBody := fmt.Sprintf(
					"Hi %s,\n\n"+
						"Your recent post on Sojorn was removed because it was found to violate our community guidelines.\n\n"+
						"Post content: \"%s\"\n\n"+
						"If you believe this was a mistake, you can appeal this decision in your Sojorn app:\n"+
						"Go to Profile → Settings → Appeals\n\n"+
						"Our moderation team will review your appeal within 48 hours.\n\n"+
						"— The Sojorn Team",
					displayName, snippet,
				)
				log.Info().Str("email", userEmail).Msg("Sending content removal appeal email")
				h.postRepo.Pool().Exec(ctx,
					`INSERT INTO email_queue (to_email, subject, body, created_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT DO NOTHING`,
					userEmail, "Your Sojorn post was removed", appealBody,
				)
			}

			log.Warn().Str("post_id", post.ID.String()).Str("author_id", userID.String()).Msg("Post removed by AI moderation — not allowed content")
		}()
	}

	// Log AI moderation decision to audit log — async
	if h.moderationService != nil {
		postID := post.ID
		postStatus := post.Status
		postIsNSFW := post.IsNSFW
		postCIS := post.CISScore
		postTone := post.ToneLabel
		reqBody := req.Body
		go func() {
			decision := "pass"
			flagReason := ""
			if postTone != nil && *postTone != "" {
				flagReason = *postTone
			}
			if postStatus == "removed" || postStatus == "pending_moderation" {
				decision = "flag"
			} else if postIsNSFW {
				decision = "nsfw"
			}
			var scores *services.ThreePoisonsScore
			if postCIS != nil {
				invCis := 1.0 - *postCIS
				scores = &services.ThreePoisonsScore{Hate: invCis, Greed: 0, Delusion: 0}
			} else {
				scores = &services.ThreePoisonsScore{}
			}
			h.moderationService.LogAIDecision(context.Background(), "post", postID, userID, reqBody, scores, nil, decision, flagReason, orDecision, nil)
		}()
	}

	// Auto-extract link preview — fully async, never blocks the response
	if h.linkPreviewService != nil {
		linkURL := services.ExtractFirstURL(req.Body)
		if linkURL != "" {
			postID := post.ID.String()
			go func() {
				bgCtx, bgCancel := context.WithTimeout(context.Background(), 10*time.Second)
				defer bgCancel()
				var isOfficial bool
				_ = h.postRepo.Pool().QueryRow(bgCtx, `SELECT COALESCE(is_official, false) FROM profiles WHERE id = $1`, userID).Scan(&isOfficial)
				lp, lpErr := h.linkPreviewService.FetchPreview(bgCtx, linkURL, isOfficial)
				if lpErr == nil && lp != nil {
					h.linkPreviewService.ProxyImageToR2(bgCtx, lp)
					_ = h.linkPreviewService.SaveLinkPreview(bgCtx, postID, lp)
				}
			}()
		}
	}

	// Check for @mentions and notify mentioned users
	go func() {
		if h.notificationService != nil && strings.Contains(req.Body, "@") {
			postIDStr := post.ID.String()
			h.notificationService.NotifyMention(c.Request.Context(), userIDStr.(string), postIDStr, req.Body)
		}
	}()

	c.JSON(http.StatusCreated, gin.H{
		"post": post,
		"tags": tags,
		"tone_analysis": gin.H{
			"tone": tone,
			"cis":  cis,
		},
	})
}

func (h *PostHandler) GetFeed(c *gin.Context) {
	userIDStr, _ := c.Get("user_id")

	limit := utils.GetQueryInt(c, "limit", 20)
	offset := utils.GetQueryInt(c, "offset", 0)
	category := c.Query("category")
	hasVideo := c.Query("has_video") == "true"

	// Check user's NSFW preference
	showNSFW := false
	if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), userIDStr.(string)); err == nil && settings.NSFWEnabled != nil {
		showNSFW = *settings.NSFWEnabled
	}

	posts, err := h.feedService.GetFeed(c.Request.Context(), userIDStr.(string), category, hasVideo, limit, offset, showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feed", "details": err.Error()})
		return
	}

	h.enrichLinkPreviews(c.Request.Context(), posts)
	c.JSON(http.StatusOK, gin.H{"posts": posts})
}

func (h *PostHandler) GetProfilePosts(c *gin.Context) {
	authorID := c.Param("id")
	if authorID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Author ID required"})
		return
	}

	limit := utils.GetQueryInt(c, "limit", 20)
	offset := utils.GetQueryInt(c, "offset", 0)
	onlyChains := c.Query("chained") == "true"

	viewerID := ""
	if val, exists := c.Get("user_id"); exists {
		viewerID = val.(string)
	}

	// Check viewer's NSFW preference
	showNSFW := false
	if viewerID != "" {
		if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID); err == nil && settings.NSFWEnabled != nil {
			showNSFW = *settings.NSFWEnabled
		}
	}

	posts, err := h.postRepo.GetPostsByAuthor(c.Request.Context(), authorID, viewerID, limit, offset, onlyChains, showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch profile posts", "details": err.Error()})
		return
	}

	h.enrichLinkPreviews(c.Request.Context(), posts)
	c.JSON(http.StatusOK, gin.H{"posts": posts})
}

func (h *PostHandler) GetPost(c *gin.Context) {
	log.Error().Msg("=== DEBUG: GetPost handler called ===")
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	// Check viewer's NSFW preference
	showNSFW := false
	if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), userIDStr.(string)); err == nil && settings.NSFWEnabled != nil {
		showNSFW = *settings.NSFWEnabled
	}

	post, err := h.postRepo.GetPostByID(c.Request.Context(), postID, userIDStr.(string), showNSFW)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}

	// Sign URL
	if post.ImageURL != nil {
		signed := h.assetService.SignImageURL(*post.ImageURL)
		post.ImageURL = &signed
	}
	if post.VideoURL != nil {
		signed := h.assetService.SignVideoURL(*post.VideoURL)
		post.VideoURL = &signed
	}
	if post.ThumbnailURL != nil {
		signed := h.assetService.SignImageURL(*post.ThumbnailURL)
		post.ThumbnailURL = &signed
	}

	h.enrichSinglePostLinkPreview(c.Request.Context(), post)
	c.JSON(http.StatusOK, gin.H{"post": post})
}

func (h *PostHandler) UpdatePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	var req struct {
		Body string `json:"body" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	err := h.postRepo.UpdatePost(c.Request.Context(), postID, userIDStr.(string), req.Body)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post updated"})
}

func (h *PostHandler) DeletePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.DeletePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post deleted"})
}

func (h *PostHandler) PinPost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	var req struct {
		Pinned bool `json:"pinned"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	err := h.postRepo.PinPost(c.Request.Context(), postID, userIDStr.(string), req.Pinned)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to pin/unpin post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post pin status updated"})
}

func (h *PostHandler) UpdateVisibility(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	var req struct {
		Visibility string `json:"visibility" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	err := h.postRepo.UpdateVisibility(c.Request.Context(), postID, userIDStr.(string), req.Visibility)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update visibility", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post visibility updated"})
}

func (h *PostHandler) LikePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.LikePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to like post", "details": err.Error()})
		return
	}

	// Send push notification to post author
	go func() {
		// Use Background context because the request context will be cancelled
		bgCtx := context.Background()
		post, err := h.postRepo.GetPostByID(bgCtx, postID, userIDStr.(string))
		if err != nil || post.AuthorID.String() == userIDStr.(string) {
			return // Don't notify self
		}

		if h.notificationService != nil {
			postType := "standard"
			if post.IsBeacon {
				postType = "beacon"
			} else if post.VideoURL != nil && *post.VideoURL != "" {
				postType = "quip"
			}

			h.notificationService.NotifyLike(
				bgCtx,
				post.AuthorID.String(),
				userIDStr.(string),
				postID,
				postType,
				"❤️",
			)
		}
	}()

	c.JSON(http.StatusOK, gin.H{"message": "Post liked"})
}

func (h *PostHandler) UnlikePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.UnlikePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unlike post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post unliked"})
}

// HidePost records a "Not Interested" signal for a post.
// The post will be excluded from all subsequent feed queries for this user,
// and repeated hides of the same author trigger algorithmic suppression.
func (h *PostHandler) HidePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.HidePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hide post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post hidden"})
}

func (h *PostHandler) SavePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.SavePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save post", "details": err.Error()})
		return
	}

	// Send push notification to post author
	go func() {
		bgCtx := context.Background()
		post, err := h.postRepo.GetPostByID(bgCtx, postID, userIDStr.(string))
		if err != nil || post.AuthorID.String() == userIDStr.(string) {
			return // Don't notify self
		}

		actor, err := h.userRepo.GetProfileByID(bgCtx, userIDStr.(string))
		if err != nil || h.notificationService == nil {
			return
		}

		// Determine post type for proper deep linking
		postType := "standard"
		if post.IsBeacon {
			postType = "beacon"
		} else if post.VideoURL != nil && *post.VideoURL != "" {
			postType = "quip"
		}

		metadata := map[string]interface{}{
			"actor_name": actor.DisplayName,
			"post_id":    postID,
			"post_type":  postType,
		}
		h.notificationService.CreateNotification(
			context.Background(),
			post.AuthorID.String(),
			userIDStr.(string),
			"save",
			&postID,
			nil,
			metadata,
		)
	}()

	c.JSON(http.StatusOK, gin.H{"message": "Post saved"})
}

func (h *PostHandler) UnsavePost(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.UnsavePost(c.Request.Context(), postID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unsave post", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Post unsaved"})
}

func (h *PostHandler) GetSavedPosts(c *gin.Context) {
	userID := c.Param("id")
	if userID == "" || userID == "me" {
		userIDStr, exists := c.Get("user_id")
		if exists {
			userID = userIDStr.(string)
		}
	}

	if userID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
		return
	}

	limit := utils.GetQueryInt(c, "limit", 20)
	offset := utils.GetQueryInt(c, "offset", 0)

	// Check viewer's NSFW preference
	showNSFW := false
	if viewerID, exists := c.Get("user_id"); exists {
		if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID.(string)); err == nil && settings.NSFWEnabled != nil {
			showNSFW = *settings.NSFWEnabled
		}
	}

	posts, err := h.postRepo.GetSavedPosts(c.Request.Context(), userID, limit, offset, showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch saved posts", "details": err.Error()})
		return
	}

	h.enrichLinkPreviews(c.Request.Context(), posts)
	c.JSON(http.StatusOK, gin.H{"posts": posts})
}

func (h *PostHandler) GetLikedPosts(c *gin.Context) {
	userID := c.Param("id")
	if userID == "" || userID == "me" {
		userIDStr, exists := c.Get("user_id")
		if exists {
			userID = userIDStr.(string)
		}
	}

	if userID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "User ID required"})
		return
	}

	limit := utils.GetQueryInt(c, "limit", 20)
	offset := utils.GetQueryInt(c, "offset", 0)

	// Check viewer's NSFW preference
	showNSFW := false
	if viewerID, exists := c.Get("user_id"); exists {
		if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID.(string)); err == nil && settings.NSFWEnabled != nil {
			showNSFW = *settings.NSFWEnabled
		}
	}

	posts, err := h.postRepo.GetLikedPosts(c.Request.Context(), userID, limit, offset, showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch liked posts", "details": err.Error()})
		return
	}

	h.enrichLinkPreviews(c.Request.Context(), posts)
	c.JSON(http.StatusOK, gin.H{"posts": posts})
}

func (h *PostHandler) GetPostChain(c *gin.Context) {
	postID := c.Param("id")

	// Check viewer's NSFW preference
	showNSFW := false
	if viewerID, exists := c.Get("user_id"); exists {
		if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), viewerID.(string)); err == nil && settings.NSFWEnabled != nil {
			showNSFW = *settings.NSFWEnabled
		}
	}

	posts, err := h.postRepo.GetPostChain(c.Request.Context(), postID, showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch post chain", "details": err.Error()})
		return
	}

	// Sign URLs for all posts in the chain
	for i := range posts {
		if posts[i].ImageURL != nil {
			signed := h.assetService.SignImageURL(*posts[i].ImageURL)
			posts[i].ImageURL = &signed
		}
		if posts[i].VideoURL != nil {
			signed := h.assetService.SignVideoURL(*posts[i].VideoURL)
			posts[i].VideoURL = &signed
		}
		if posts[i].ThumbnailURL != nil {
			signed := h.assetService.SignImageURL(*posts[i].ThumbnailURL)
			posts[i].ThumbnailURL = &signed
		}
	}

	h.enrichLinkPreviews(c.Request.Context(), posts)
	c.JSON(http.StatusOK, gin.H{"posts": posts})
}

func (h *PostHandler) GetPostFocusContext(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	// Check viewer's NSFW preference
	showNSFW := false
	if settings, err := h.userRepo.GetUserSettings(c.Request.Context(), userIDStr.(string)); err == nil && settings.NSFWEnabled != nil {
		showNSFW = *settings.NSFWEnabled
	}

	focusContext, err := h.postRepo.GetPostFocusContext(c.Request.Context(), postID, userIDStr.(string), showNSFW)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch focus context", "details": err.Error()})
		return
	}

	h.signPostMedia(focusContext.TargetPost)
	h.signPostMedia(focusContext.ParentPost)
	for i := range focusContext.Children {
		h.signPostMedia(&focusContext.Children[i])
	}

	// Enrich link previews for all posts in focus context
	h.enrichSinglePostLinkPreview(c.Request.Context(), focusContext.TargetPost)
	h.enrichSinglePostLinkPreview(c.Request.Context(), focusContext.ParentPost)
	for i := range focusContext.Children {
		h.enrichSinglePostLinkPreview(c.Request.Context(), &focusContext.Children[i])
	}

	c.JSON(http.StatusOK, focusContext)
}

func (h *PostHandler) signPostMedia(post *models.Post) {
	if post == nil {
		return
	}
	if post.ImageURL != nil {
		signed := h.assetService.SignImageURL(*post.ImageURL)
		post.ImageURL = &signed
	}
	if post.VideoURL != nil {
		signed := h.assetService.SignVideoURL(*post.VideoURL)
		post.VideoURL = &signed
	}
	if post.ThumbnailURL != nil {
		signed := h.assetService.SignImageURL(*post.ThumbnailURL)
		post.ThumbnailURL = &signed
	}
}

func (h *PostHandler) VouchBeacon(c *gin.Context) {
	beaconID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.VouchBeacon(c.Request.Context(), beaconID, userIDStr.(string))
	if err != nil {
		if err.Error() == "post is not a beacon" {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to vouch for beacon", "details": err.Error()})
		return
	}

	// Beacons are anonymous — no notifications sent to preserve privacy

	c.JSON(http.StatusOK, gin.H{"message": "Beacon vouched successfully"})
}

func (h *PostHandler) ReportBeacon(c *gin.Context) {
	beaconID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.ReportBeacon(c.Request.Context(), beaconID, userIDStr.(string))
	if err != nil {
		if err.Error() == "post is not a beacon" {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to report beacon", "details": err.Error()})
		return
	}

	// Beacons are anonymous — no notifications sent to preserve privacy

	c.JSON(http.StatusOK, gin.H{"message": "Beacon reported successfully"})
}

func (h *PostHandler) RemoveBeaconVote(c *gin.Context) {
	beaconID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	err := h.postRepo.RemoveBeaconVote(c.Request.Context(), beaconID, userIDStr.(string))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove beacon vote", "details": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Beacon vote removed successfully"})
}

func (h *PostHandler) ToggleReaction(c *gin.Context) {
	postID := c.Param("id")
	userIDStr, _ := c.Get("user_id")

	var req struct {
		Emoji string `json:"emoji" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	emoji := strings.TrimSpace(req.Emoji)
	if emoji == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Emoji is required"})
		return
	}

	counts, myReactions, err := h.postRepo.ToggleReaction(c.Request.Context(), postID, userIDStr.(string), emoji)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle reaction", "details": err.Error()})
		return
	}

	// Check if reaction was added (exists in myReactions)
	reactionAdded := false
	for _, r := range myReactions {
		if r == emoji {
			reactionAdded = true
			break
		}
	}

	if reactionAdded {
		go func() {
			bgCtx := context.Background()
			post, err := h.postRepo.GetPostByID(bgCtx, postID, userIDStr.(string))
			if err != nil || post.AuthorID.String() == userIDStr.(string) {
				return // Don't notify self
			}

			if h.notificationService != nil {
				// Get actor details
				actor, err := h.userRepo.GetProfileByID(bgCtx, userIDStr.(string))
				if err != nil {
					return
				}

				metadata := map[string]interface{}{
					"actor_name": actor.DisplayName,
					"post_id":    postID,
					"emoji":      emoji,
				}

				// Using "like" type for now, or "quip_reaction" if quip
				notifType := "like"
				if post.VideoURL != nil && *post.VideoURL != "" {
					notifType = "quip_reaction"
					metadata["post_type"] = "quip"
				} else {
					metadata["post_type"] = "post"
				}

				h.notificationService.CreateNotification(
					bgCtx,
					post.AuthorID.String(),
					userIDStr.(string),
					notifType,
					&postID,
					nil,
					metadata,
				)
			}
		}()
	}

	c.JSON(http.StatusOK, gin.H{
		"reactions":    counts,
		"my_reactions": myReactions,
	})
}

// GetSafeDomains returns the list of approved safe domains for the Flutter app.
func (h *PostHandler) GetSafeDomains(c *gin.Context) {
	if h.linkPreviewService == nil {
		c.JSON(http.StatusOK, gin.H{"domains": []string{}})
		return
	}
	domains, err := h.linkPreviewService.ListSafeDomains(c.Request.Context(), "", true)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"domains": domains})
}

// CheckURLSafety checks if a URL is from a safe domain.
func (h *PostHandler) CheckURLSafety(c *gin.Context) {
	urlStr := c.Query("url")
	if urlStr == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "url parameter required"})
		return
	}
	if h.linkPreviewService == nil {
		c.JSON(http.StatusOK, gin.H{"safe": false, "status": "unknown"})
		return
	}
	result := h.linkPreviewService.CheckURLSafety(c.Request.Context(), urlStr)
	c.JSON(http.StatusOK, result)
}
