package repository

import (
	"context"
	"fmt"

	libmongo "apps/backend/internal/lib/mongo"
	"apps/backend/internal/module/usage/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 {
	events    *mon.Model
	counters  *mon.Model
	prefs     *mon.Model
	purchases *mon.Model
}

func NewMonStore(uri, database string) *MonStore {
	uri = libmongo.MustMongoURI(uri)
	return &MonStore{
		events:    mon.MustNewModel(uri, database, "usage_events"),
		counters:  mon.MustNewModel(uri, database, "usage_monthly_counters"),
		prefs:     mon.MustNewModel(uri, database, "usage_prefs"),
		purchases: mon.MustNewModel(uri, database, "usage_purchases"),
	}
}

func monthlyCounterID(uid int64, monthKey string) string {
	return fmt.Sprintf("%d:%s", uid, monthKey)
}

func meterCounterField(meter string) (string, error) {
	switch meter {
	case domain.MeterAICopy, domain.MeterAIResearch, domain.MeterWebSearch, domain.MeterAIImage:
		return "by_meter." + meter, nil
	default:
		return "", fmt.Errorf("invalid usage meter %q", meter)
	}
}

func (s *MonStore) ensureMonthlyCounter(ctx context.Context, uid int64, monthKey string) error {
	id := monthlyCounterID(uid, monthKey)
	var existing domain.MonthlyCounter
	if err := s.counters.FindOne(ctx, &existing, bson.M{"_id": id}); err == nil {
		return nil
	} else if err != mon.ErrNotFound {
		return err
	}
	events, err := s.ListEvents(ctx, uid, monthKey, domain.KeyModePlatform, 0)
	if err != nil {
		return err
	}
	counter := &domain.MonthlyCounter{
		ID: id, UID: uid, MonthKey: monthKey, ByMeter: map[string]int{}, UpdatedAt: domain.NowNano(),
	}
	for _, event := range events {
		credits := event.Credits
		if minimum := domain.MeterCost(event.Meter); credits < minimum {
			credits = minimum
		}
		counter.TotalCredits += credits
		counter.ByMeter[event.Meter] += credits
	}
	if _, err := s.counters.InsertOne(ctx, counter); err != nil && !mongo.IsDuplicateKeyError(err) {
		return err
	}
	return nil
}

func (s *MonStore) ReservePlatform(ctx context.Context, uid int64, monthKey, meter string, cost, totalLimit, meterLimit int) error {
	field, err := meterCounterField(meter)
	if err != nil {
		return err
	}
	if totalLimit >= 0 && cost > totalLimit {
		return domain.ErrQuotaExceeded
	}
	if meterLimit >= 0 && cost > meterLimit {
		return domain.ErrMeterCapExceeded
	}
	if err := s.ensureMonthlyCounter(ctx, uid, monthKey); err != nil {
		return err
	}
	filter := bson.M{"_id": monthlyCounterID(uid, monthKey)}
	if totalLimit >= 0 {
		filter["total_credits"] = bson.M{"$lte": totalLimit - cost}
	}
	if meterLimit >= 0 {
		filter["$or"] = bson.A{
			bson.M{field: bson.M{"$exists": false}},
			bson.M{field: bson.M{"$lte": meterLimit - cost}},
		}
	}
	res, err := s.counters.UpdateOne(ctx, filter, bson.M{
		"$inc": bson.M{"total_credits": cost, field: cost},
		"$set": bson.M{"updated_at": domain.NowNano()},
	})
	if err != nil {
		return err
	}
	if res.MatchedCount != 0 {
		return nil
	}
	counter, err := s.GetMonthlyCounter(ctx, uid, monthKey)
	if err != nil {
		return err
	}
	if totalLimit >= 0 && counter.TotalCredits+cost > totalLimit {
		return domain.ErrQuotaExceeded
	}
	if meterLimit >= 0 && counter.ByMeter[meter]+cost > meterLimit {
		return domain.ErrMeterCapExceeded
	}
	return fmt.Errorf("platform quota reservation did not match counter")
}

