package store

import (
	"context"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"

	"bountyboard/internal/domain"
)

// Metrics aggregations (§6.2) — always over the immutable bountyAwards
// ledger plus task timelines, never recomputed from mutable task fields.

type WeeklyPoint struct {
	Week   time.Time `bson:"_id" json:"week"`
	Amount float64   `bson:"amount" json:"amount"`
	Tasks  int64     `bson:"tasks" json:"tasks"`
}

type GroupTotal struct {
	Key    string  `bson:"_id" json:"key"`
	Amount float64 `bson:"amount" json:"amount"`
	Tasks  int64   `bson:"tasks" json:"tasks"`
}

func awardMatch(developerID, consultantID, customerID string, from, to time.Time) bson.M {
	m := bson.M{"awardedAt": bson.M{"$gte": from, "$lte": to}}
	if developerID != "" {
		m["developerId"] = developerID
	}
	if consultantID != "" {
		m["consultantId"] = consultantID
	}
	if customerID != "" {
		m["customerId"] = customerID
	}
	return m
}

// AwardTotals returns sum + count for the filter.
func (s *Store) AwardTotals(ctx context.Context, developerID, consultantID, customerID string, from, to time.Time) (float64, int64, error) {
	cur, err := s.DB.Collection("bountyAwards").Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: awardMatch(developerID, consultantID, customerID, from, to)}},
		{{Key: "$group", Value: bson.M{"_id": nil,
			"amount": bson.M{"$sum": "$amount"}, "tasks": bson.M{"$sum": 1}}}},
	})
	if err != nil {
		return 0, 0, fmt.Errorf("award totals: %w", err)
	}
	var rows []struct {
		Amount float64 `bson:"amount"`
		Tasks  int64   `bson:"tasks"`
	}
	if err := cur.All(ctx, &rows); err != nil {
		return 0, 0, fmt.Errorf("decode totals: %w", err)
	}
	if len(rows) == 0 {
		return 0, 0, nil
	}
	return rows[0].Amount, rows[0].Tasks, nil
}

// WeeklyAwards buckets earnings into ISO weeks (§6.2 earnings-over-time).
func (s *Store) WeeklyAwards(ctx context.Context, developerID, consultantID, customerID string, from, to time.Time) ([]WeeklyPoint, error) {
	cur, err := s.DB.Collection("bountyAwards").Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: awardMatch(developerID, consultantID, customerID, from, to)}},
		{{Key: "$group", Value: bson.M{
			"_id":    bson.M{"$dateTrunc": bson.M{"date": "$awardedAt", "unit": "week"}},
			"amount": bson.M{"$sum": "$amount"},
			"tasks":  bson.M{"$sum": 1},
		}}},
		{{Key: "$sort", Value: bson.M{"_id": 1}}},
	})
	if err != nil {
		return nil, fmt.Errorf("weekly awards: %w", err)
	}
	out := []WeeklyPoint{}
	if err := cur.All(ctx, &out); err != nil {
		return nil, fmt.Errorf("decode weekly: %w", err)
	}
	return out, nil
}

// AwardsGroupedBy aggregates totals per developerId or customerId.
func (s *Store) AwardsGroupedBy(ctx context.Context, field, developerID, consultantID, customerID string, from, to time.Time) ([]GroupTotal, error) {
	cur, err := s.DB.Collection("bountyAwards").Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: awardMatch(developerID, consultantID, customerID, from, to)}},
		{{Key: "$group", Value: bson.M{"_id": "$" + field,
			"amount": bson.M{"$sum": "$amount"}, "tasks": bson.M{"$sum": 1}}}},
		{{Key: "$sort", Value: bson.M{"amount": -1}}},
	})
	if err != nil {
		return nil, fmt.Errorf("grouped awards: %w", err)
	}
	out := []GroupTotal{}
	if err := cur.All(ctx, &out); err != nil {
		return nil, fmt.Errorf("decode grouped: %w", err)
	}
	return out, nil
}

