package usecase

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"
	"time"

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

// PlanQuota returns monthly platform credits by plan id (for invite cap).
type PlanQuota func(planID string) int

// BonusCrediter adds invite bonus points to inviter platform pool.
type BonusCrediter interface {
	CreditInviteBonus(ctx context.Context, uid int64, points int, ref string) error
}

// Notifier optional station notification.
type Notifier interface {
	Notify(ctx context.Context, ownerUID int64, title, body, kind, refType, refID string) error
}

// HealthSignals optional inputs for recompute.
type HealthSignals struct {
	Sends24h           int
	IntervalViolations int
	FailRate7d         float64 // 0..1
	TokenBad           bool
}

type Service struct {
	Repo        domain.Repository
	PlanQuota   PlanQuota
	Bonus       BonusCrediter
	Notifier    Notifier
	SignalProbe string // documentation: "degraded" | "partial" | "full"
}

func New(repo domain.Repository) *Service {
	return &Service{
		Repo:        repo,
		PlanQuota:   defaultPlanQuota,
		SignalProbe: "degraded", // Threads scope not fully available; OC-08 path active
	}
}

func defaultPlanQuota(planID string) int {
	switch strings.ToLower(planID) {
	case "starter":
		return 600
	case "pro":
		return 2000
	default:
		return 120
	}
}

// --- Outcomes ---

func (s *Service) RecordPublished(ctx context.Context, ownerUID int64, sourceType, sourceID, accountID string, sentAt int64) (*domain.OutcomeEvent, error) {
	if ownerUID <= 0 || sourceType == "" || sourceID == "" {
		return nil, fmt.Errorf("%w: missing source", domain.ErrValidation)
	}
	if sentAt <= 0 {
		sentAt = domain.NowNano()
	}
	now := domain.NowNano()
	e := &domain.OutcomeEvent{
		OwnerUID:         ownerUID,
		SourceType:       sourceType,
		SourceID:         sourceID,
		ThreadsAccountID: accountID,
		Kind:             domain.KindReach,
		Confidence:       domain.ConfidenceConfirmed,
		Status:           domain.StatusObserving,
		WindowEndsAt:     sentAt + int64(domain.ObserveWindow),
		SentAt:           sentAt,
		CreatedAt:        now,
		UpdatedAt:        now,
	}
	if err := s.Repo.UpsertOutcomeBySource(ctx, e); err != nil {
		return nil, err
	}
	return s.Repo.GetOutcome(ctx, e.ID)
}

func (s *Service) GetOutcome(ctx context.Context, ownerUID int64, id string) (*domain.OutcomeEvent, error) {
	e, err := s.Repo.GetOutcome(ctx, id)
	if err != nil {
		return nil, err
	}
	if e.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	return e, nil
}

func (s *Service) ListOutcomes(ctx context.Context, ownerUID int64, f domain.OutcomeListFilter) ([]*domain.OutcomeEvent, int64, error) {
	if f.Page < 1 {
		f.Page = 1
	}
	if f.PageSize < 1 || f.PageSize > 50 {
		f.PageSize = 20
	}
	return s.Repo.ListOutcomes(ctx, ownerUID, f)
}

func (s *Service) Summary(ctx context.Context, ownerUID int64, from, to int64) (*domain.OutcomeSummary, error) {
	if to <= 0 {
		to = domain.NowNano()
	}
	if from <= 0 {
		from = to - int64(7*24*time.Hour)
	}
	list, _, err := s.Repo.ListOutcomes(ctx, ownerUID, domain.OutcomeListFilter{From: from, To: to, Page: 1, PageSize: 1000})
	if err != nil {
		return nil, err
	}
	sum := &domain.OutcomeSummary{From: from, To: to}
	for _, e := range list {
		sum.Reach++
		if e.Kind == domain.KindReply || e.ReplyCountDelta > 0 {
			sum.Conversations++
		}
		if e.FollowersDelta > 0 || e.Kind == domain.KindFollow {
			follows := e.FollowersDelta
			if follows < 1 {
				follows = 1
			}
			if e.Confidence == domain.ConfidenceConfirmed {
				sum.FollowsConfirmed += follows
			} else {
				sum.FollowsPossible += follows
			}
		}
		if e.HasConversion || e.Kind == domain.KindConversion {
			sum.Conversions++
			sum.ConversionAmount += e.ConversionAmount
		}
	}
	return sum, nil
}

