package usecase

import (
	"context"
	"fmt"
	"strings"
	"time"
	"unicode/utf8"

	"apps/backend/internal/module/crm/domain"
	radarDomain "apps/backend/internal/module/radar/domain"
)

// GrowthOutcomes writes conversion into existing growth_outcomes (source_type=radar_opportunity).
type GrowthOutcomes interface {
	RecordConversion(ctx context.Context, ownerUID int64, contactID string, amount float64, currency, note string) (outcomeID string, err error)
	AmendConversion(ctx context.Context, ownerUID int64, outcomeID string, amount float64, currency, note string) error
}

type Service struct {
	Repo   domain.Repository
	Growth GrowthOutcomes
	// RadarOpps optional for contact detail briefs and conversion attribution
	RadarOpps interface {
		GetOpportunity(ctx context.Context, id string) (*radarDomain.Opportunity, error)
		ListOpportunities(ctx context.Context, ownerUID int64, f radarDomain.OpportunityListFilter) ([]*radarDomain.Opportunity, int64, error)
	}
	Notifier FollowUpNotifier
}

type FollowUpNotifier interface {
	NotifyFollowUp(ctx context.Context, ownerUID int64, contactID, followUpID string) error
}

func New(repo domain.Repository) *Service {
	return &Service{Repo: repo}
}

// BindOpportunity implements radar.ContactBinder.
func (s *Service) BindOpportunity(ctx context.Context, ownerUID int64, opp *radarDomain.Opportunity) (string, error) {
	if opp == nil {
		return "", domain.ErrValidation
	}
	c := &domain.Contact{
		OwnerUID:       ownerUID,
		SourcePlatform: domain.PlatformThreads,
		AuthorHandle:   opp.AuthorHandle,
		Stage:          domain.StageNewFound,
		OpportunityIDs: []string{opp.ID},
		TopIntentBand:  opp.IntentBand,
		TopIntentScore: opp.IntentScore,
		LastTouchAt:    domain.NowNano(),
		FollowUpDays:   domain.DefaultFollowUpDays,
	}
	got, err := s.Repo.UpsertContactByIdentity(ctx, c)
	if err != nil {
		return "", err
	}
	// touch
	_ = s.Repo.InsertTouch(ctx, &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: got.ID,
		Type: domain.TouchStage, ToStage: got.Stage, Body: "從商機加入名單",
		ActorUID: ownerUID, CreatedAt: domain.NowNano(),
	})
	return got.ID, nil
}

func (s *Service) ListContacts(ctx context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, map[string]int, error) {
	if ownerUID <= 0 {
		return nil, 0, nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
	}
	f.Query = strings.TrimSpace(f.Query)
	if utf8.RuneCountInString(f.Query) > 80 {
		return nil, 0, nil, fmt.Errorf("%w: query is too long", domain.ErrValidation)
	}
	if f.Stage != "" && !domain.IsStage(f.Stage) {
		return nil, 0, nil, fmt.Errorf("%w: unknown stage %q", domain.ErrValidation, f.Stage)
	}
	if f.PageSize > 50 {
		f.PageSize = 50
	}
	list, total, err := s.Repo.ListContacts(ctx, ownerUID, f)
	if err != nil {
		return nil, 0, nil, err
	}
	counts, err := s.Repo.CountByStage(ctx, ownerUID)
	if err != nil {
		return nil, 0, nil, err
	}
	return list, total, counts, nil
}

func (s *Service) GetContact(ctx context.Context, ownerUID, page, pageSize int64, id string) (*domain.Contact, []*domain.ContactTouch, int64, error) {
	c, err := s.GetContactOnly(ctx, ownerUID, id)
	if err != nil {
		return nil, nil, 0, err
	}
	touches, total, err := s.Repo.ListTouches(ctx, ownerUID, id, int(page), int(pageSize))
	return c, touches, total, err
}

// GetContactOnly returns the contact without timeline (owner-checked).
func (s *Service) GetContactOnly(ctx context.Context, ownerUID int64, id string) (*domain.Contact, error) {
	c, err := s.Repo.GetContact(ctx, id)
	if err != nil {
		return nil, err
	}
	if c.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	if c.RemovedAt > 0 {
		return nil, domain.ErrNotFound
	}
	return c, nil
}

