package repository

import (
	"context"
	"regexp"
	"strings"

	libmongo "apps/backend/internal/lib/mongo"
	"apps/backend/internal/module/crm/domain"

	"github.com/zeromicro/go-zero/core/stores/mon"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type MonStore struct {
	contacts  *mon.Model
	touches   *mon.Model
	followups *mon.Model
}

func NewMonStore(uri, database string) *MonStore {
	uri = libmongo.MustMongoURI(uri)
	return &MonStore{
		contacts:  mon.MustNewModel(uri, database, "crm_contacts"),
		touches:   mon.MustNewModel(uri, database, "crm_touches"),
		followups: mon.MustNewModel(uri, database, "crm_followups"),
	}
}

func (s *MonStore) UpsertContactByIdentity(ctx context.Context, c *domain.Contact) (*domain.Contact, error) {
	if err := c.Normalize(); err != nil {
		return nil, err
	}
	filter := bson.M{
		"owner_uid":       c.OwnerUID,
		"source_platform": c.SourcePlatform,
		"author_handle":   c.AuthorHandle,
	}
	var existing domain.Contact
	err := s.contacts.FindOne(ctx, &existing, filter)
	if err == nil {
		// merge
		if existing.RemovedAt > 0 {
			existing.RemovedAt = 0
			existing.Stage = domain.StageNewFound
			existing.NeedsFollowUp = false
			existing.LastTouchAt = c.LastTouchAt
		}
		seen := map[string]bool{}
		for _, id := range existing.OpportunityIDs {
			seen[id] = true
		}
		for _, id := range c.OpportunityIDs {
			if id != "" && !seen[id] {
				existing.OpportunityIDs = append(existing.OpportunityIDs, id)
			}
		}
		if c.TopIntentScore > existing.TopIntentScore {
			existing.TopIntentScore = c.TopIntentScore
			existing.TopIntentBand = c.TopIntentBand
		}
		existing.UpdatedAt = domain.NowNano()
		_, err = s.contacts.ReplaceOne(ctx, bson.M{"_id": existing.ID}, &existing)
		return &existing, err
	}
	if err != mon.ErrNotFound {
		return nil, err
	}
	if c.ID == "" {
		c.ID = domain.NewID()
	}
	now := domain.NowNano()
	if c.CreatedAt == 0 {
		c.CreatedAt = now
	}
	c.UpdatedAt = now
	_, err = s.contacts.InsertOne(ctx, c)
	if err != nil && mongo.IsDuplicateKeyError(err) {
		return s.UpsertContactByIdentity(ctx, c)
	}
	return c, err
}

func (s *MonStore) GetContact(ctx context.Context, id string) (*domain.Contact, error) {
	var c domain.Contact
	err := s.contacts.FindOne(ctx, &c, bson.M{"_id": id})
	if err == mon.ErrNotFound {
		return nil, domain.ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	return &c, nil
}

func (s *MonStore) SaveContact(ctx context.Context, c *domain.Contact) error {
	_, err := s.contacts.ReplaceOne(ctx, bson.M{"_id": c.ID}, c, options.Replace().SetUpsert(true))
	return err
}

func (s *MonStore) RemoveContact(ctx context.Context, ownerUID int64, id string, at int64) error {
	if at <= 0 {
		at = domain.NowNano()
	}
	_, err := s.followups.UpdateMany(ctx, bson.M{
		"owner_uid":  ownerUID,
		"contact_id": id,
		"status": bson.M{"$in": bson.A{
			domain.FollowUpScheduled,
			domain.FollowUpSnoozed,
			domain.FollowUpNotified,
		}},
	}, bson.M{"$set": bson.M{"status": domain.FollowUpDone, "updated_at": at}})
	if err != nil {
		return err
	}
	res, err := s.contacts.UpdateOne(ctx, bson.M{
		"_id":       id,
		"owner_uid": ownerUID,
		"$or": bson.A{
			bson.M{"removed_at": bson.M{"$exists": false}},
			bson.M{"removed_at": 0},
		},
	}, bson.M{"$set": bson.M{
		"removed_at":      at,
		"needs_follow_up": false,
		"updated_at":      at,
	}})
	if err != nil {
		return err
	}
	if res.MatchedCount == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func activeContactQuery(ownerUID int64) bson.M {
	return bson.M{
		"owner_uid": ownerUID,
		"$and": bson.A{bson.M{"$or": bson.A{
			bson.M{"removed_at": bson.M{"$exists": false}},
			bson.M{"removed_at": 0},
		}}},
	}
}

func (s *MonStore) ListContacts(ctx context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
	q := activeContactQuery(ownerUID)
	if search := strings.TrimSpace(f.Query); search != "" {
		pattern := regexp.QuoteMeta(search)
		q["$and"] = append(q["$and"].(bson.A), bson.M{"$or": bson.A{
			bson.M{"author_handle": bson.M{"$regex": pattern, "$options": "i"}},
			bson.M{"display_name": bson.M{"$regex": pattern, "$options": "i"}},
		}})
	}
	if f.Stage != "" {
		q["stage"] = f.Stage
	}
	if f.FollowUp != nil {
		q["needs_follow_up"] = *f.FollowUp
	}
	if f.Band != "" {
		q["top_intent_band"] = f.Band
	}
	total, err := s.contacts.CountDocuments(ctx, q)
	if err != nil {
		return nil, 0, err
	}
	page, ps := f.Page, f.PageSize
	if page < 1 {
		page = 1
	}
	if ps < 1 {
		ps = 20
	}
	sortKey := "last_touch_at"
	if f.Sort == "intent_score" {
		sortKey = "top_intent_score"
	}
	var list []*domain.Contact
	err = s.contacts.Find(ctx, &list, q, options.Find().
		SetSort(bson.D{{Key: sortKey, Value: -1}}).
		SetSkip(int64((page-1)*ps)).
		SetLimit(int64(ps)))
	return list, total, err
}

func (s *MonStore) CountByStage(ctx context.Context, ownerUID int64) (map[string]int, error) {
	// Counts describe the complete active pipeline, not just the current page.
	var list []*domain.Contact
	err := s.contacts.Find(ctx, &list, activeContactQuery(ownerUID), options.Find().SetProjection(bson.M{"stage": 1, "needs_follow_up": 1}))
	if err != nil {
		return nil, err
	}
	out := map[string]int{}
	follow := 0
	for _, c := range list {
		out[c.Stage]++
		if c.NeedsFollowUp {
			follow++
		}
	}
	out["needs_follow_up"] = follow
	return out, nil
}

func (s *MonStore) InsertTouch(ctx context.Context, t *domain.ContactTouch) error {
	if t.ID == "" {
		t.ID = domain.NewID()
	}
	_, err := s.touches.InsertOne(ctx, t)
	return err
}

func (s *MonStore) ListTouches(ctx context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
	q := bson.M{"owner_uid": ownerUID, "contact_id": contactID}
	total, err := s.touches.CountDocuments(ctx, q)
	if err != nil {
		return nil, 0, err
	}
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 20
	}
	var list []*domain.ContactTouch
	err = s.touches.Find(ctx, &list, q, options.Find().
		SetSort(bson.D{{Key: "created_at", Value: -1}}).
		SetSkip(int64((page-1)*pageSize)).
		SetLimit(int64(pageSize)))
	return list, total, err
}

func (s *MonStore) SaveFollowUp(ctx context.Context, f *domain.FollowUp) error {
	_, err := s.followups.ReplaceOne(ctx, bson.M{"_id": f.ID}, f, options.Replace().SetUpsert(true))
	return err
}

func (s *MonStore) GetFollowUp(ctx context.Context, id string) (*domain.FollowUp, error) {
	var f domain.FollowUp
	err := s.followups.FindOne(ctx, &f, bson.M{"_id": id})
	if err == mon.ErrNotFound {
		return nil, domain.ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	return &f, nil
}

func (s *MonStore) ListFollowUps(ctx context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
	q := bson.M{"owner_uid": ownerUID}
	if f.Status != "" {
		q["status"] = f.Status
	}
	total, err := s.followups.CountDocuments(ctx, q)
	if err != nil {
		return nil, 0, err
	}
	page, ps := f.Page, f.PageSize
	if page < 1 {
		page = 1
	}
	if ps < 1 {
		ps = 20
	}
	var list []*domain.FollowUp
	err = s.followups.Find(ctx, &list, q, options.Find().
		SetSort(bson.D{{Key: "due_at", Value: 1}}).
		SetSkip(int64((page-1)*ps)).
		SetLimit(int64(ps)))
	return list, total, err
}

func (s *MonStore) ListDueFollowUps(ctx context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
	if limit <= 0 {
		limit = 50
	}
	var list []*domain.FollowUp
	// notified 也要再掃：第二次通知（進而 escalated）靠的是它下一次到期。
	// snoozed 已不再寫入，保留以相容既有資料。
	err := s.followups.Find(ctx, &list, bson.M{
		"status": bson.M{"$in": []string{
			domain.FollowUpScheduled, domain.FollowUpSnoozed, domain.FollowUpNotified,
		}},
		"due_at": bson.M{"$lte": now},
	}, options.Find().SetLimit(int64(limit)))
	return list, err
}
