package repository

import (
	"context"
	"sort"
	"strings"
	"sync"
	"time"

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

type Memory struct {
	mu         sync.Mutex
	outcomes   map[string]*domain.OutcomeEvent
	bySource   map[string]string
	checkups   map[string]*domain.WeeklyCheckup
	health     map[string]*domain.AccountHealth
	workspaces map[string]*domain.Workspace
	wsState    map[int64]*domain.MemberWorkspaceState
	reviews    map[string]*domain.DraftReview
	rewards    map[string]*domain.InviteReward
	playbooks  map[string]*domain.Playbook
	utm        map[string]*domain.UtmLink
	wsMembers  map[string]*domain.WorkspaceMember
	bench      map[string]*domain.BenchmarkSample
}

func NewMemory() *Memory {
	return &Memory{
		outcomes:   map[string]*domain.OutcomeEvent{},
		bySource:   map[string]string{},
		checkups:   map[string]*domain.WeeklyCheckup{},
		health:     map[string]*domain.AccountHealth{},
		workspaces: map[string]*domain.Workspace{},
		wsState:    map[int64]*domain.MemberWorkspaceState{},
		reviews:    map[string]*domain.DraftReview{},
		rewards:    map[string]*domain.InviteReward{},
		playbooks:  map[string]*domain.Playbook{},
		utm:        map[string]*domain.UtmLink{},
		wsMembers:  map[string]*domain.WorkspaceMember{},
		bench:      map[string]*domain.BenchmarkSample{},
	}
}

func sourceKey(t, id string) string { return t + ":" + id }

func (m *Memory) UpsertOutcomeBySource(_ context.Context, e *domain.OutcomeEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	sk := sourceKey(e.SourceType, e.SourceID)
	if id, ok := m.bySource[sk]; ok {
		// 發佈 hook 可能重送；已存在就保持原 sent_at 與所有觀測／成交訊號。
		*e = *m.outcomes[id]
		return nil
	} else if e.ID == "" {
		e.ID = domain.NewID()
	}
	cp := *e
	m.outcomes[e.ID] = &cp
	m.bySource[sk] = e.ID
	return nil
}

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

func (m *Memory) ListOutcomes(_ context.Context, ownerUID int64, f domain.OutcomeListFilter) ([]*domain.OutcomeEvent, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var all []*domain.OutcomeEvent
	for _, e := range m.outcomes {
		if e.OwnerUID != ownerUID {
			continue
		}
		if f.Kind != "" && e.Kind != f.Kind {
			continue
		}
		if f.Confidence != "" && e.Confidence != f.Confidence {
			continue
		}
		if f.SourceType != "" && e.SourceType != f.SourceType {
			continue
		}
		if f.From > 0 && e.SentAt < f.From {
			continue
		}
		if f.To > 0 && e.SentAt > f.To {
			continue
		}
		cp := *e
		all = append(all, &cp)
	}
	sort.Slice(all, func(i, j int) bool { return all[i].SentAt > all[j].SentAt })
	total := int64(len(all))
	page, ps := f.Page, f.PageSize
	if page < 1 {
		page = 1
	}
	if ps < 1 {
		ps = 20
	}
	start := (page - 1) * ps
	if start >= len(all) {
		return nil, total, nil
	}
	end := start + ps
	if end > len(all) {
		end = len(all)
	}
	return all[start:end], total, nil
}

func (m *Memory) SaveOutcome(_ context.Context, e *domain.OutcomeEvent) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *e
	m.outcomes[e.ID] = &cp
	m.bySource[sourceKey(e.SourceType, e.SourceID)] = e.ID
	return nil
}

func (m *Memory) ListObservingDue(_ context.Context, now int64, limit int) ([]*domain.OutcomeEvent, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.OutcomeEvent
	for _, e := range m.outcomes {
		if e.Status != domain.StatusObserving {
			continue
		}
		cp := *e
		out = append(out, &cp)
		if limit > 0 && len(out) >= limit {
			break
		}
	}
	return out, nil
}