// RemoveContact removes a contact from the working list and closes active
// follow-ups. Opportunity, touch, and conversion history keep their contact ID.
func (s *Service) RemoveContact(ctx context.Context, ownerUID int64, id string) error {
	if _, err := s.GetContactOnly(ctx, ownerUID, id); err != nil {
		return err
	}
	return s.Repo.RemoveContact(ctx, ownerUID, id, domain.NowNano())
}

// OpportunityBriefs resolves linked opportunities for contact detail.
func (s *Service) OpportunityBriefs(ctx context.Context, ownerUID int64, ids []string) []map[string]any {
	out := make([]map[string]any, 0, len(ids))
	if s.RadarOpps == nil {
		return out
	}
	for _, id := range ids {
		o, err := s.RadarOpps.GetOpportunity(ctx, id)
		if err != nil || o == nil || o.OwnerUID != ownerUID {
			continue
		}
		out = append(out, map[string]any{
			"id": o.ID, "permalink": o.Permalink, "text": o.Text,
			"intent_score": o.IntentScore, "intent_band": o.IntentBand, "created_at": o.CreatedAt,
		})
	}
	return out
}

func (s *Service) UpdateStage(ctx context.Context, ownerUID int64, id, stage, note string) (*domain.Contact, error) {
	if !domain.IsStage(stage) {
		return nil, fmt.Errorf("%w: unknown stage %q", domain.ErrValidation, stage)
	}
	c, err := s.Repo.GetContact(ctx, id)
	if err != nil {
		return nil, err
	}
	if c.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	from := c.Stage
	c.Stage = stage
	c.LastTouchAt = domain.NowNano()
	c.UpdatedAt = c.LastTouchAt
	if err := s.Repo.SaveContact(ctx, c); err != nil {
		return nil, err
	}
	_ = s.Repo.InsertTouch(ctx, &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: id,
		Type: domain.TouchStage, FromStage: from, ToStage: stage, Body: note,
		ActorUID: ownerUID, CreatedAt: domain.NowNano(),
	})
	return c, nil
}

func (s *Service) SetFollowUp(ctx context.Context, ownerUID int64, id string, needs bool, days int) (*domain.Contact, error) {
	c, err := s.Repo.GetContact(ctx, id)
	if err != nil {
		return nil, err
	}
	if c.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	c.NeedsFollowUp = needs
	if days > 0 {
		c.FollowUpDays = days
	}
	c.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveContact(ctx, c); err != nil {
		return nil, err
	}
	if needs {
		due := domain.NowNano() + int64(c.FollowUpDays)*int64(24*time.Hour)
		fu := &domain.FollowUp{
			ID: domain.NewID(), OwnerUID: ownerUID, ContactID: id,
			DueAt: due, Status: domain.FollowUpScheduled,
			CreatedAt: domain.NowNano(), UpdatedAt: domain.NowNano(),
		}
		_ = s.Repo.SaveFollowUp(ctx, fu)
	}
	return c, nil
}

func (s *Service) AddNote(ctx context.Context, ownerUID int64, id, body string) (*domain.ContactTouch, error) {
	c, err := s.Repo.GetContact(ctx, id)
	if err != nil {
		return nil, err
	}
	if c.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	t := &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: id,
		Type: domain.TouchNote, Body: body, ActorUID: ownerUID, CreatedAt: domain.NowNano(),
	}
	if err := s.Repo.InsertTouch(ctx, t); err != nil {
		return nil, err
	}
	c.LastTouchAt = t.CreatedAt
	c.UpdatedAt = t.CreatedAt
	_ = s.Repo.SaveContact(ctx, c)
	return t, nil
}