func (s *MonStore) ReleasePlatform(ctx context.Context, uid int64, monthKey, meter string, cost int) error {
	field, err := meterCounterField(meter)
	if err != nil {
		return err
	}
	if cost <= 0 {
		return nil
	}
	// Both guards keep the refund from pushing either counter negative; a non-match means
	// there is nothing to give back, which is not an error.
	filter := bson.M{
		"_id":           monthlyCounterID(uid, monthKey),
		"total_credits": bson.M{"$gte": cost},
		field:           bson.M{"$gte": cost},
	}
	_, err = s.counters.UpdateOne(ctx, filter, bson.M{
		"$inc": bson.M{"total_credits": -cost, field: -cost},
		"$set": bson.M{"updated_at": domain.NowNano()},
	})
	return err
}

func (s *MonStore) GetMonthlyCounter(ctx context.Context, uid int64, monthKey string) (*domain.MonthlyCounter, error) {
	var counter domain.MonthlyCounter
	err := s.counters.FindOne(ctx, &counter, bson.M{"_id": monthlyCounterID(uid, monthKey)})
	if err == mon.ErrNotFound {
		return nil, domain.ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	if counter.ByMeter == nil {
		counter.ByMeter = map[string]int{}
	}
	return &counter, nil
}

func (s *MonStore) InsertEvent(ctx context.Context, e *domain.Event) error {
	_, err := s.events.InsertOne(ctx, e)
	return err
}

func (s *MonStore) ListEvents(ctx context.Context, uid int64, monthKey, keyMode string, limit int) ([]*domain.Event, error) {
	filter := bson.M{"uid": uid}
	if monthKey != "" {
		filter["month_key"] = monthKey
	}
	if keyMode != "" && keyMode != "all" {
		filter["key_mode"] = keyMode
	}
	opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
	if limit > 0 {
		opts.SetLimit(int64(limit))
	}
	var list []*domain.Event
	err := s.events.Find(ctx, &list, filter, opts)
	return list, err
}

func (s *MonStore) ListEventsAll(ctx context.Context, monthKey string, limit int) ([]*domain.Event, error) {
	filter := bson.M{}
	if monthKey != "" {
		filter["month_key"] = monthKey
	}
	opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
	if limit > 0 {
		opts.SetLimit(int64(limit))
	}
	var list []*domain.Event
	err := s.events.Find(ctx, &list, filter, opts)
	return list, err
}

func (s *MonStore) ListEventsRange(ctx context.Context, fromNano, toExclusiveNano int64) ([]*domain.Event, error) {
	var list []*domain.Event
	err := s.events.Find(ctx, &list, bson.M{
		"created_at": bson.M{"$gte": fromNano, "$lt": toExclusiveNano},
	}, options.Find().SetSort(bson.D{{Key: "created_at", Value: 1}}))
	return list, err
}

func (s *MonStore) GetPrefs(ctx context.Context, uid int64) (*domain.MemberPrefs, error) {
	var p domain.MemberPrefs
	err := s.prefs.FindOne(ctx, &p, bson.M{"uid": uid})
	if err != nil {
		if err == mon.ErrNotFound {
			return &domain.MemberPrefs{UID: uid, PlanID: domain.PlanFree}, nil
		}
		return nil, err
	}
	return &p, nil
}

func (s *MonStore) SavePrefs(ctx context.Context, p *domain.MemberPrefs) error {
	_, err := s.prefs.ReplaceOne(ctx, bson.M{"uid": p.UID}, p, options.Replace().SetUpsert(true))
	return err
}

func (s *MonStore) InsertPurchase(ctx context.Context, p *domain.Purchase) error {
	_, err := s.purchases.InsertOne(ctx, p)
	return err
}

func (s *MonStore) ListPurchases(ctx context.Context, uid int64, limit int) ([]*domain.Purchase, error) {
	opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
	if limit > 0 {
		opts.SetLimit(int64(limit))
	}
	var list []*domain.Purchase
	err := s.purchases.Find(ctx, &list, bson.M{"uid": uid}, opts)
	return list, err
}

var _ domain.Repository = (*MonStore)(nil)