// ListAwards returns the raw ledger rows for CSV export.
func (s *Store) ListAwards(ctx context.Context, developerID, consultantID, customerID string, from, to time.Time) ([]BountyAward, error) {
	cur, err := s.DB.Collection("bountyAwards").Find(ctx,
		awardMatch(developerID, consultantID, customerID, from, to))
	if err != nil {
		return nil, fmt.Errorf("list awards: %w", err)
	}
	out := []BountyAward{}
	if err := cur.All(ctx, &out); err != nil {
		return nil, fmt.Errorf("decode awards: %w", err)
	}
	return out, nil
}

// ReviewOutcomes counts approved vs changes_requested submissions for a
// developer (approval rate, §6.2).
func (s *Store) ReviewOutcomes(ctx context.Context, developerID string, from, to time.Time) (approved, changes int64, err error) {
	cur, err := s.tasks().Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: bson.M{"assignee.userId": developerID}}},
		{{Key: "$unwind", Value: "$timeline"}},
		{{Key: "$match", Value: bson.M{
			"timeline.event": bson.M{"$in": []string{"approved", "changes_requested"}},
			"timeline.at":    bson.M{"$gte": from, "$lte": to},
		}}},
		{{Key: "$group", Value: bson.M{"_id": "$timeline.event", "n": bson.M{"$sum": 1}}}},
	})
	if err != nil {
		return 0, 0, fmt.Errorf("review outcomes: %w", err)
	}
	var rows []struct {
		ID string `bson:"_id"`
		N  int64  `bson:"n"`
	}
	if err := cur.All(ctx, &rows); err != nil {
		return 0, 0, fmt.Errorf("decode outcomes: %w", err)
	}
	for _, r := range rows {
		if r.ID == "approved" {
			approved = r.N
		} else {
			changes = r.N
		}
	}
	return approved, changes, nil
}

// TimeLogged sums the developer's logged minutes.
func (s *Store) TimeLogged(ctx context.Context, developerID string, from, to time.Time) (int64, error) {
	cur, err := s.tasks().Aggregate(ctx, mongo.Pipeline{
		{{Key: "$unwind", Value: "$timeLog"}},
		{{Key: "$match", Value: bson.M{
			"timeLog.developerId": developerID,
			"timeLog.at":          bson.M{"$gte": from, "$lte": to},
		}}},
		{{Key: "$group", Value: bson.M{"_id": nil, "minutes": bson.M{"$sum": "$timeLog.minutes"}}}},
	})
	if err != nil {
		return 0, fmt.Errorf("time logged: %w", err)
	}
	var rows []struct {
		Minutes int64 `bson:"minutes"`
	}
	if err := cur.All(ctx, &rows); err != nil {
		return 0, fmt.Errorf("decode time: %w", err)
	}
	if len(rows) == 0 {
		return 0, nil
	}
	return rows[0].Minutes, nil
}

// AvgAssignToApproveHours computes the mean lead time from assignment to
// approval over approved tasks (timeline-derived).
func (s *Store) AvgAssignToApproveHours(ctx context.Context, developerID string, from, to time.Time) (float64, error) {
	cur, err := s.tasks().Find(ctx, bson.M{
		"assignee.userId": developerID,
		"status":          domain.StatusApproved,
		"updatedAt":       bson.M{"$gte": from, "$lte": to},
	})
	if err != nil {
		return 0, fmt.Errorf("approved tasks: %w", err)
	}
	var tasks []domain.Task
	if err := cur.All(ctx, &tasks); err != nil {
		return 0, fmt.Errorf("decode approved: %w", err)
	}
	var total time.Duration
	var n int
	for _, t := range tasks {
		var assignedAt, approvedAt time.Time
		for _, e := range t.Timeline {
			if (e.Event == "claim_approved" || e.Event == "assigned_to_ai") && assignedAt.IsZero() {
				assignedAt = e.At
			}
			if e.Event == "approved" {
				approvedAt = e.At
			}
		}
		if !assignedAt.IsZero() && approvedAt.After(assignedAt) {
			total += approvedAt.Sub(assignedAt)
			n++
		}
	}
	if n == 0 {
		return 0, nil
	}
	return total.Hours() / float64(n), nil
}