func (s *Service) ReportConversion(ctx context.Context, ownerUID int64, id string, amount float64, note, currency string) (*domain.OutcomeEvent, error) {
	e, err := s.GetOutcome(ctx, ownerUID, id)
	if err != nil {
		return nil, err
	}
	if currency == "" {
		currency = "TWD"
	}
	now := domain.NowNano()
	e.HasConversion = true
	e.ConversionAmount = amount
	e.ConversionNote = note
	e.ConversionCurrency = currency
	e.Kind = domain.KindConversion
	e.Confidence = domain.ConfidenceConfirmed
	e.Status = domain.StatusConfirmed
	e.UpdatedAt = now
	if err := s.Repo.SaveOutcome(ctx, e); err != nil {
		return nil, err
	}
	return e, nil
}

func (s *Service) UpdateConversion(ctx context.Context, ownerUID int64, id string, amount float64, note, currency string) (*domain.OutcomeEvent, error) {
	return s.ReportConversion(ctx, ownerUID, id, amount, note, currency)
}

func (s *Service) DeleteConversion(ctx context.Context, ownerUID int64, id string) error {
	e, err := s.GetOutcome(ctx, ownerUID, id)
	if err != nil {
		return err
	}
	e.HasConversion = false
	e.ConversionAmount = 0
	e.ConversionNote = ""
	e.ConversionCurrency = ""
	if e.ReplyCountDelta > 0 {
		e.Kind = domain.KindReply
		e.Status = domain.StatusConfirmed
	} else if e.FollowersDelta > 0 {
		e.Kind = domain.KindFollow
		e.Status = domain.StatusPossible
		e.Confidence = domain.ConfidencePossible
	} else {
		e.Kind = domain.KindReach
		e.Status = domain.StatusObserving
		e.Confidence = domain.ConfidenceConfirmed
	}
	e.UpdatedAt = domain.NowNano()
	return s.Repo.SaveOutcome(ctx, e)
}

// ApplyObserveSignals updates observing events. Missing signals are skipped (OC-08).
func (s *Service) ApplyObserveSignals(ctx context.Context, id string, replyDelta, likeDelta, followersDelta int) (*domain.OutcomeEvent, error) {
	e, err := s.Repo.GetOutcome(ctx, id)
	if err != nil {
		return nil, err
	}
	now := domain.NowNano()
	if e.Status != domain.StatusObserving && e.Status != domain.StatusPossible {
		if now > e.WindowEndsAt && e.Status == domain.StatusObserving {
			e.Status = domain.StatusExpired
			e.UpdatedAt = now
			_ = s.Repo.SaveOutcome(ctx, e)
		}
		return e, nil
	}
	if replyDelta > 0 {
		e.ReplyCountDelta += replyDelta
		e.Kind = domain.KindReply
		e.Confidence = domain.ConfidenceConfirmed
		e.Status = domain.StatusConfirmed
	}
	if likeDelta > 0 {
		e.LikeCountDelta += likeDelta
		if e.Kind == domain.KindReach {
			e.Kind = domain.KindInteraction
			e.Confidence = domain.ConfidencePossible
			e.Status = domain.StatusPossible
		}
	}
	if followersDelta > 0 {
		e.FollowersDelta += followersDelta
		if e.Kind != domain.KindReply && e.Kind != domain.KindConversion {
			e.Kind = domain.KindFollow
			e.Confidence = domain.ConfidencePossible
			e.Status = domain.StatusPossible
		}
	}
	if now > e.WindowEndsAt && e.Status == domain.StatusObserving {
		e.Status = domain.StatusExpired
	}
	e.UpdatedAt = now
	if err := s.Repo.SaveOutcome(ctx, e); err != nil {
		return nil, err
	}
	return e, nil
}

func (s *Service) ObserveTick(ctx context.Context, now int64) (int, error) {
	if now <= 0 {
		now = domain.NowNano()
	}
	list, err := s.Repo.ListObservingDue(ctx, now, 100)
	if err != nil {
		return 0, err
	}
	n := 0
	for _, e := range list {
		if now > e.WindowEndsAt {
			e.Status = domain.StatusExpired
			e.UpdatedAt = now
			_ = s.Repo.SaveOutcome(ctx, e)
			n++
		}
		// Signal fetch left degraded: no external Threads probe in default path.
	}
	return n, nil
}

// --- Checkups (no usage debit — caller must not meter) ---

