package usecase

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"apps/backend/internal/module/ai"
	"apps/backend/internal/module/radar/domain"
	usageUC "apps/backend/internal/module/usage/usecase"
)

type Service struct {
	Repo          domain.Repository
	ProductSource ProductContextSource
	// Quota 未接時退回最低方案上限，見 watch_quota.go。
	Quota PlanQuota
	// Usage 為 nil 時不扣點（單元測試路徑）。
	Usage *usageUC.Service
	// AI 是測試／fallback client；AIRegistry＋ResolveAI 才是正式路徑。
	AI         ai.Client
	AIRegistry *ai.Registry
	ResolveAI  func(ctx context.Context, uid int64) (provider, model, apiKey string, err error)
	ResolveKey func(ctx context.Context, uid int64, meter string) (mode, apiKey string, err error)
	// PainTerms 可空：接不上只影響建議品質。
	PainTerms PainTermSource
	// SweepJobs 每日排程／手動觸發共用；nil 時 ScheduleDailySweeps 回 ErrNotReady。
	SweepJobs SweepJobScheduler
	// Dual-path fetch (prefer HitFetch adapter over individual providers).
	HitFetch       HitFetcher
	Search         ThreadSearcher
	Chrome         ChromeSearcher
	DevMode        DevModeReader
	CrawlerSession CrawlerSessionReader
	// Notifier for sweep failures (optional).
	Notifier SweepNotifier
	// CRM bridge for accept → contact (optional until M4).
	CRM ContactBinder
	// Health gates auto-send of public replies (AccountHealth throttle).
	Health       HealthGate
	judgeCacheMu sync.Mutex
	judgeCache   map[string]cachedJudge
	// ReplyQueue 是既有 Outbox 佇列（studio.QueueExternalReply）；nil 時 outbox 標記
	// 只記錄不真送，讓離線測試／demo 環境仍能跑（見 MarkReplyUsed）。
	ReplyQueue ReplyQueue
	// MediaResolver 把商機的 permalink 解成可送出的 Threads 數字 media id
	// （商機 external_id 本身是 permalink，不是 media id，借用海巡既有 crawler 解析，不重造第二套）。
	MediaResolver MediaResolver
}

type cachedJudge struct {
	result    *JudgeResult
	createdAt int64
}

// ContactBinder creates or binds a CRM contact when accepting an opportunity.
type ContactBinder interface {
	BindOpportunity(ctx context.Context, ownerUID int64, opp *domain.Opportunity) (contactID string, err error)
}

func New(repo domain.Repository) *Service {
	return &Service{Repo: repo, judgeCache: map[string]cachedJudge{}}
}

/*
GetServiceProfile 未建檔時回 domain.ErrNotFound，由呼叫端決定怎麼表達。

HTTP 層會把它翻成 exists=false 的 200（表單本來就要能開空的），但 usecase 不能
自己回一個零值檔案 —— 那樣「沒建檔」與「建了一份空的」就分不出來。
*/
func (s *Service) GetServiceProfile(ctx context.Context, ownerUID int64) (*domain.ServiceProfile, error) {
	if ownerUID <= 0 {
		return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
	}
	return s.Repo.GetServiceProfile(ctx, ownerUID)
}

func (s *Service) HasServiceProfile(ctx context.Context, ownerUID int64) (bool, error) {
	_, err := s.GetServiceProfile(ctx, ownerUID)
	if errors.Is(err, domain.ErrNotFound) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// UpsertServiceProfile 整份覆寫。owner_uid 由呼叫端從 JWT 取，request 帶的一律忽略。
func (s *Service) UpsertServiceProfile(ctx context.Context, ownerUID int64, in *domain.ServiceProfile) (*domain.ServiceProfile, error) {
	if in == nil {
		return nil, fmt.Errorf("%w: profile required", domain.ErrValidation)
	}
	in.OwnerUID = ownerUID
	if err := in.Normalize(); err != nil {
		return nil, err
	}

	now := domain.NowNano()
	in.UpdatedAt = now
	in.CreatedAt = now
	if existing, err := s.Repo.GetServiceProfile(ctx, ownerUID); err == nil {
		in.CreatedAt = existing.CreatedAt
	} else if !errors.Is(err, domain.ErrNotFound) {
		return nil, err
	}

	if err := s.Repo.SaveServiceProfile(ctx, in); err != nil {
		return nil, err
	}
	return s.Repo.GetServiceProfile(ctx, ownerUID)
}
