package usecase

import (
	"context"
	"fmt"
	"math"
	"sort"
	"time"

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

const analyticsDateLayout = "2006-01-02"

func ParseAnalyticsQuery(granularity, fromRaw, toRaw string) (domain.AnalyticsQuery, error) {
	if granularity != "day" && granularity != "month" && granularity != "year" {
		return domain.AnalyticsQuery{}, fmt.Errorf("%w: granularity must be day, month, or year", domain.ErrInvalidRange)
	}
	from, err := time.Parse(analyticsDateLayout, fromRaw)
	if err != nil {
		return domain.AnalyticsQuery{}, fmt.Errorf("%w: from must be YYYY-MM-DD", domain.ErrInvalidRange)
	}
	to, err := time.Parse(analyticsDateLayout, toRaw)
	if err != nil {
		return domain.AnalyticsQuery{}, fmt.Errorf("%w: to must be YYYY-MM-DD", domain.ErrInvalidRange)
	}
	if from.After(to) {
		return domain.AnalyticsQuery{}, fmt.Errorf("%w: from must not be after to", domain.ErrInvalidRange)
	}
	q := domain.AnalyticsQuery{Granularity: granularity, From: from.UTC(), To: to.UTC()}
	if len(analyticsBuckets(q)) > map[string]int{"day": 93, "month": 36, "year": 8}[granularity] {
		return domain.AnalyticsQuery{}, fmt.Errorf("%w: range exceeds %s limit", domain.ErrInvalidRange, granularity)
	}
	return q, nil
}

type analyticsBucketRange struct {
	key   string
	label string
	from  time.Time
	to    time.Time
}

func analyticsBuckets(q domain.AnalyticsQuery) []analyticsBucketRange {
	var buckets []analyticsBucketRange
	switch q.Granularity {
	case "day":
		for cur := q.From; !cur.After(q.To); cur = cur.AddDate(0, 0, 1) {
			buckets = append(buckets, analyticsBucketRange{key: cur.Format(analyticsDateLayout), label: cur.Format("01/02"), from: cur, to: cur})
		}
	case "month":
		for cur := time.Date(q.From.Year(), q.From.Month(), 1, 0, 0, 0, 0, time.UTC); !cur.After(q.To); cur = cur.AddDate(0, 1, 0) {
			end := cur.AddDate(0, 1, -1)
			buckets = append(buckets, analyticsBucketRange{key: cur.Format("2006-01"), label: cur.Format("2006/01"), from: maxTime(cur, q.From), to: minTime(end, q.To)})
		}
	case "year":
		for year := q.From.Year(); year <= q.To.Year(); year++ {
			start := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
			end := time.Date(year, 12, 31, 0, 0, 0, 0, time.UTC)
			key := fmt.Sprintf("%04d", year)
			buckets = append(buckets, analyticsBucketRange{key: key, label: key, from: maxTime(start, q.From), to: minTime(end, q.To)})
		}
	}
	return buckets
}

func maxTime(a, b time.Time) time.Time {
	if a.After(b) {
		return a
	}
	return b
}

func minTime(a, b time.Time) time.Time {
	if a.Before(b) {
		return a
	}
	return b
}

func proratedCredits(monthly int, from, to time.Time) int {
	credits := 0.0
	for cur := from; !cur.After(to); cur = cur.AddDate(0, 0, 1) {
		days := time.Date(cur.Year(), cur.Month()+1, 0, 0, 0, 0, 0, time.UTC).Day()
		credits += float64(monthly) / float64(days)
	}
	return int(math.Round(credits))
}

func isAIMeter(meter string) bool {
	return meter == domain.MeterAICopy || meter == domain.MeterAIResearch || meter == domain.MeterAIImage
}

func emptyMeterCounts() map[string]domain.MeterCount {
	return map[string]domain.MeterCount{
		domain.MeterAICopy: {}, domain.MeterAIResearch: {}, domain.MeterWebSearch: {}, domain.MeterAIImage: {},
	}
}

func (s *Service) TenantAnalytics(ctx context.Context, q domain.AnalyticsQuery, tenantMembers []domain.AnalyticsMember) (*domain.TenantAnalytics, error) {
	events, err := s.Repo.ListEventsRange(ctx, q.From.UnixNano(), q.To.AddDate(0, 0, 1).UnixNano())
	if err != nil {
		return nil, err
	}

	type memberAcc struct {
		row domain.AnalyticsMemberRow
	}
	members := make(map[int64]*memberAcc, len(tenantMembers))
	for _, member := range tenantMembers {
		members[member.UID] = &memberAcc{row: domain.AnalyticsMemberRow{UID: member.UID, Email: member.Email, DisplayName: member.DisplayName}}
	}
	for _, event := range events {
		if members[event.UID] == nil {
			members[event.UID] = &memberAcc{row: domain.AnalyticsMemberRow{UID: event.UID}}
		}
	}

	buckets := analyticsBuckets(q)
	bucketByKey := make(map[string]int, len(buckets))
	result := &domain.TenantAnalytics{
		Granularity: q.Granularity, From: q.From.Format(analyticsDateLayout), To: q.To.Format(analyticsDateLayout),
		RangeLabel: fmt.Sprintf("%s - %s", q.From.Format(analyticsDateLayout), q.To.Format(analyticsDateLayout)),
		ByMeter:    emptyMeterCounts(), Byok: domain.ByokBlock{ByMeter: emptyMeterCounts()},
	}
	for _, bucket := range buckets {
		row := domain.AnalyticsBucket{Key: bucket.key, Label: bucket.label}
		result.Series = append(result.Series, row)
		bucketByKey[bucket.key] = len(result.Series) - 1
	}

	for _, acc := range members {
		prefs, prefsErr := s.ensurePrefs(ctx, acc.row.UID)
		if prefsErr != nil {
			return nil, prefsErr
		}
		plan := domain.ResolvePlan(prefs.PlanID)
		acc.row.PlanID, acc.row.Unlimited = plan.ID, prefs.Unlimited
		acc.row.PurchasedCredits = proratedCredits(plan.MonthlyCredits, q.From, q.To)
		result.PurchasedCredits += acc.row.PurchasedCredits
		for i, bucket := range buckets {
			result.Series[i].PurchasedCredits += proratedCredits(plan.MonthlyCredits, bucket.from, bucket.to)
		}
	}

	for _, event := range events {
		bucketIndex, hasBucket := bucketByKey[eventBucketKey(event.CreatedAt, q.Granularity)]
		acc := members[event.UID]
		if event.KeyMode == domain.KeyModeByok {
			result.Byok.CallCount++
			meter := result.Byok.ByMeter[event.Meter]
			meter.Count++
			result.Byok.ByMeter[event.Meter] = meter
			acc.row.ByokCallCount++
			if hasBucket {
				result.Series[bucketIndex].ByokCallCount++
			}
			continue
		}
		if event.KeyMode != domain.KeyModePlatform {
			continue
		}
		credits := event.Credits
		if credits < 0 {
			credits = 0
		}
		result.ConsumedCredits += credits
		meter := result.ByMeter[event.Meter]
		meter.Count++
		meter.Credits += credits
		result.ByMeter[event.Meter] = meter
		acc.row.TotalCredits += credits
		if isAIMeter(event.Meter) {
			result.AICalls++
			acc.row.AICalls++
			if hasBucket {
				result.Series[bucketIndex].AICalls++
			}
		} else if event.Meter == domain.MeterWebSearch {
			result.SearchCalls++
			acc.row.SearchCalls++
			if hasBucket {
				result.Series[bucketIndex].SearchCalls++
			}
		}
		if hasBucket {
			result.Series[bucketIndex].ConsumedCredits += credits
		}
	}

	result.RemainingCredits = maxInt(0, result.PurchasedCredits-result.ConsumedCredits)
	result.Pct = percentage(result.ConsumedCredits, result.PurchasedCredits)
	for _, acc := range members {
		acc.row.RemainingCredits = maxInt(0, acc.row.PurchasedCredits-acc.row.TotalCredits)
		acc.row.Pct = percentage(acc.row.TotalCredits, acc.row.PurchasedCredits)
		result.Members = append(result.Members, acc.row)
	}
	sort.Slice(result.Members, func(i, j int) bool {
		if result.Members[i].TotalCredits == result.Members[j].TotalCredits {
			return result.Members[i].UID < result.Members[j].UID
		}
		return result.Members[i].TotalCredits > result.Members[j].TotalCredits
	})
	return result, nil
}

func eventBucketKey(nano int64, granularity string) string {
	t := time.Unix(0, nano).UTC()
	switch granularity {
	case "day":
		return t.Format(analyticsDateLayout)
	case "month":
		return t.Format("2006-01")
	default:
		return t.Format("2006")
	}
}

func percentage(used, total int) int {
	if total <= 0 {
		return 0
	}
	return minInt(999, int(math.Round(float64(used)*100/float64(total))))
}

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}
