package ccm

import (
	"encoding/json"
	"fmt"
	"math"
	"os"
	"regexp"
	"sync"
	"time"

	"github.com/sagernet/sing-box/log"
	E "github.com/sagernet/sing/common/exceptions"
)

type UsageStats struct {
	RequestCount                    int   `json:"request_count"`
	MessagesCount                   int   `json:"messages_count"`
	InputTokens                     int64 `json:"input_tokens"`
	OutputTokens                    int64 `json:"output_tokens"`
	CacheReadInputTokens            int64 `json:"cache_read_input_tokens"`
	CacheCreationInputTokens        int64 `json:"cache_creation_input_tokens"`
	CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"`
	CacheCreation1HourInputTokens   int64 `json:"cache_creation_1h_input_tokens,omitempty"`
}

type CostCombination struct {
	Model         string                `json:"model"`
	ContextWindow int                   `json:"context_window"`
	WeekStartUnix int64                 `json:"week_start_unix,omitempty"`
	Total         UsageStats            `json:"total"`
	ByUser        map[string]UsageStats `json:"by_user"`
}

type AggregatedUsage struct {
	LastUpdated  time.Time         `json:"last_updated"`
	Combinations []CostCombination `json:"combinations"`
	mutex        sync.Mutex
	filePath     string
	logger       log.ContextLogger
	lastSaveTime time.Time
	pendingSave  bool
	saveTimer    *time.Timer
	saveMutex    sync.Mutex
}

type UsageStatsJSON struct {
	RequestCount                    int     `json:"request_count"`
	MessagesCount                   int     `json:"messages_count"`
	InputTokens                     int64   `json:"input_tokens"`
	OutputTokens                    int64   `json:"output_tokens"`
	CacheReadInputTokens            int64   `json:"cache_read_input_tokens"`
	CacheCreationInputTokens        int64   `json:"cache_creation_input_tokens"`
	CacheCreation5MinuteInputTokens int64   `json:"cache_creation_5m_input_tokens,omitempty"`
	CacheCreation1HourInputTokens   int64   `json:"cache_creation_1h_input_tokens,omitempty"`
	CostUSD                         float64 `json:"cost_usd"`
}

type CostCombinationJSON struct {
	Model         string                    `json:"model"`
	ContextWindow int                       `json:"context_window"`
	WeekStartUnix int64                     `json:"week_start_unix,omitempty"`
	Total         UsageStatsJSON            `json:"total"`
	ByUser        map[string]UsageStatsJSON `json:"by_user"`
}

type CostsSummaryJSON struct {
	TotalUSD      float64                       `json:"total_usd"`
	ByUser        map[string]float64            `json:"by_user"`
	ByWeek        map[string]float64            `json:"by_week,omitempty"`
	ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
}

type AggregatedUsageJSON struct {
	LastUpdated  time.Time             `json:"last_updated"`
	Costs        CostsSummaryJSON      `json:"costs"`
	Combinations []CostCombinationJSON `json:"combinations"`
}

type WeeklyCycleHint struct {
	WindowMinutes int64
	ResetAt       time.Time
}

type ModelPricing struct {
	InputPrice             float64
	OutputPrice            float64
	CacheReadPrice         float64
	CacheWritePrice5Minute float64
	CacheWritePrice1Hour   float64
}

type modelFamily struct {
	pattern         *regexp.Regexp
	standardPricing ModelPricing
	premiumPricing  *ModelPricing
}