func (m *Memory) SaveCheckup(_ context.Context, c *domain.WeeklyCheckup) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *c
	m.checkups[c.ID] = &cp
	return nil
}

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

func (m *Memory) LatestCheckup(_ context.Context, ownerUID int64) (*domain.WeeklyCheckup, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var best *domain.WeeklyCheckup
	for _, c := range m.checkups {
		if c.OwnerUID != ownerUID {
			continue
		}
		if best == nil || c.CreatedAt > best.CreatedAt {
			cp := *c
			best = &cp
		}
	}
	if best == nil {
		return nil, domain.ErrNotFound
	}
	return best, nil
}

func (m *Memory) ListCheckups(_ context.Context, ownerUID int64, page, pageSize int) ([]*domain.WeeklyCheckup, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var all []*domain.WeeklyCheckup
	for _, c := range m.checkups {
		if c.OwnerUID == ownerUID {
			cp := *c
			all = append(all, &cp)
		}
	}
	sort.Slice(all, func(i, j int) bool { return all[i].CreatedAt > all[j].CreatedAt })
	total := int64(len(all))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	start := (page - 1) * pageSize
	if start >= len(all) {
		return nil, total, nil
	}
	end := start + pageSize
	if end > len(all) {
		end = len(all)
	}
	return all[start:end], total, nil
}

func (m *Memory) CountCheckupGenerations(_ context.Context, ownerUID int64, sinceNano int64) (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var n int64
	for _, c := range m.checkups {
		if c.OwnerUID == ownerUID && c.CreatedAt >= sinceNano {
			n++
		}
	}
	return n, nil
}

func (m *Memory) SaveHealth(_ context.Context, h *domain.AccountHealth) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *h
	m.health[h.ThreadsAccountID] = &cp
	return nil
}

func (m *Memory) GetHealth(_ context.Context, accountID string) (*domain.AccountHealth, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	h, ok := m.health[accountID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *h
	return &cp, nil
}

func (m *Memory) ListHealth(_ context.Context, ownerUID int64) ([]*domain.AccountHealth, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.AccountHealth
	for _, h := range m.health {
		if h.OwnerUID == ownerUID {
			cp := *h
			out = append(out, &cp)
		}
	}
	return out, nil
}

func (m *Memory) SaveWorkspace(_ context.Context, w *domain.Workspace) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *w
	m.workspaces[w.ID] = &cp
	return nil
}

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

func (m *Memory) ListWorkspaces(_ context.Context, ownerUID int64, includeArchived bool) ([]*domain.Workspace, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.Workspace
	for _, w := range m.workspaces {
		if w.OwnerUID != ownerUID {
			continue
		}
		if w.Archived && !includeArchived {
			continue
		}
		cp := *w
		out = append(out, &cp)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt < out[j].CreatedAt })
	return out, nil
}

func (m *Memory) CountActiveWorkspaces(_ context.Context, ownerUID int64) (int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var n int64
	for _, w := range m.workspaces {
		if w.OwnerUID == ownerUID && !w.Archived {
			n++
		}
	}
	return n, nil
}

func (m *Memory) GetMemberWSState(_ context.Context, ownerUID int64) (*domain.MemberWorkspaceState, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	s, ok := m.wsState[ownerUID]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *s
	return &cp, nil
}

func (m *Memory) SaveMemberWSState(_ context.Context, s *domain.MemberWorkspaceState) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *s
	m.wsState[s.OwnerUID] = &cp
	return nil
}

func (m *Memory) SaveReview(_ context.Context, r *domain.DraftReview) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *r
	m.reviews[r.ID] = &cp
	return nil
}

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