func (s *Service) GenerateCheckup(ctx context.Context, ownerUID int64, timezone string, force bool) (*domain.WeeklyCheckup, error) {
	now := domain.NowNano()
	if force {
		since := now - int64(7*24*time.Hour)
		cnt, err := s.Repo.CountCheckupGenerations(ctx, ownerUID, since)
		if err != nil {
			return nil, err
		}
		if cnt >= 2 {
			return nil, domain.ErrRateLimited
		}
	}
	weekKey := weekKeyFor(timezone, time.Now().UTC())
	// Build report from outcome summary (no external AI required for MVP reliability).
	sum, err := s.Summary(ctx, ownerUID, 0, 0)
	if err != nil {
		return nil, err
	}
	findings := []domain.CheckupFinding{}
	if sum.Reach > 0 {
		findings = append(findings, domain.CheckupFinding{
			Claim:    fmt.Sprintf("近 7 日送出／標記完成 %d 則，可作為本週觸達基線", sum.Reach),
			Evidence: []string{fmt.Sprintf("outcome_reach=%d", sum.Reach)},
		})
	}
	if sum.Conversations > 0 {
		findings = append(findings, domain.CheckupFinding{
			Claim:    fmt.Sprintf("有 %d 則進入對話訊號，優先回覆這些串", sum.Conversations),
			Evidence: []string{fmt.Sprintf("outcome_conversations=%d", sum.Conversations)},
		})
	}
	if sum.Conversions > 0 {
		findings = append(findings, domain.CheckupFinding{
			Claim:    fmt.Sprintf("已回報 %d 筆成交，持續追蹤同類痛點", sum.Conversions),
			Evidence: []string{fmt.Sprintf("outcome_conversions=%d amount=%.0f", sum.Conversions, sum.ConversionAmount)},
		})
	}
	if len(findings) == 0 {
		findings = append(findings, domain.CheckupFinding{
			Claim:    "本週尚無足夠送出紀錄；建議先完成海巡外展或標記完成以累積訊號",
			Evidence: []string{"outcome_reach=0"},
		})
	}
	// keep only findings with evidence
	var okFindings []domain.CheckupFinding
	for _, f := range findings {
		if len(f.Evidence) > 0 && strings.TrimSpace(f.Claim) != "" {
			okFindings = append(okFindings, f)
		}
	}
	actions := []domain.CheckupAction{
		{Title: "處理海巡待回", Reason: "把待處理命中變成可歸因送出", Deeplink: "scout"},
		{Title: "寫一則主貼", Reason: "維持帳號節奏，累積可分析貼文", Deeplink: "studio_compose"},
		{Title: "回顧今日成果", Reason: "用成本＋成果判斷本週重點", Deeplink: "today"},
	}
	c := &domain.WeeklyCheckup{
		ID:        domain.NewID(),
		OwnerUID:  ownerUID,
		WeekKey:   weekKey,
		Status:    domain.CheckupReady,
		Summary:   fmt.Sprintf("本週觸達 %d、對話 %d、成交 %d。", sum.Reach, sum.Conversations, sum.Conversions),
		Findings:  okFindings,
		Actions:   actions,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := s.Repo.SaveCheckup(ctx, c); err != nil {
		return nil, err
	}
	if s.Notifier != nil {
		_ = s.Notifier.Notify(ctx, ownerUID, "每週帳號健檢已完成", c.Summary, "system", "checkup", c.ID)
	}
	return c, nil
}

func weekKeyFor(tz string, now time.Time) string {
	loc := time.UTC
	if tz != "" {
		if l, err := time.LoadLocation(tz); err == nil {
			loc = l
		}
	}
	local := now.In(loc)
	// ISO week
	y, w := local.ISOWeek()
	return fmt.Sprintf("%d-W%02d", y, w)
}

func (s *Service) LatestCheckup(ctx context.Context, ownerUID int64) (*domain.WeeklyCheckup, error) {
	return s.Repo.LatestCheckup(ctx, ownerUID)
}

func (s *Service) GetCheckup(ctx context.Context, ownerUID int64, id string) (*domain.WeeklyCheckup, error) {
	c, err := s.Repo.GetCheckup(ctx, id)
	if err != nil {
		return nil, err
	}
	if c.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	return c, nil
}

func (s *Service) ListCheckups(ctx context.Context, ownerUID int64, page, pageSize int) ([]*domain.WeeklyCheckup, int64, error) {
	return s.Repo.ListCheckups(ctx, ownerUID, page, pageSize)
}

// --- Account health ---

func (s *Service) RecomputeHealth(ctx context.Context, ownerUID int64, accountID string, sig HealthSignals) (*domain.AccountHealth, error) {
	score := 100
	var factors []string
	if sig.Sends24h > 20 {
		score -= 25
		factors = append(factors, fmt.Sprintf("24h 發送 %d 次偏高", sig.Sends24h))
	} else if sig.Sends24h > 10 {
		score -= 10
		factors = append(factors, fmt.Sprintf("24h 發送 %d 次", sig.Sends24h))
	}
	if sig.IntervalViolations > 0 {
		d := sig.IntervalViolations * 8
		if d > 30 {
			d = 30
		}
		score -= d
		factors = append(factors, fmt.Sprintf("間隔違規 %d 次", sig.IntervalViolations))
	}
	if sig.FailRate7d >= 0.5 {
		score -= 25
		factors = append(factors, fmt.Sprintf("7 日失敗率 %.0f%%", sig.FailRate7d*100))
	} else if sig.FailRate7d >= 0.25 {
		score -= 12
		factors = append(factors, fmt.Sprintf("7 日失敗率 %.0f%%", sig.FailRate7d*100))
	}
	if sig.TokenBad {
		score -= 40
		factors = append(factors, "token 失效或錯誤")
	}
	if score < 0 {
		score = 0
	}
	level := domain.HealthNormal
	advice := "操作節奏正常，可持續。"
	if score < 40 {
		level = domain.HealthThrottle
		advice = "帳號風險偏高：已阻擋自動送出，請改用複製→標記完成，並降低密度。"
	} else if score < 70 {
		level = domain.HealthWarn
		advice = "建議加倍發送間隔，並優先處理失敗步驟。"
	}
	if len(factors) == 0 {
		factors = []string{"無異常訊號"}
	}
	h := &domain.AccountHealth{
		ThreadsAccountID: accountID,
		OwnerUID:         ownerUID,
		Score:            score,
		Level:            level,
		Factors:          factors,
		Advice:           advice,
		ComputedAt:       domain.NowNano(),
	}
	if err := s.Repo.SaveHealth(ctx, h); err != nil {
		return nil, err
	}
	return h, nil
}

func (s *Service) GetHealth(ctx context.Context, ownerUID int64, accountID string) (*domain.AccountHealth, error) {
	h, err := s.Repo.GetHealth(ctx, accountID)
	if err != nil {
		if err == domain.ErrNotFound {
			// default healthy until computed
			return &domain.AccountHealth{
				ThreadsAccountID: accountID,
				OwnerUID:         ownerUID,
				Score:            85,
				Level:            domain.HealthNormal,
				Factors:          []string{"尚未重算，預設正常"},
				Advice:           "尚無足夠操作數據。",
				ComputedAt:       domain.NowNano(),
			}, nil
		}
		return nil, err
	}
	if h.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	return h, nil
}

func (s *Service) ListHealth(ctx context.Context, ownerUID int64) ([]*domain.AccountHealth, error) {
	return s.Repo.ListHealth(ctx, ownerUID)
}

// EnsureAutoSendAllowed returns health_warning message or ErrThrottle.
func (s *Service) EnsureAutoSendAllowed(ctx context.Context, ownerUID int64, accountID string) (warning string, err error) {
	if accountID == "" {
		return "", nil
	}
	h, err := s.GetHealth(ctx, ownerUID, accountID)
	if err != nil {
		return "", err
	}
	switch h.Level {
	case domain.HealthThrottle:
		return "", fmt.Errorf("%w: score=%d %s", domain.ErrThrottle, h.Score, h.Advice)
	case domain.HealthWarn:
		return fmt.Sprintf("健康分 %d（警告）：%s", h.Score, h.Advice), nil
	default:
		return "", nil
	}
}

// --- Invite rewards ---

func (s *Service) OnFirstPaidPlan(ctx context.Context, inviteeUID, parentUID int64, planID string) (*domain.InviteReward, error) {
	planID = strings.ToLower(strings.TrimSpace(planID))
	if parentUID <= 0 || inviteeUID <= 0 {
		return &domain.InviteReward{Status: domain.RewardSkipped}, nil
	}
	if planID != "starter" && planID != "pro" {
		return &domain.InviteReward{Status: domain.RewardSkipped}, nil
	}
	has, err := s.Repo.HasInviteRewardForInvitee(ctx, inviteeUID)
	if err != nil {
		return nil, err
	}
	if has {
		return &domain.InviteReward{Status: domain.RewardSkipped, InviteeUID: inviteeUID}, nil
	}
	points := 100
	if planID == "pro" {
		points = 300
	}
	quota := 120
	if s.PlanQuota != nil {
		// cap based on inviter plan — unknown → free quota
		quota = s.PlanQuota("free")
	}
	// Prefer looking up inviter plan via PlanQuota with "inviter" if set externally;
	// default month cap = free 50% unless Bonus path sets better.
	monthCap := quota / 2
	if monthCap < 0 {
		monthCap = 0
	}
	// Try inviter starter/pro quotas: use max of free half as floor; service callers
	// should set PlanQuota to resolve inviter plan.
	if s.PlanQuota != nil {
		// convention: PlanQuota("__inviter__") not used; pass inviter plan via
		// PlanQuota already closed over. Month cap = PlanQuota("current")/2.
		// Here PlanQuota receives plan id; for month cap we call with "starter" max?
		// Spec: 邀請人當月方案額度 50%. Caller should set:
		//   s.PlanQuota = func(id string) int { if id=="@inviter") return inviterQuota }
		if q := s.PlanQuota("@inviter"); q > 0 {
			monthCap = q / 2
		}
	}
	monthStart := time.Date(time.Now().UTC().Year(), time.Now().UTC().Month(), 1, 0, 0, 0, 0, time.UTC).UnixNano()
	used, err := s.Repo.SumInviteRewardPointsMonth(ctx, parentUID, monthStart)
	if err != nil {
		return nil, err
	}
	r := &domain.InviteReward{
		ID:         domain.NewID(),
		InviterUID: parentUID,
		InviteeUID: inviteeUID,
		PlanID:     planID,
		Points:     points,
		CreatedAt:  domain.NowNano(),
	}
	if used+points > monthCap && monthCap >= 0 {
		r.Status = domain.RewardCapped
		r.Points = 0
		_ = s.Repo.SaveInviteReward(ctx, r)
		return r, nil
	}
	r.Status = domain.RewardCredited
	if err := s.Repo.SaveInviteReward(ctx, r); err != nil {
		return nil, err
	}
	if s.Bonus != nil && points > 0 {
		_ = s.Bonus.CreditInviteBonus(ctx, parentUID, points, r.ID)
	}
	return r, nil
}

func (s *Service) InviteRewardsSummary(ctx context.Context, inviterUID int64, inviterPlanID string) (total, month, cap int, recent []*domain.InviteReward, err error) {
	quota := defaultPlanQuota(inviterPlanID)
	if s.PlanQuota != nil {
		if q := s.PlanQuota(inviterPlanID); q > 0 {
			quota = q
		}
	}
	cap = quota / 2
	monthStart := time.Date(time.Now().UTC().Year(), time.Now().UTC().Month(), 1, 0, 0, 0, 0, time.UTC).UnixNano()
	total, err = s.Repo.SumInviteRewardPointsTotal(ctx, inviterUID)
	if err != nil {
		return
	}
	month, err = s.Repo.SumInviteRewardPointsMonth(ctx, inviterUID, monthStart)
	if err != nil {
		return
	}
	recent, err = s.Repo.ListInviteRewards(ctx, inviterUID, 20)
	return
}

// --- Workspaces ---

func (s *Service) EnsureDefaultWorkspace(ctx context.Context, ownerUID int64) (*domain.Workspace, error) {
	list, err := s.Repo.ListWorkspaces(ctx, ownerUID, true)
	if err != nil {
		return nil, err
	}
	for _, w := range list {
		if w.IsDefault && !w.Archived {
			return w, nil
		}
	}
	for _, w := range list {
		if !w.Archived {
			return w, nil
		}
	}
	now := domain.NowNano()
	w := &domain.Workspace{
		ID:        domain.NewID(),
		OwnerUID:  ownerUID,
		Name:      "預設工作區",
		IsDefault: true,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := s.Repo.SaveWorkspace(ctx, w); err != nil {
		return nil, err
	}
	_ = s.Repo.SaveMemberWSState(ctx, &domain.MemberWorkspaceState{
		OwnerUID: ownerUID, CurrentWorkspaceID: w.ID, UpdatedAt: now,
	})
	return w, nil
}

func (s *Service) ListWorkspaces(ctx context.Context, ownerUID int64) ([]*domain.Workspace, string, error) {
	_, _ = s.EnsureDefaultWorkspace(ctx, ownerUID)
	list, err := s.Repo.ListWorkspaces(ctx, ownerUID, false)
	if err != nil {
		return nil, "", err
	}
	cur := ""
	st, err := s.Repo.GetMemberWSState(ctx, ownerUID)
	if err == nil && st != nil {
		cur = st.CurrentWorkspaceID
	}
	if cur == "" && len(list) > 0 {
		cur = list[0].ID
	}
	return list, cur, nil
}

func (s *Service) CreateWorkspace(ctx context.Context, ownerUID int64, name string) (*domain.Workspace, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return nil, fmt.Errorf("%w: name required", domain.ErrValidation)
	}
	_, _ = s.EnsureDefaultWorkspace(ctx, ownerUID)
	now := domain.NowNano()
	w := &domain.Workspace{
		ID:        domain.NewID(),
		OwnerUID:  ownerUID,
		Name:      name,
		CreatedAt: now,
		UpdatedAt: now,
	}
	if err := s.Repo.SaveWorkspace(ctx, w); err != nil {
		return nil, err
	}
	return w, nil
}

func (s *Service) UpdateWorkspace(ctx context.Context, ownerUID int64, id, name string, review *bool) (*domain.Workspace, error) {
	w, err := s.Repo.GetWorkspace(ctx, id)
	if err != nil {
		return nil, err
	}
	if w.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	if name = strings.TrimSpace(name); name != "" {
		w.Name = name
	}
	if review != nil {
		w.ReviewRequired = *review
	}
	w.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveWorkspace(ctx, w); err != nil {
		return nil, err
	}
	return w, nil
}

func (s *Service) ArchiveWorkspace(ctx context.Context, ownerUID int64, id string) error {
	w, err := s.Repo.GetWorkspace(ctx, id)
	if err != nil {
		return err
	}
	if w.OwnerUID != ownerUID {
		return domain.ErrForbidden
	}
	n, err := s.Repo.CountActiveWorkspaces(ctx, ownerUID)
	if err != nil {
		return err
	}
	if n <= 1 {
		return fmt.Errorf("%w: must keep at least one workspace", domain.ErrValidation)
	}
	w.Archived = true
	w.UpdatedAt = domain.NowNano()
	return s.Repo.SaveWorkspace(ctx, w)
}

func (s *Service) SwitchWorkspace(ctx context.Context, ownerUID int64, id string) error {
	w, err := s.Repo.GetWorkspace(ctx, id)
	if err != nil {
		return err
	}
	if w.OwnerUID != ownerUID || w.Archived {
		return domain.ErrForbidden
	}
	return s.Repo.SaveMemberWSState(ctx, &domain.MemberWorkspaceState{
		OwnerUID: ownerUID, CurrentWorkspaceID: id, UpdatedAt: domain.NowNano(),
	})
}

func (s *Service) CurrentWorkspaceID(ctx context.Context, ownerUID int64) (string, error) {
	list, cur, err := s.ListWorkspaces(ctx, ownerUID)
	if err != nil {
		return "", err
	}
	if cur != "" {
		return cur, nil
	}
	if len(list) > 0 {
		return list[0].ID, nil
	}
	return "", domain.ErrNotFound
}

// --- Draft review ---

func (s *Service) SubmitReview(ctx context.Context, ownerUID int64, refType, refID, workspaceID string) (*domain.DraftReview, error) {
	if refType == "" || refID == "" {
		return nil, fmt.Errorf("%w: ref required", domain.ErrValidation)
	}
	if workspaceID == "" {
		var err error
		workspaceID, err = s.CurrentWorkspaceID(ctx, ownerUID)
		if err != nil {
			return nil, err
		}
	}
	now := domain.NowNano()
	r := &domain.DraftReview{
		ID:          domain.NewID(),
		OwnerUID:    ownerUID,
		WorkspaceID: workspaceID,
		RefType:     refType,
		RefID:       refID,
		Status:      domain.ReviewPending,
		CreatedAt:   now,
		UpdatedAt:   now,
	}
	if err := s.Repo.SaveReview(ctx, r); err != nil {
		return nil, err
	}
	return r, nil
}

func (s *Service) DecideReview(ctx context.Context, actorUID int64, id, status, reason string) (*domain.DraftReview, error) {
	r, err := s.Repo.GetReview(ctx, id)
	if err != nil {
		return nil, err
	}
	// owner of draft OR workspace collaborator may decide
	if r.OwnerUID != actorUID && !s.CanReviewInWorkspace(ctx, actorUID, r.WorkspaceID) {
		return nil, domain.ErrForbidden
	}
	r.Status = status
	r.Reason = reason
	r.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveReview(ctx, r); err != nil {
		return nil, err
	}
	return r, nil
}

func (s *Service) ListReviews(ctx context.Context, ownerUID int64, status string, page, pageSize int) ([]*domain.DraftReview, int64, error) {
	return s.Repo.ListReviews(ctx, ownerUID, status, page, pageSize)
}

// EnsureReviewAllowsAutoSend checks workspace review gate.
func (s *Service) EnsureReviewAllowsAutoSend(ctx context.Context, ownerUID int64, workspaceID, refType, refID string) error {
	if workspaceID == "" {
		var err error
		workspaceID, err = s.CurrentWorkspaceID(ctx, ownerUID)
		if err != nil {
			return nil // no workspace → no gate
		}
	}
	w, err := s.Repo.GetWorkspace(ctx, workspaceID)
	if err != nil {
		if err == domain.ErrNotFound {
			return nil
		}
		return err
	}
	if !w.ReviewRequired {
		return nil
	}
	r, err := s.Repo.FindReviewByRef(ctx, ownerUID, refType, refID)
	if err != nil {
		return domain.ErrReviewPending
	}
	if r.Status != domain.ReviewApproved {
		return domain.ErrReviewPending
	}
	return nil
}

// ExportReportMarkdown returns a simple text report (PDF engine optional; content is the contract).
func (s *Service) ExportReportMarkdown(ctx context.Context, ownerUID int64, workspaceID, yearMonth string) (filename, body string, err error) {
	if yearMonth == "" {
		yearMonth = time.Now().UTC().Format("2006-01")
	}
	w, err := s.Repo.GetWorkspace(ctx, workspaceID)
	if err != nil {
		return "", "", err
	}
	if w.OwnerUID != ownerUID {
		ok, _, _ := s.Repo.IsWorkspaceMember(ctx, workspaceID, ownerUID)
		if !ok {
			return "", "", domain.ErrForbidden
		}
	}
	sum, err := s.Summary(ctx, ownerUID, 0, 0)
	if err != nil {
		return "", "", err
	}
	title := w.Name
	if w.BrandName != "" {
		title = w.BrandName
	}
	filename = fmt.Sprintf("report-%s-%s.md", sanitize(title), yearMonth)
	body = fmt.Sprintf("# 月報 %s · %s\n\n- 觸達: %d\n- 對話: %d\n- 成交: %d (%.0f)\n- 追蹤(可能/確定): %d / %d\n",
		title, yearMonth, sum.Reach, sum.Conversations, sum.Conversions, sum.ConversionAmount, sum.FollowsPossible, sum.FollowsConfirmed)
	if w.FooterText != "" {
		body += "\n---\n" + w.FooterText + "\n"
	}
	if !w.HidePoweredBy {
		body += "\n_Powered by 巡樓_\n"
	}
	return filename, body, nil
}

// ExportReport returns md or pdf data URL.
func (s *Service) ExportReport(ctx context.Context, ownerUID int64, workspaceID, yearMonth, format string) (filename, contentType, dataURL string, err error) {
	fn, body, err := s.ExportReportMarkdown(ctx, ownerUID, workspaceID, yearMonth)
	if err != nil {
		return "", "", "", err
	}
	format = strings.ToLower(strings.TrimSpace(format))
	if format == "pdf" {
		title := strings.TrimSuffix(fn, ".md")
		pdf := ExportReportPDF(title, body)
		b64 := encodeB64(pdf)
		return strings.TrimSuffix(fn, ".md") + ".pdf", "application/pdf", "data:application/pdf;base64," + b64, nil
	}
	b64 := encodeB64([]byte(body))
	return fn, "text/markdown", "data:text/markdown;base64," + b64, nil
}

func encodeB64(b []byte) string {
	return base64.StdEncoding.EncodeToString(b)
}

func sanitize(s string) string {
	s = strings.TrimSpace(s)
	s = strings.ReplaceAll(s, " ", "-")
	if s == "" {
		return "workspace"
	}
	return s
}