var (
	opus46StandardPricing = ModelPricing{
		InputPrice:             5.0,
		OutputPrice:            25.0,
		CacheReadPrice:         0.5,
		CacheWritePrice5Minute: 6.25,
		CacheWritePrice1Hour:   10.0,
	}

	opus46PremiumPricing = ModelPricing{
		InputPrice:             10.0,
		OutputPrice:            37.5,
		CacheReadPrice:         1.0,
		CacheWritePrice5Minute: 12.5,
		CacheWritePrice1Hour:   20.0,
	}

	opus45Pricing = ModelPricing{
		InputPrice:             5.0,
		OutputPrice:            25.0,
		CacheReadPrice:         0.5,
		CacheWritePrice5Minute: 6.25,
		CacheWritePrice1Hour:   10.0,
	}

	opus4Pricing = ModelPricing{
		InputPrice:             15.0,
		OutputPrice:            75.0,
		CacheReadPrice:         1.5,
		CacheWritePrice5Minute: 18.75,
		CacheWritePrice1Hour:   30.0,
	}

	sonnet46StandardPricing = ModelPricing{
		InputPrice:             3.0,
		OutputPrice:            15.0,
		CacheReadPrice:         0.3,
		CacheWritePrice5Minute: 3.75,
		CacheWritePrice1Hour:   6.0,
	}

	sonnet46PremiumPricing = ModelPricing{
		InputPrice:             6.0,
		OutputPrice:            22.5,
		CacheReadPrice:         0.6,
		CacheWritePrice5Minute: 7.5,
		CacheWritePrice1Hour:   12.0,
	}

	sonnet45StandardPricing = ModelPricing{
		InputPrice:             3.0,
		OutputPrice:            15.0,
		CacheReadPrice:         0.3,
		CacheWritePrice5Minute: 3.75,
		CacheWritePrice1Hour:   6.0,
	}

	sonnet45PremiumPricing = ModelPricing{
		InputPrice:             6.0,
		OutputPrice:            22.5,
		CacheReadPrice:         0.6,
		CacheWritePrice5Minute: 7.5,
		CacheWritePrice1Hour:   12.0,
	}

	sonnet4StandardPricing = ModelPricing{
		InputPrice:             3.0,
		OutputPrice:            15.0,
		CacheReadPrice:         0.3,
		CacheWritePrice5Minute: 3.75,
		CacheWritePrice1Hour:   6.0,
	}

	sonnet4PremiumPricing = ModelPricing{
		InputPrice:             6.0,
		OutputPrice:            22.5,
		CacheReadPrice:         0.6,
		CacheWritePrice5Minute: 7.5,
		CacheWritePrice1Hour:   12.0,
	}

	sonnet37Pricing = ModelPricing{
		InputPrice:             3.0,
		OutputPrice:            15.0,
		CacheReadPrice:         0.3,
		CacheWritePrice5Minute: 3.75,
		CacheWritePrice1Hour:   6.0,
	}

	sonnet35Pricing = ModelPricing{
		InputPrice:             3.0,
		OutputPrice:            15.0,
		CacheReadPrice:         0.3,
		CacheWritePrice5Minute: 3.75,
		CacheWritePrice1Hour:   6.0,
	}

	haiku45Pricing = ModelPricing{
		InputPrice:             1.0,
		OutputPrice:            5.0,
		CacheReadPrice:         0.1,
		CacheWritePrice5Minute: 1.25,
		CacheWritePrice1Hour:   2.0,
	}

	haiku4Pricing = ModelPricing{
		InputPrice:             1.0,
		OutputPrice:            5.0,
		CacheReadPrice:         0.1,
		CacheWritePrice5Minute: 1.25,
		CacheWritePrice1Hour:   2.0,
	}

	haiku35Pricing = ModelPricing{
		InputPrice:             0.8,
		OutputPrice:            4.0,
		CacheReadPrice:         0.08,
		CacheWritePrice5Minute: 1.0,
		CacheWritePrice1Hour:   1.6,
	}

	haiku3Pricing = ModelPricing{
		InputPrice:             0.25,
		OutputPrice:            1.25,
		CacheReadPrice:         0.03,
		CacheWritePrice5Minute: 0.3,
		CacheWritePrice1Hour:   0.5,
	}

	opus3Pricing = ModelPricing{
		InputPrice:             15.0,
		OutputPrice:            75.0,
		CacheReadPrice:         1.5,
		CacheWritePrice5Minute: 18.75,
		CacheWritePrice1Hour:   30.0,
	}

	modelFamilies = []modelFamily{
		{
			pattern:         regexp.MustCompile(`^claude-opus-4-6(?:-|$)`),
			standardPricing: opus46StandardPricing,
			premiumPricing:  &opus46PremiumPricing,
		},
		{
			pattern:         regexp.MustCompile(`^claude-opus-4-5(?:-|$)`),
			standardPricing: opus45Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`),
			standardPricing: opus4Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`),
			standardPricing: opus3Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`),
			standardPricing: sonnet46StandardPricing,
			premiumPricing:  &sonnet46PremiumPricing,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`),
			standardPricing: sonnet45StandardPricing,
			premiumPricing:  &sonnet45PremiumPricing,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`),
			standardPricing: sonnet4StandardPricing,
			premiumPricing:  &sonnet4PremiumPricing,
		},
		{
			pattern:         regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`),
			standardPricing: sonnet37Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`),
			standardPricing: sonnet35Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`),
			standardPricing: haiku45Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-haiku-4(?:-|$)`),
			standardPricing: haiku4Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`),
			standardPricing: haiku35Pricing,
			premiumPricing:  nil,
		},
		{
			pattern:         regexp.MustCompile(`^claude-3-haiku(?:-|$)`),
			standardPricing: haiku3Pricing,
			premiumPricing:  nil,
		},
	}
)