func (s *Service) Merge(ctx context.Context, ownerUID int64, targetID, sourceID string) (*domain.Contact, error) {
	if targetID == sourceID {
		return nil, fmt.Errorf("%w: cannot merge contact into itself", domain.ErrValidation)
	}
	target, err := s.Repo.GetContact(ctx, targetID)
	if err != nil {
		return nil, err
	}
	source, err := s.Repo.GetContact(ctx, sourceID)
	if err != nil {
		return nil, err
	}
	if target.OwnerUID != ownerUID || source.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	seen := map[string]bool{}
	for _, id := range target.OpportunityIDs {
		seen[id] = true
	}
	for _, id := range source.OpportunityIDs {
		if !seen[id] {
			target.OpportunityIDs = append(target.OpportunityIDs, id)
		}
	}
	target.MergedFrom = append(target.MergedFrom, sourceID)
	if source.TopIntentScore > target.TopIntentScore {
		target.TopIntentScore = source.TopIntentScore
		target.TopIntentBand = source.TopIntentBand
	}
	target.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveContact(ctx, target); err != nil {
		return nil, err
	}
	// mark source lost / archived-ish
	source.Stage = domain.StageLost
	source.UpdatedAt = domain.NowNano()
	_ = s.Repo.SaveContact(ctx, source)
	return target, nil
}

func (s *Service) Unmerge(ctx context.Context, ownerUID int64, targetID, mergedID string) (*domain.Contact, error) {
	target, err := s.Repo.GetContact(ctx, targetID)
	if err != nil {
		return nil, err
	}
	if target.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	out := make([]string, 0, len(target.MergedFrom))
	for _, id := range target.MergedFrom {
		if id != mergedID {
			out = append(out, id)
		}
	}
	target.MergedFrom = out
	target.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveContact(ctx, target); err != nil {
		return nil, err
	}
	return target, nil
}

func (s *Service) ReportConversion(ctx context.Context, ownerUID int64, contactID string, amount float64, currency, note string) (*domain.Contact, string, error) {
	c, err := s.Repo.GetContact(ctx, contactID)
	if err != nil {
		return nil, "", err
	}
	if c.OwnerUID != ownerUID {
		return nil, "", domain.ErrForbidden
	}
	if s.Growth == nil {
		return nil, "", fmt.Errorf("%w: growth outcomes not configured", domain.ErrNotReady)
	}
	now := domain.NowNano()
	outcomeID, err := s.Growth.RecordConversion(ctx, ownerUID, contactID, amount, currency, note)
	if err != nil {
		return nil, "", err
	}
	c.Stage = domain.StageWon
	c.OutcomeID = outcomeID
	c.LastTouchAt = now
	c.UpdatedAt = now
	if err := s.Repo.SaveContact(ctx, c); err != nil {
		return nil, "", err
	}
	_ = s.Repo.InsertTouch(ctx, &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: contactID,
		Type: domain.TouchConversion, ToStage: domain.StageWon,
		Body:     fmt.Sprintf("成交回報 %.0f %s %s", amount, currency, note),
		ActorUID: ownerUID, CreatedAt: now,
	})
	return c, outcomeID, nil
}

func (s *Service) UpdateConversion(ctx context.Context, ownerUID int64, contactID string, amount float64, currency, note string) (*domain.Contact, string, error) {
	c, err := s.Repo.GetContact(ctx, contactID)
	if err != nil {
		return nil, "", err
	}
	if c.OwnerUID != ownerUID {
		return nil, "", domain.ErrForbidden
	}
	if s.Growth == nil || c.OutcomeID == "" {
		return s.ReportConversion(ctx, ownerUID, contactID, amount, currency, note)
	}
	if err := s.Growth.AmendConversion(ctx, ownerUID, c.OutcomeID, amount, currency, note); err != nil {
		return s.ReportConversion(ctx, ownerUID, contactID, amount, currency, note)
	}
	_ = s.Repo.InsertTouch(ctx, &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: contactID,
		Type: domain.TouchConversion, Body: "修改成交紀錄",
		ActorUID: ownerUID, CreatedAt: domain.NowNano(),
	})
	return c, c.OutcomeID, nil
}

func (s *Service) DeleteConversion(ctx context.Context, ownerUID int64, contactID string) error {
	c, err := s.Repo.GetContact(ctx, contactID)
	if err != nil {
		return err
	}
	if c.OwnerUID != ownerUID {
		return domain.ErrForbidden
	}
	// audit touch; leave outcome row but clear link
	c.OutcomeID = ""
	c.UpdatedAt = domain.NowNano()
	_ = s.Repo.SaveContact(ctx, c)
	return s.Repo.InsertTouch(ctx, &domain.ContactTouch{
		ID: domain.NewID(), OwnerUID: ownerUID, ContactID: contactID,
		Type: domain.TouchConversion, Body: "刪除成交標記",
		ActorUID: ownerUID, CreatedAt: domain.NowNano(),
	})
}

