package repository

import (
	"context"
	"sort"
	"strings"

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

func (m *Memory) UpsertByExternalID(_ context.Context, o *domain.Opportunity) (*domain.Opportunity, error) {
	if o == nil {
		return nil, domain.ErrValidation
	}
	m.mu.Lock()
	defer m.mu.Unlock()

	key := ownerExternalKey(o.OwnerUID, strings.TrimSpace(o.ExternalID))
	if id, ok := m.ownerExternal[key]; ok {
		existing := m.opportunities[id]
		// Hit: merge matched_terms only — do not re-judge or overwrite status/score.
		existing.MatchedTerms = domain.MergeMatchedTerms(existing.MatchedTerms, o.MatchedTerms)
		for _, incoming := range o.ProductMatches {
			if err := incoming.ValidateForWrite(); err != nil {
				return nil, err
			}
			mergeMemoryProductMatch(existing, incoming)
		}
		existing.LastMatchedAt = domain.NowNano()
		arbitrateMemoryPrimary(existing)
		existing.UpdatedAt = domain.NowNano()
		return cloneOpportunity(existing), nil
	}

	if err := o.ValidateForWrite(); err != nil {
		return nil, err
	}
	domain.ApplyPrimaryProduct(o)
	if o.ID == "" {
		o.ID = domain.NewID()
	}
	if o.ReviewState == "" {
		o.ReviewState = reviewStateFor(o)
	}
	if !domain.IsReviewState(o.ReviewState) {
		return nil, domain.ErrValidation
	}
	now := domain.NowNano()
	if o.CreatedAt == 0 {
		o.CreatedAt = now
	}
	o.UpdatedAt = now
	if o.LastMatchedAt == 0 {
		o.LastMatchedAt = now
	}
	if o.IntentBand == "" && o.Status != domain.OppJudging {
		o.ApplyBandFromScore()
	}

	cp := *o
	cp.Reasons = append([]domain.OpportunityReason(nil), o.Reasons...)
	cp.MatchedTerms = append([]string(nil), o.MatchedTerms...)
	cp.ProductMatches = make([]*domain.ProductMatch, 0, len(o.ProductMatches))
	for _, match := range o.ProductMatches {
		cp.ProductMatches = append(cp.ProductMatches, domain.CloneProductMatch(match))
	}
	if o.Override != nil {
		ov := *o.Override
		cp.Override = &ov
	}
	if o.PrimaryProductOverride != nil {
		ov := *o.PrimaryProductOverride
		cp.PrimaryProductOverride = &ov
	}
	m.opportunities[cp.ID] = &cp
	m.ownerExternal[key] = cp.ID

	out := cp
	out.Reasons = append([]domain.OpportunityReason(nil), cp.Reasons...)
	out.MatchedTerms = append([]string(nil), cp.MatchedTerms...)
	if cp.Override != nil {
		ov := *cp.Override
		out.Override = &ov
	}
	out.ProductMatches = make([]*domain.ProductMatch, 0, len(cp.ProductMatches))
	for _, match := range cp.ProductMatches {
		out.ProductMatches = append(out.ProductMatches, domain.CloneProductMatch(match))
	}
	if cp.PrimaryProductOverride != nil {
		ov := *cp.PrimaryProductOverride
		out.PrimaryProductOverride = &ov
	}
	return &out, nil
}

func (m *Memory) MergeProductMatch(_ context.Context, ownerUID int64, opportunityID string, match *domain.ProductMatch) (*domain.Opportunity, error) {
	if ownerUID <= 0 || match == nil {
		return nil, domain.ErrValidation
	}
	if err := match.ValidateForWrite(); err != nil {
		return nil, err
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[opportunityID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	if o.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	mergeMemoryProductMatch(o, match)
	arbitrateMemoryPrimary(o)
	o.UpdatedAt = domain.NowNano()
	return cloneOpportunity(o), nil
}

func (m *Memory) SetPrimaryProduct(_ context.Context, ownerUID int64, opportunityID, productID string, override *domain.ProductPrimaryOverride) (*domain.Opportunity, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[opportunityID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	if o.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	var match *domain.ProductMatch
	for _, candidate := range o.ProductMatches {
		if candidate.ProductID == productID {
			match = candidate
			break
		}
	}
	if match == nil {
		return nil, domain.ErrValidation
	}
	if (!match.Eligible || match.Excluded) && (override == nil || strings.TrimSpace(override.Reason) == "") {
		return nil, domain.ErrValidation
	}
	if override != nil {
		cp := *override
		if cp.At == 0 {
			cp.At = domain.NowNano()
		}
		o.PrimaryProductOverride = &cp
		o.PrimaryProductOverridden = true
	} else {
		o.PrimaryProductOverride = nil
		o.PrimaryProductOverridden = false
	}
	setMemoryPrimary(o, match)
	o.UpdatedAt = domain.NowNano()
	return cloneOpportunity(o), nil
}

func mergeMemoryProductMatch(o *domain.Opportunity, incoming *domain.ProductMatch) {
	for _, current := range o.ProductMatches {
		if current.ProductID == incoming.ProductID {
			current.WatchIDs = domain.MergeMatchedTerms(current.WatchIDs, incoming.WatchIDs)
			current.MatchedTerms = domain.MergeMatchedTerms(current.MatchedTerms, incoming.MatchedTerms)
			return
		}
	}
	o.ProductMatches = append(o.ProductMatches, domain.CloneProductMatch(incoming))
}

func setMemoryPrimary(o *domain.Opportunity, match *domain.ProductMatch) {
	o.PrimaryBrandID, o.PrimaryProductID = match.BrandID, match.ProductID
	o.PrimaryBrandName, o.PrimaryProductLabel = match.BrandNameSnapshot, match.ProductLabelSnapshot
	o.PrimaryProductFitScore, o.PrimaryProductFitBand = match.ProductFitScore, match.ProductFitBand
	domain.ApplyPrimaryProduct(o)
}

func arbitrateMemoryPrimary(o *domain.Opportunity) {
	domain.ApplyPrimaryProduct(o)
}

func (m *Memory) GetOpportunity(_ context.Context, id string) (*domain.Opportunity, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[id]
	if !ok {
		return nil, domain.ErrNotFound
	}
	return cloneOpportunity(o), nil
}

func (m *Memory) GetByExternalID(_ context.Context, ownerUID int64, externalID string) (*domain.Opportunity, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	id, ok := m.ownerExternal[ownerExternalKey(ownerUID, strings.TrimSpace(externalID))]
	if !ok {
		return nil, domain.ErrNotFound
	}
	o, ok := m.opportunities[id]
	if !ok {
		return nil, domain.ErrNotFound
	}
	return cloneOpportunity(o), nil
}

func (m *Memory) ListOpportunities(_ context.Context, ownerUID int64, f domain.OpportunityListFilter) ([]*domain.Opportunity, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	matched := make([]*domain.Opportunity, 0)
	for _, o := range m.opportunities {
		if o.OwnerUID != ownerUID {
			continue
		}
		if f.Band != "" && o.IntentBand != f.Band {
			continue
		}
		if len(f.Statuses) > 0 {
			ok := false
			for _, st := range f.Statuses {
				if o.Status == st {
					ok = true
					break
				}
			}
			if !ok {
				continue
			}
		} else if f.Status != "" && o.Status != f.Status {
			continue
		}
		if f.WatchID != "" && o.WatchID != f.WatchID {
			continue
		}
		if f.BrandID != "" && !opportunityHasBrand(o, f.BrandID) {
			continue
		}
		if f.ProductID != "" && !opportunityHasProduct(o, f.ProductID) {
			continue
		}
		if f.FitBand != "" && !opportunityHasFitBand(o, f.FitBand) {
			continue
		}
		if f.MatchState != "" && !opportunityMatchState(o, f.MatchState) {
			continue
		}
		if f.ReviewState != "" && reviewStateFor(o) != f.ReviewState {
			continue
		}
		if f.PriorityBand != "" && o.PriorityBand != f.PriorityBand {
			continue
		}
		if f.CreatedFrom > 0 && o.CreatedAt < f.CreatedFrom {
			continue
		}
		if f.CreatedTo > 0 && o.CreatedAt >= f.CreatedTo {
			continue
		}
		if f.PostedFrom > 0 || f.PostedTo > 0 {
			if f.TimeScope == "today" || f.TimeScope == "7d" {
				if !domain.InInboxTimeScope(o.PostedAt, o.LastMatchedAt, o.CreatedAt, f.PostedFrom, f.PostedTo) {
					continue
				}
			} else {
				if f.PostedFrom > 0 && o.PostedAt < f.PostedFrom {
					continue
				}
				if f.PostedTo > 0 && o.PostedAt >= f.PostedTo {
					continue
				}
			}
		}
		matched = append(matched, cloneOpportunity(o))
	}
	sort.Slice(matched, func(i, j int) bool {
		if f.Sort == "posted" || f.Sort == "newest" {
			if matched[i].PostedAt != matched[j].PostedAt {
				return matched[i].PostedAt > matched[j].PostedAt
			}
		} else if f.Sort == "oldest" {
			if matched[i].PostedAt != matched[j].PostedAt {
				return matched[i].PostedAt < matched[j].PostedAt
			}
		} else if f.Sort == "product_fit" {
			if primaryFit(matched[i]) != primaryFit(matched[j]) {
				return primaryFit(matched[i]) > primaryFit(matched[j])
			}
		} else if f.Sort == "demand_intent" {
			if matched[i].IntentScore != matched[j].IntentScore {
				return matched[i].IntentScore > matched[j].IntentScore
			}
		} else if f.Sort == "priority" || f.Sort == "priority_score" {
			if matched[i].PriorityScore != matched[j].PriorityScore {
				return matched[i].PriorityScore > matched[j].PriorityScore
			}
		} else {
			if matched[i].IntentScore != matched[j].IntentScore {
				return matched[i].IntentScore > matched[j].IntentScore
			}
			if primaryFit(matched[i]) != primaryFit(matched[j]) {
				return primaryFit(matched[i]) > primaryFit(matched[j])
			}
			if matched[i].PostedAt != matched[j].PostedAt {
				return matched[i].PostedAt > matched[j].PostedAt
			}
		}
		return matched[i].ID < matched[j].ID
	})

	total := int64(len(matched))
	page, ps := f.Page, f.PageSize
	if page < 1 {
		page = 1
	}
	if ps < 1 {
		ps = 20
	}
	start := (page - 1) * ps
	if start >= len(matched) {
		return nil, total, nil
	}
	end := start + ps
	if end > len(matched) {
		end = len(matched)
	}
	return matched[start:end], total, nil
}

func (m *Memory) UpdateOpportunityReviewState(_ context.Context, ownerUID int64, opportunityID string, patch domain.ReviewStatePatch) (*domain.Opportunity, error) {
	if ownerUID <= 0 || !domain.IsReviewState(patch.State) {
		return nil, domain.ErrValidation
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[opportunityID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	if o.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	current := reviewStateFor(o)
	if patch.State == domain.ReviewRemoved {
		if !domain.IsRemovalReason(patch.RemovalReason) {
			return nil, domain.ErrValidation
		}
		if patch.RemovalReason == domain.RemovalOther && strings.TrimSpace(patch.RemovalNote) == "" {
			return nil, domain.ErrValidation
		}
		if patch.RemovalReason == domain.RemovalDuplicate {
			if patch.DuplicateOpportunityID == "" || patch.DuplicateOpportunityID == opportunityID {
				return nil, domain.ErrValidation
			}
			target, exists := m.opportunities[patch.DuplicateOpportunityID]
			if !exists || target.OwnerUID != ownerUID || target.AuthorHandle != o.AuthorHandle {
				return nil, domain.ErrValidation
			}
		}
		if current == domain.ReviewRemoved && o.RemovalReason == patch.RemovalReason && o.RemovalNote == strings.TrimSpace(patch.RemovalNote) {
			return cloneOpportunity(o), nil
		}
		o.PreviousReviewState = current
		o.ReviewState = domain.ReviewRemoved
		o.RemovalReason = patch.RemovalReason
		o.RemovalNote = strings.TrimSpace(patch.RemovalNote)
		o.RemovedAt = domain.NowNano()
		o.RemovedBy = ownerUID
	} else {
		if current == patch.State && o.RemovalReason == "" {
			return cloneOpportunity(o), nil
		}
		o.ReviewState = patch.State
		o.RemovalReason = ""
		o.RemovalNote = ""
		o.RemovedAt = 0
		o.RemovedBy = 0
	}
	event := domain.ReviewEvent{OwnerUID: ownerUID, OpportunityID: opportunityID, From: current, To: patch.State, Reason: patch.RemovalReason, Note: patch.RemovalNote, ActorUID: ownerUID}
	if err := event.Normalize(); err != nil {
		return nil, err
	}
	m.reviewEvents[opportunityID] = append(m.reviewEvents[opportunityID], event)
	o.UpdatedAt = domain.NowNano()
	return cloneOpportunity(o), nil
}

func (m *Memory) ListOpportunityReviewEvents(_ context.Context, ownerUID int64, opportunityID string) ([]domain.ReviewEvent, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[opportunityID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	if o.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	events := append([]domain.ReviewEvent(nil), m.reviewEvents[opportunityID]...)
	return events, nil
}

func reviewStateFor(o *domain.Opportunity) string {
	if o.ReviewState != "" {
		return o.ReviewState
	}
	state, _ := domain.LegacyReviewState(o.Status)
	return state
}

func (m *Memory) CountToday(_ context.Context, ownerUID int64, at int64) (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	start, end := domain.UTCDayBounds(at)
	var n int64
	for _, o := range m.opportunities {
		if o.OwnerUID == ownerUID && o.CreatedAt >= start && o.CreatedAt < end {
			n++
		}
	}
	return n, nil
}

func (m *Memory) UpdateOpportunityStatus(_ context.Context, id string, status string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[id]
	if !ok {
		return domain.ErrNotFound
	}
	if err := o.Transition(status); err != nil {
		return err
	}
	return nil
}

func (m *Memory) SetOpportunityOverride(_ context.Context, id string, ov *domain.OpportunityOverride, newBand, newStatus string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[id]
	if !ok {
		return domain.ErrNotFound
	}
	if ov == nil {
		return domain.ErrValidation
	}
	if newStatus != "" && newStatus != o.Status {
		if err := o.Transition(newStatus); err != nil {
			return err
		}
	}
	if newBand != "" {
		if !domain.IsIntentBand(newBand) {
			return domain.ErrValidation
		}
		o.IntentBand = newBand
	}
	cp := *ov
	if cp.At == 0 {
		cp.At = domain.NowNano()
	}
	o.Override = &cp
	o.UpdatedAt = domain.NowNano()
	return nil
}

func cloneOpportunity(o *domain.Opportunity) *domain.Opportunity {
	cp := *o
	cp.Reasons = append([]domain.OpportunityReason(nil), o.Reasons...)
	cp.MatchedTerms = append([]string(nil), o.MatchedTerms...)
	if o.Override != nil {
		ov := *o.Override
		cp.Override = &ov
	}
	cp.ProductMatches = make([]*domain.ProductMatch, 0, len(o.ProductMatches))
	for _, match := range o.ProductMatches {
		cp.ProductMatches = append(cp.ProductMatches, domain.CloneProductMatch(match))
	}
	if o.PrimaryProductOverride != nil {
		ov := *o.PrimaryProductOverride
		cp.PrimaryProductOverride = &ov
	}
	return &cp
}

func opportunityHasBrand(o *domain.Opportunity, id string) bool {
	for _, m := range o.ProductMatches {
		if m.BrandID == id {
			return true
		}
	}
	return false
}
func opportunityHasProduct(o *domain.Opportunity, id string) bool {
	for _, m := range o.ProductMatches {
		if m.ProductID == id {
			return true
		}
	}
	return false
}
func opportunityHasFitBand(o *domain.Opportunity, band string) bool {
	for _, m := range o.ProductMatches {
		if m.ProductFitBand == band {
			return true
		}
	}
	return false
}
func opportunityMatchState(o *domain.Opportunity, state string) bool {
	if state == "generic" {
		return len(o.ProductMatches) == 0
	}
	for _, m := range o.ProductMatches {
		switch state {
		case "eligible":
			if m.Eligible && !m.Excluded {
				return true
			}
		case "weak":
			if m.ProductFitBand == domain.ProductFitBandWeak {
				return true
			}
		case "excluded":
			if m.Excluded {
				return true
			}
		}
	}
	if state == "stale" {
		return domain.IsStaleHardReject(o.PostedAt, domain.NowNano())
	}
	return false
}
func primaryFit(o *domain.Opportunity) int { return o.PrimaryProductFitScore }

func (m *Memory) SetOpportunityContact(_ context.Context, id, contactID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	o, ok := m.opportunities[id]
	if !ok {
		return domain.ErrNotFound
	}
	o.ContactID = contactID
	o.UpdatedAt = domain.NowNano()
	return nil
}