func getPricing(model string, contextWindow int) ModelPricing {
	isPremium := contextWindow >= contextWindowPremium

	for _, family := range modelFamilies {
		if family.pattern.MatchString(model) {
			if isPremium && family.premiumPricing != nil {
				return *family.premiumPricing
			}
			return family.standardPricing
		}
	}

	return sonnet4StandardPricing
}

func calculateCost(stats UsageStats, model string, contextWindow int) float64 {
	pricing := getPricing(model, contextWindow)

	cacheCreationCost := 0.0
	if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 {
		cacheCreationCost = float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute +
			float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour
	} else {
		// Backward compatibility for usage files generated before TTL split tracking.
		cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute
	}

	cost := (float64(stats.InputTokens)*pricing.InputPrice +
		float64(stats.OutputTokens)*pricing.OutputPrice +
		float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice +
		cacheCreationCost) / 1_000_000

	return math.Round(cost*100) / 100
}

func roundCost(cost float64) float64 {
	return math.Round(cost*100) / 100
}

func normalizeCombinations(combinations []CostCombination) {
	for index := range combinations {
		if combinations[index].ByUser == nil {
			combinations[index].ByUser = make(map[string]UsageStats)
		}
	}
}

func addUsageToCombinations(
	combinations *[]CostCombination,
	model string,
	contextWindow int,
	weekStartUnix int64,
	messagesCount int,
	inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
	user string,
) {
	var matchedCombination *CostCombination
	for index := range *combinations {
		combination := &(*combinations)[index]
		if combination.Model == model && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix {
			matchedCombination = combination
			break
		}
	}

	if matchedCombination == nil {
		newCombination := CostCombination{
			Model:         model,
			ContextWindow: contextWindow,
			WeekStartUnix: weekStartUnix,
			Total:         UsageStats{},
			ByUser:        make(map[string]UsageStats),
		}
		*combinations = append(*combinations, newCombination)
		matchedCombination = &(*combinations)[len(*combinations)-1]
	}

	if cacheCreationTokens == 0 {
		cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens
	}

	matchedCombination.Total.RequestCount++
	matchedCombination.Total.MessagesCount += messagesCount
	matchedCombination.Total.InputTokens += inputTokens
	matchedCombination.Total.OutputTokens += outputTokens
	matchedCombination.Total.CacheReadInputTokens += cacheReadTokens
	matchedCombination.Total.CacheCreationInputTokens += cacheCreationTokens
	matchedCombination.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
	matchedCombination.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens

	if user != "" {
		userStats := matchedCombination.ByUser[user]
		userStats.RequestCount++
		userStats.MessagesCount += messagesCount
		userStats.InputTokens += inputTokens
		userStats.OutputTokens += outputTokens
		userStats.CacheReadInputTokens += cacheReadTokens
		userStats.CacheCreationInputTokens += cacheCreationTokens
		userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens
		userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens
		matchedCombination.ByUser[user] = userStats
	}
}