func (s *Service) ListFollowUps(ctx context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
	return s.Repo.ListFollowUps(ctx, ownerUID, f)
}

func (s *Service) DoneFollowUp(ctx context.Context, ownerUID int64, id string) (*domain.FollowUp, error) {
	f, err := s.Repo.GetFollowUp(ctx, id)
	if err != nil {
		return nil, err
	}
	if f.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	f.Status = domain.FollowUpDone
	f.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveFollowUp(ctx, f); err != nil {
		return nil, err
	}
	return f, nil
}

func (s *Service) SnoozeFollowUp(ctx context.Context, ownerUID int64, id string, days int) (*domain.FollowUp, error) {
	if days <= 0 {
		days = 3
	}
	f, err := s.Repo.GetFollowUp(ctx, id)
	if err != nil {
		return nil, err
	}
	if f.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	// spec FU-04：延後只是把到期日後移，狀態回到可再次排程的 scheduled。
	f.Status = domain.FollowUpScheduled
	f.DueAt = domain.NowNano() + int64(days)*int64(24*time.Hour)
	f.UpdatedAt = domain.NowNano()
	if err := s.Repo.SaveFollowUp(ctx, f); err != nil {
		return nil, err
	}
	return f, nil
}

// GenerateFollowUpMessage drafts a short follow-up message (T588); never auto-sends.
func (s *Service) GenerateFollowUpMessage(ctx context.Context, ownerUID int64, followUpID string) (string, error) {
	f, err := s.Repo.GetFollowUp(ctx, followUpID)
	if err != nil {
		return "", err
	}
	if f.OwnerUID != ownerUID {
		return "", domain.ErrForbidden
	}
	c, err := s.GetContactOnly(ctx, ownerUID, f.ContactID)
	if err != nil {
		return "", err
	}
	handle := c.AuthorHandle
	if handle == "" {
		handle = "你好"
	} else {
		handle = "@" + handle
	}
	// 繁中台灣語氣、不硬銷；使用者可再改。
	return fmt.Sprintf(
		"%s 嗨，上次聊到你的需求，想再跟你確認一下目前進度如何？若還在比較方案，我可以幫你整理重點給你參考。",
		handle,
	), nil
}

// ScanFollowUps marks due items notified (M5 job).
func (s *Service) ScanFollowUps(ctx context.Context, now int64) (int, error) {
	if now <= 0 {
		now = domain.NowNano()
	}
	due, err := s.Repo.ListDueFollowUps(ctx, now, 100)
	if err != nil {
		return 0, err
	}
	n := 0
	for _, f := range due {
		f.NotifiedCount++
		f.UpdatedAt = now
		if f.NotifiedCount >= domain.MaxFollowUpNotifications {
			// 達上限只建議轉未成交，不再排下一次通知（spec FU-03）
			f.Status = domain.FollowUpEscalated
		} else {
			// 到期日必須往後推，否則下一個 tick 會立刻重複通知同一筆
			f.Status = domain.FollowUpNotified
			f.DueAt = now + int64(s.followUpDays(ctx, f.ContactID))*int64(24*time.Hour)
		}
		if err := s.Repo.SaveFollowUp(ctx, f); err != nil {
			continue
		}
		if s.Notifier != nil {
			_ = s.Notifier.NotifyFollowUp(ctx, f.OwnerUID, f.ContactID, f.ID)
		}
		n++
	}
	return n, nil
}

// followUpDays resolves the owner's configured interval, falling back to the
// default when the contact is gone or never had one set.
func (s *Service) followUpDays(ctx context.Context, contactID string) int {
	c, err := s.Repo.GetContact(ctx, contactID)
	if err != nil || c == nil || c.FollowUpDays <= 0 {
		return domain.DefaultFollowUpDays
	}
	return c.FollowUpDays
}