// AtomizationLeadTimeHours: mean imported→published lead time per §6.2,
// measured on published tasks against their root's creation.
func (s *Store) AtomizationLeadTimeHours(ctx context.Context, customerIDs []string, from, to time.Time) (float64, error) {
	if len(customerIDs) == 0 {
		return 0, nil
	}
	cur, err := s.tasks().Find(ctx, bson.M{
		"customerId":  bson.M{"$in": customerIDs},
		"publishedAt": bson.M{"$gte": from, "$lte": to},
	})
	if err != nil {
		return 0, fmt.Errorf("published tasks: %w", err)
	}
	var tasks []domain.Task
	if err := cur.All(ctx, &tasks); err != nil {
		return 0, fmt.Errorf("decode published: %w", err)
	}
	rootCreated := map[string]time.Time{}
	var total time.Duration
	var n int
	for _, t := range tasks {
		created, ok := rootCreated[t.RootID]
		if !ok {
			root, err := s.TaskByID(ctx, t.RootID)
			if err != nil {
				continue
			}
			created = root.CreatedAt
			rootCreated[t.RootID] = created
		}
		if t.PublishedAt.After(created) {
			total += t.PublishedAt.Sub(created)
			n++
		}
	}
	if n == 0 {
		return 0, nil
	}
	return total.Hours() / float64(n), nil
}

// OpenBoardDepth counts currently published tasks per customer (§6.2).
func (s *Store) OpenBoardDepth(ctx context.Context, customerIDs []string) (map[string]int64, error) {
	out := map[string]int64{}
	if len(customerIDs) == 0 {
		return out, nil
	}
	cur, err := s.tasks().Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: bson.M{
			"customerId": bson.M{"$in": customerIDs},
			"status":     bson.M{"$in": []domain.TaskStatus{domain.StatusPublished, domain.StatusClaimRequested}},
		}}},
		{{Key: "$group", Value: bson.M{"_id": "$customerId", "n": bson.M{"$sum": 1}}}},
	})
	if err != nil {
		return nil, fmt.Errorf("board depth: %w", err)
	}
	var rows []struct {
		ID string `bson:"_id"`
		N  int64  `bson:"n"`
	}
	if err := cur.All(ctx, &rows); err != nil {
		return nil, fmt.Errorf("decode depth: %w", err)
	}
	for _, r := range rows {
		out[r.ID] = r.N
	}
	return out, nil
}

// Leaderboard: top developers by total bounty (opt-outs filtered by caller).
func (s *Store) Leaderboard(ctx context.Context, from, to time.Time, limit int) ([]GroupTotal, error) {
	if limit <= 0 || limit > 50 {
		limit = 10
	}
	cur, err := s.DB.Collection("bountyAwards").Aggregate(ctx, mongo.Pipeline{
		{{Key: "$match", Value: bson.M{"awardedAt": bson.M{"$gte": from, "$lte": to}}}},
		{{Key: "$group", Value: bson.M{"_id": "$developerId",
			"amount": bson.M{"$sum": "$amount"}, "tasks": bson.M{"$sum": 1}}}},
		{{Key: "$sort", Value: bson.M{"amount": -1}}},
		{{Key: "$limit", Value: limit * 2}}, // headroom for opt-out filtering
	})
	if err != nil {
		return nil, fmt.Errorf("leaderboard: %w", err)
	}
	out := []GroupTotal{}
	if err := cur.All(ctx, &out); err != nil {
		return nil, fmt.Errorf("decode leaderboard: %w", err)
	}
	return out, nil
}