func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) {
	result := make([]CostCombinationJSON, len(combinations))
	var totalCost float64

	for index, combination := range combinations {
		combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ContextWindow)
		totalCost += combinationTotalCost

		combinationJSON := CostCombinationJSON{
			Model:         combination.Model,
			ContextWindow: combination.ContextWindow,
			WeekStartUnix: combination.WeekStartUnix,
			Total: UsageStatsJSON{
				RequestCount:                    combination.Total.RequestCount,
				MessagesCount:                   combination.Total.MessagesCount,
				InputTokens:                     combination.Total.InputTokens,
				OutputTokens:                    combination.Total.OutputTokens,
				CacheReadInputTokens:            combination.Total.CacheReadInputTokens,
				CacheCreationInputTokens:        combination.Total.CacheCreationInputTokens,
				CacheCreation5MinuteInputTokens: combination.Total.CacheCreation5MinuteInputTokens,
				CacheCreation1HourInputTokens:   combination.Total.CacheCreation1HourInputTokens,
				CostUSD:                         combinationTotalCost,
			},
			ByUser: make(map[string]UsageStatsJSON),
		}

		for user, userStats := range combination.ByUser {
			userCost := calculateCost(userStats, combination.Model, combination.ContextWindow)
			if aggregateUserCosts != nil {
				aggregateUserCosts[user] += userCost
			}

			combinationJSON.ByUser[user] = UsageStatsJSON{
				RequestCount:                    userStats.RequestCount,
				MessagesCount:                   userStats.MessagesCount,
				InputTokens:                     userStats.InputTokens,
				OutputTokens:                    userStats.OutputTokens,
				CacheReadInputTokens:            userStats.CacheReadInputTokens,
				CacheCreationInputTokens:        userStats.CacheCreationInputTokens,
				CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens,
				CacheCreation1HourInputTokens:   userStats.CacheCreation1HourInputTokens,
				CostUSD:                         userCost,
			}
		}

		result[index] = combinationJSON
	}

	return result, roundCost(totalCost)
}

func formatUTCOffsetLabel(timestamp time.Time) string {
	_, offsetSeconds := timestamp.Zone()
	sign := "+"
	if offsetSeconds < 0 {
		sign = "-"
		offsetSeconds = -offsetSeconds
	}
	offsetHours := offsetSeconds / 3600
	offsetMinutes := (offsetSeconds % 3600) / 60
	if offsetMinutes == 0 {
		return fmt.Sprintf("UTC%s%d", sign, offsetHours)
	}
	return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes)
}

func formatWeekStartKey(cycleStartAt time.Time) string {
	localCycleStart := cycleStartAt.In(time.Local)
	return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart))
}

func buildByWeekCost(combinations []CostCombination) map[string]float64 {
	byWeek := make(map[string]float64)
	for _, combination := range combinations {
		if combination.WeekStartUnix <= 0 {
			continue
		}
		weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
		weekKey := formatWeekStartKey(weekStartAt)
		byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ContextWindow)
	}
	for weekKey, weekCost := range byWeek {
		byWeek[weekKey] = roundCost(weekCost)
	}
	return byWeek
}

func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
	byUserAndWeek := make(map[string]map[string]float64)
	for _, combination := range combinations {
		if combination.WeekStartUnix <= 0 {
			continue
		}
		weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
		weekKey := formatWeekStartKey(weekStartAt)
		for user, userStats := range combination.ByUser {
			userWeeks, exists := byUserAndWeek[user]
			if !exists {
				userWeeks = make(map[string]float64)
				byUserAndWeek[user] = userWeeks
			}
			userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow)
		}
	}
	for _, weekCosts := range byUserAndWeek {
		for weekKey, cost := range weekCosts {
			weekCosts[weekKey] = roundCost(cost)
		}
	}
	return byUserAndWeek
}

func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
	if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
		return 0
	}
	windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute
	return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix()
}