func (m *Memory) FindReviewByRef(_ context.Context, ownerUID int64, refType, refID string) (*domain.DraftReview, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var best *domain.DraftReview
	for _, r := range m.reviews {
		if r.OwnerUID == ownerUID && r.RefType == refType && r.RefID == refID {
			if best == nil || r.UpdatedAt > best.UpdatedAt {
				cp := *r
				best = &cp
			}
		}
	}
	if best == nil {
		return nil, domain.ErrNotFound
	}
	return best, nil
}

func (m *Memory) ListReviews(_ context.Context, ownerUID int64, status string, page, pageSize int) ([]*domain.DraftReview, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var all []*domain.DraftReview
	for _, r := range m.reviews {
		if r.OwnerUID != ownerUID {
			continue
		}
		if status != "" && r.Status != status {
			continue
		}
		cp := *r
		all = append(all, &cp)
	}
	total := int64(len(all))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	start := (page - 1) * pageSize
	if start >= len(all) {
		return nil, total, nil
	}
	end := start + pageSize
	if end > len(all) {
		end = len(all)
	}
	return all[start:end], total, nil
}

func (m *Memory) SaveInviteReward(_ context.Context, r *domain.InviteReward) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := *r
	m.rewards[r.ID] = &cp
	return nil
}

func (m *Memory) ListInviteRewards(_ context.Context, inviterUID int64, limit int) ([]*domain.InviteReward, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var all []*domain.InviteReward
	for _, r := range m.rewards {
		if r.InviterUID == inviterUID {
			cp := *r
			all = append(all, &cp)
		}
	}
	sort.Slice(all, func(i, j int) bool { return all[i].CreatedAt > all[j].CreatedAt })
	if limit > 0 && len(all) > limit {
		all = all[:limit]
	}
	return all, nil
}

func (m *Memory) SumInviteRewardPointsMonth(_ context.Context, inviterUID int64, monthStartNano int64) (int, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	sum := 0
	for _, r := range m.rewards {
		if r.InviterUID == inviterUID && r.Status == domain.RewardCredited && r.CreatedAt >= monthStartNano {
			sum += r.Points
		}
	}
	return sum, nil
}

func (m *Memory) SumInviteRewardPointsTotal(_ context.Context, inviterUID int64) (int, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	sum := 0
	for _, r := range m.rewards {
		if r.InviterUID == inviterUID && r.Status == domain.RewardCredited {
			sum += r.Points
		}
	}
	return sum, nil
}

func (m *Memory) HasInviteRewardForInvitee(_ context.Context, inviteeUID int64) (bool, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	for _, r := range m.rewards {
		if r.InviteeUID == inviteeUID && (r.Status == domain.RewardCredited || r.Status == domain.RewardCapped) {
			return true, nil
		}
	}
	return false, nil
}

// Ensure compile-time interface
var _ domain.Repository = (*Memory)(nil)

// MonthStartNano helper for tests
func MonthStartNano(t time.Time) int64 {
	u := t.UTC()
	return time.Date(u.Year(), u.Month(), 1, 0, 0, 0, 0, time.UTC).UnixNano()
}

// --- P2 ---

func (m *Memory) SavePlaybook(_ context.Context, p *domain.Playbook) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.playbooks == nil {
		m.playbooks = map[string]*domain.Playbook{}
	}
	cp := *p
	m.playbooks[p.ID] = &cp
	return nil
}

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

func (m *Memory) ListPlaybooks(_ context.Context, kind, niche string, ownerOnlyUID int64, page, pageSize int) ([]*domain.Playbook, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var all []*domain.Playbook
	for _, p := range m.playbooks {
		if kind != "" && p.Kind != kind {
			continue
		}
		if niche != "" && !stringsContainsFold(p.Niche, niche) {
			continue
		}
		if ownerOnlyUID > 0 && p.OwnerUID != ownerOnlyUID {
			continue
		}
		cp := *p
		all = append(all, &cp)
	}
	sort.Slice(all, func(i, j int) bool { return all[i].CreatedAt > all[j].CreatedAt })
	total := int64(len(all))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	start := (page - 1) * pageSize
	if start >= len(all) {
		return nil, total, nil
	}
	end := start + pageSize
	if end > len(all) {
		end = len(all)
	}
	return all[start:end], total, nil
}

