package usecase

import (
	"context"
	"fmt"

	"apps/backend/internal/module/scout/domain"
)

// StageRunPosts writes a run's candidate set behind the repository visibility
// barrier. Repositories expose these posts only after the run is succeeded.
func (s *Service) StageRunPosts(ctx context.Context, ownerUID int64, runID string, posts []*domain.Post) error {
	if s == nil || s.Repo == nil || runID == "" {
		return domain.ErrValidation
	}
	r, err := s.Repo.GetRun(ctx, ownerUID, runID)
	if err != nil {
		return err
	}
	if r.Status != domain.RunRunning {
		return domain.ErrIllegalRunStatus
	}
	for _, post := range posts {
		if post == nil || post.ID == "" || post.OwnerUID != ownerUID || post.RunID != runID {
			return fmt.Errorf("%w: staged post must belong to run", domain.ErrValidation)
		}
	}
	return s.Repo.PublishRunPosts(ctx, ownerUID, runID, posts)
}

// PublishRun atomically changes visibility by transitioning running →
// succeeded only after all staged posts have been written. If writing fails,
// the run remains non-visible and the caller can fail it safely.
func (s *Service) PublishRun(ctx context.Context, ownerUID int64, runID string, posts []*domain.Post) error {
	if err := s.StageRunPosts(ctx, ownerUID, runID, posts); err != nil {
		return err
	}
	r, err := s.Repo.GetRun(ctx, ownerUID, runID)
	if err != nil {
		return err
	}
	r.EligibleCount = len(posts)
	r.PendingCount = countPendingRunPosts(posts)
	if r.TargetCount > 0 && len(posts) < r.TargetCount {
		r.ShortfallCount = r.TargetCount - len(posts)
		if len(r.ShortfallReasons) == 0 {
			r.ShortfallReasons = []string{domain.ShortfallSourceExhausted}
		}
	} else {
		r.ShortfallCount = 0
		r.ShortfallReasons = []string{}
	}
	if err := r.Transition(domain.RunSucceeded, domain.NowNano()); err != nil {
		return err
	}
	if err := s.Repo.ReplaceRunGuarded(ctx, ownerUID, runID, []string{domain.RunRunning}, r); err != nil {
		return err
	}
	// Seen identities are recorded only after the visibility barrier opens.
	// Marking is idempotent; a failure here must not turn a visible successful
	// run into a misleading failed response.
	for _, post := range posts {
		identity := canonicalPostIdentity(post.Permalink)
		if identity == "" {
			identity = canonicalPostIdentity(post.ExternalID)
		}
		if identity == "" && post.ID != "" {
			identity = "id:" + post.ID
		}
		if identity != "" {
			_ = s.Repo.MarkSeenIdentity(ctx, ownerUID, identity, post.ID, domain.NowNano())
		}
	}
	return nil
}

// FailRun is idempotent for terminal runs and guarded for queued/running
// states, so a retry cannot move a completed run backwards.
func (s *Service) FailRun(ctx context.Context, ownerUID int64, runID, reason string) error {
	if s == nil || s.Repo == nil || runID == "" {
		return domain.ErrValidation
	}
	r, err := s.Repo.GetRun(ctx, ownerUID, runID)
	if err != nil {
		return err
	}
	if domain.IsRunTerminal(r.Status) {
		return nil
	}
	if err := r.Transition(domain.RunFailed, domain.NowNano()); err != nil {
		return err
	}
	r.Error = safeRunError(reason)
	return s.Repo.ReplaceRunGuarded(ctx, ownerUID, runID, []string{domain.RunQueued, domain.RunRunning}, r)
}

func countPendingRunPosts(posts []*domain.Post) int {
	n := 0
	for _, post := range posts {
		if post != nil && (post.OutreachStatus == domain.OutreachNew || post.OutreachStatus == domain.OutreachDrafted) {
			n++
		}
	}
	return n
}

func safeRunError(reason string) string {
	if len(reason) > 500 {
		return reason[:500]
	}
	return reason
}