func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
	u.mutex.Lock()
	defer u.mutex.Unlock()

	result := &AggregatedUsageJSON{
		LastUpdated: u.LastUpdated,
		Costs: CostsSummaryJSON{
			TotalUSD: 0,
			ByUser:   make(map[string]float64),
			ByWeek:   make(map[string]float64),
		},
	}

	globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser)
	result.Combinations = globalCombinationsJSON
	result.Costs.TotalUSD = totalCost
	result.Costs.ByWeek = buildByWeekCost(u.Combinations)

	if len(result.Costs.ByWeek) == 0 {
		result.Costs.ByWeek = nil
	}

	result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
	if len(result.Costs.ByUserAndWeek) == 0 {
		result.Costs.ByUserAndWeek = nil
	}

	for user, cost := range result.Costs.ByUser {
		result.Costs.ByUser[user] = roundCost(cost)
	}

	return result
}

func (u *AggregatedUsage) Load() error {
	u.mutex.Lock()
	defer u.mutex.Unlock()

	u.LastUpdated = time.Time{}
	u.Combinations = nil

	data, err := os.ReadFile(u.filePath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}

	var temp struct {
		LastUpdated  time.Time         `json:"last_updated"`
		Combinations []CostCombination `json:"combinations"`
	}

	err = json.Unmarshal(data, &temp)
	if err != nil {
		return err
	}

	u.LastUpdated = temp.LastUpdated
	u.Combinations = temp.Combinations
	normalizeCombinations(u.Combinations)

	return nil
}

func (u *AggregatedUsage) Save() error {
	jsonData := u.ToJSON()

	data, err := json.MarshalIndent(jsonData, "", "  ")
	if err != nil {
		return err
	}

	tmpFile := u.filePath + ".tmp"
	err = os.WriteFile(tmpFile, data, 0o644)
	if err != nil {
		return err
	}
	defer os.Remove(tmpFile)
	err = os.Rename(tmpFile, u.filePath)
	if err == nil {
		u.saveMutex.Lock()
		u.lastSaveTime = time.Now()
		u.saveMutex.Unlock()
	}
	return err
}

func (u *AggregatedUsage) AddUsage(
	model string,
	contextWindow int,
	messagesCount int,
	inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
	user string,
) error {
	return u.AddUsageWithCycleHint(model, contextWindow, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user, time.Now(), nil)
}

func (u *AggregatedUsage) AddUsageWithCycleHint(
	model string,
	contextWindow int,
	messagesCount int,
	inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64,
	user string,
	observedAt time.Time,
	cycleHint *WeeklyCycleHint,
) error {
	if model == "" {
		return E.New("model cannot be empty")
	}
	if contextWindow <= 0 {
		return E.New("contextWindow must be positive")
	}
	if observedAt.IsZero() {
		observedAt = time.Now()
	}

	u.mutex.Lock()
	defer u.mutex.Unlock()

	u.LastUpdated = observedAt
	weekStartUnix := deriveWeekStartUnix(cycleHint)

	addUsageToCombinations(&u.Combinations, model, contextWindow, weekStartUnix, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user)

	go u.scheduleSave()

	return nil
}

func (u *AggregatedUsage) scheduleSave() {
	const saveInterval = time.Minute

	u.saveMutex.Lock()
	defer u.saveMutex.Unlock()

	timeSinceLastSave := time.Since(u.lastSaveTime)

	if timeSinceLastSave >= saveInterval {
		go u.saveAsync()
		return
	}

	if u.pendingSave {
		return
	}

	u.pendingSave = true
	remainingTime := saveInterval - timeSinceLastSave

	u.saveTimer = time.AfterFunc(remainingTime, func() {
		u.saveMutex.Lock()
		u.pendingSave = false
		u.saveMutex.Unlock()
		u.saveAsync()
	})
}

func (u *AggregatedUsage) saveAsync() {
	err := u.Save()
	if err != nil {
		if u.logger != nil {
			u.logger.Error("save usage statistics: ", err)
		}
	}
}

func (u *AggregatedUsage) cancelPendingSave() {
	u.saveMutex.Lock()
	defer u.saveMutex.Unlock()

	if u.saveTimer != nil {
		u.saveTimer.Stop()
		u.saveTimer = nil
	}
	u.pendingSave = false
}