func stringsContainsFold(s, sub string) bool {
	return strings.Contains(strings.ToLower(s), strings.ToLower(sub))
}

func (m *Memory) DeletePlaybook(_ context.Context, id string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if _, ok := m.playbooks[id]; !ok {
		return domain.ErrNotFound
	}
	delete(m.playbooks, id)
	return nil
}

func (m *Memory) IncPlaybookImport(_ context.Context, id string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	p, ok := m.playbooks[id]
	if !ok {
		return domain.ErrNotFound
	}
	p.ImportCount++
	return nil
}

func (m *Memory) SaveUtmLink(_ context.Context, u *domain.UtmLink) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.utm == nil {
		m.utm = map[string]*domain.UtmLink{}
	}
	cp := *u
	m.utm[u.Code] = &cp
	return nil
}

func (m *Memory) GetUtmByCode(_ context.Context, code string) (*domain.UtmLink, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	u, ok := m.utm[code]
	if !ok {
		return nil, domain.ErrNotFound
	}
	cp := *u
	return &cp, nil
}

func (m *Memory) ListUtmLinks(_ context.Context, ownerUID int64) ([]*domain.UtmLink, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.UtmLink
	for _, u := range m.utm {
		if u.OwnerUID == ownerUID {
			cp := *u
			out = append(out, &cp)
		}
	}
	return out, nil
}

func (m *Memory) IncUtmClick(_ context.Context, code string) (*domain.UtmLink, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	u, ok := m.utm[code]
	if !ok {
		return nil, domain.ErrNotFound
	}
	u.Clicks++
	cp := *u
	return &cp, nil
}

func (m *Memory) SaveWorkspaceMember(_ context.Context, mem *domain.WorkspaceMember) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.wsMembers == nil {
		m.wsMembers = map[string]*domain.WorkspaceMember{}
	}
	cp := *mem
	m.wsMembers[mem.ID] = &cp
	return nil
}

func (m *Memory) ListWorkspaceMembers(_ context.Context, workspaceID string) ([]*domain.WorkspaceMember, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.WorkspaceMember
	for _, mem := range m.wsMembers {
		if mem.WorkspaceID == workspaceID {
			cp := *mem
			out = append(out, &cp)
		}
	}
	return out, nil
}

func (m *Memory) RemoveWorkspaceMember(_ context.Context, workspaceID string, uid int64) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	for id, mem := range m.wsMembers {
		if mem.WorkspaceID == workspaceID && mem.UID == uid {
			delete(m.wsMembers, id)
			return nil
		}
	}
	return domain.ErrNotFound
}

func (m *Memory) IsWorkspaceMember(_ context.Context, workspaceID string, uid int64) (bool, string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if w, ok := m.workspaces[workspaceID]; ok && w.OwnerUID == uid {
		return true, domain.WSRoleOwner, nil
	}
	for _, mem := range m.wsMembers {
		if mem.WorkspaceID == workspaceID && mem.UID == uid {
			return true, mem.Role, nil
		}
	}
	return false, "", nil
}

func (m *Memory) UpsertBenchmarkSample(_ context.Context, s *domain.BenchmarkSample) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.bench == nil {
		m.bench = map[string]*domain.BenchmarkSample{}
	}
	key := s.Niche + ":" + s.OwnerHash
	s.ID = key
	cp := *s
	m.bench[key] = &cp
	return nil
}

func (m *Memory) ListBenchmarkSamples(_ context.Context, niche string, limit int) ([]*domain.BenchmarkSample, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	var out []*domain.BenchmarkSample
	for _, s := range m.bench {
		if niche == "" || s.Niche == niche {
			cp := *s
			out = append(out, &cp)
		}
	}
	if limit > 0 && len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}
