package repository

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"sync"

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

type Memory struct {
	mu        sync.Mutex
	contacts  map[string]*domain.Contact
	identity  map[string]string // owner|platform|handle → id
	touches   map[string]*domain.ContactTouch
	followups map[string]*domain.FollowUp
}

func NewMemory() *Memory {
	return &Memory{
		contacts:  map[string]*domain.Contact{},
		identity:  map[string]string{},
		touches:   map[string]*domain.ContactTouch{},
		followups: map[string]*domain.FollowUp{},
	}
}

func idKey(owner int64, platform, handle string) string {
	return fmt.Sprintf("%d|%s|%s", owner, platform, handle)
}

func (m *Memory) UpsertContactByIdentity(_ context.Context, c *domain.Contact) (*domain.Contact, error) {
	if err := c.Normalize(); err != nil {
		return nil, err
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	key := idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)
	if id, ok := m.identity[key]; ok {
		ex := m.contacts[id]
		if ex.RemovedAt > 0 {
			ex.RemovedAt = 0
			ex.Stage = domain.StageNewFound
			ex.NeedsFollowUp = false
			ex.LastTouchAt = c.LastTouchAt
		}
		// merge opportunity ids
		seen := map[string]bool{}
		for _, x := range ex.OpportunityIDs {
			seen[x] = true
		}
		for _, x := range c.OpportunityIDs {
			if x != "" && !seen[x] {
				ex.OpportunityIDs = append(ex.OpportunityIDs, x)
			}
		}
		if c.TopIntentScore > ex.TopIntentScore {
			ex.TopIntentScore = c.TopIntentScore
			ex.TopIntentBand = c.TopIntentBand
		}
		ex.UpdatedAt = domain.NowNano()
		cp := *ex
		return &cp, nil
	}
	if c.ID == "" {
		c.ID = domain.NewID()
	}
	now := domain.NowNano()
	if c.CreatedAt == 0 {
		c.CreatedAt = now
	}
	c.UpdatedAt = now
	cp := *c
	cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
	m.contacts[cp.ID] = &cp
	m.identity[key] = cp.ID
	out := cp
	out.OpportunityIDs = append([]string(nil), cp.OpportunityIDs...)
	return &out, nil
}

func (m *Memory) GetContact(_ context.Context, id string) (*domain.Contact, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	c, ok := m.contacts[id]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *c
	cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
	cp.MergedFrom = append([]string(nil), c.MergedFrom...)
	return &cp, nil
}

func (m *Memory) SaveContact(_ context.Context, c *domain.Contact) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *c
	cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
	cp.MergedFrom = append([]string(nil), c.MergedFrom...)
	m.contacts[c.ID] = &cp
	m.identity[idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)] = c.ID
	return nil
}

func (m *Memory) RemoveContact(_ context.Context, ownerUID int64, id string, at int64) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	c, ok := m.contacts[id]
	if !ok || c.RemovedAt > 0 {
		return domain.ErrNotFound
	}
	if c.OwnerUID != ownerUID {
		return domain.ErrForbidden
	}
	if at <= 0 {
		at = domain.NowNano()
	}
	c.RemovedAt = at
	c.NeedsFollowUp = false
	c.UpdatedAt = at
	for _, followUp := range m.followups {
		if followUp.OwnerUID != ownerUID || followUp.ContactID != id {
			continue
		}
		if followUp.Status == domain.FollowUpScheduled || followUp.Status == domain.FollowUpSnoozed || followUp.Status == domain.FollowUpNotified {
			followUp.Status = domain.FollowUpDone
			followUp.UpdatedAt = at
		}
	}
	return nil
}

func (m *Memory) ListContacts(_ context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	matched := make([]*domain.Contact, 0)
	for _, c := range m.contacts {
		if c.OwnerUID != ownerUID || c.RemovedAt > 0 {
			continue
		}
		q := strings.ToLower(strings.TrimSpace(f.Query))
		if q != "" && !strings.Contains(strings.ToLower(c.AuthorHandle), q) && !strings.Contains(strings.ToLower(c.DisplayName), q) {
			continue
		}
		if f.Stage != "" && c.Stage != f.Stage {
			continue
		}
		if f.FollowUp != nil && c.NeedsFollowUp != *f.FollowUp {
			continue
		}
		if f.Band != "" && c.TopIntentBand != f.Band {
			continue
		}
		cp := *c
		cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
		matched = append(matched, &cp)
	}
	sort.Slice(matched, func(i, j int) bool {
		if f.Sort == "intent_score" {
			if matched[i].TopIntentScore != matched[j].TopIntentScore {
				return matched[i].TopIntentScore > matched[j].TopIntentScore
			}
		}
		if matched[i].LastTouchAt != matched[j].LastTouchAt {
			return matched[i].LastTouchAt > matched[j].LastTouchAt
		}
		return matched[i].CreatedAt > matched[j].CreatedAt
	})
	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) CountByStage(_ context.Context, ownerUID int64) (map[string]int, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := map[string]int{}
	follow := 0
	for _, c := range m.contacts {
		if c.OwnerUID != ownerUID || c.RemovedAt > 0 {
			continue
		}
		out[c.Stage]++
		if c.NeedsFollowUp {
			follow++
		}
	}
	out["needs_follow_up"] = follow
	return out, nil
}

func (m *Memory) InsertTouch(_ context.Context, t *domain.ContactTouch) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if t.ID == "" {
		t.ID = domain.NewID()
	}
	cp := *t
	m.touches[t.ID] = &cp
	return nil
}

func (m *Memory) ListTouches(_ context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	matched := make([]*domain.ContactTouch, 0)
	for _, t := range m.touches {
		if t.OwnerUID == ownerUID && t.ContactID == contactID {
			cp := *t
			matched = append(matched, &cp)
		}
	}
	sort.Slice(matched, func(i, j int) bool { return matched[i].CreatedAt > matched[j].CreatedAt })
	total := int64(len(matched))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	start := (page - 1) * pageSize
	if start >= len(matched) {
		return nil, total, nil
	}
	end := start + pageSize
	if end > len(matched) {
		end = len(matched)
	}
	return matched[start:end], total, nil
}

func (m *Memory) SaveFollowUp(_ context.Context, f *domain.FollowUp) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *f
	m.followups[f.ID] = &cp
	return nil
}

func (m *Memory) GetFollowUp(_ context.Context, id string) (*domain.FollowUp, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	f, ok := m.followups[id]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *f
	return &cp, nil
}

func (m *Memory) ListFollowUps(_ context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	matched := make([]*domain.FollowUp, 0)
	for _, x := range m.followups {
		if x.OwnerUID != ownerUID {
			continue
		}
		if f.Status != "" && x.Status != f.Status {
			continue
		}
		cp := *x
		matched = append(matched, &cp)
	}
	sort.Slice(matched, func(i, j int) bool { return matched[i].DueAt < matched[j].DueAt })
	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) ListDueFollowUps(_ context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := make([]*domain.FollowUp, 0)
	for _, x := range m.followups {
		// notified 也要再掃：第二次通知（進而 escalated）靠的是它下一次到期。
		// snoozed 已不再寫入，保留以相容既有資料。
		switch x.Status {
		case domain.FollowUpScheduled, domain.FollowUpSnoozed, domain.FollowUpNotified:
		default:
			continue
		}
		if x.DueAt <= now {
			cp := *x
			out = append(out, &cp)
		}
	}
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}
