package aggregation

import (
	"fmt"
	"math/big"
	"sort"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
)

// Provenance records how a monthly bar was derived from daily bars.
// Each monthly bar has its own provenance record linking it to the source
// daily bars used for aggregation.
type Provenance struct {
	SourceTimeframe   market.Timeframe    `json:"source_timeframe"`
	TargetTimeframe   market.Timeframe    `json:"target_timeframe"`
	InstrumentID      market.InstrumentID `json:"instrument_id"`
	SourceStart       time.Time           `json:"source_start"`
	SourceEnd         time.Time           `json:"source_end"`
	SourceCount       int                 `json:"source_count"`
	AggregationRuleID string              `json:"aggregation_rule_id"`
}

// ProvenanceForBar creates a provenance record for a single monthly bar.
func ProvenanceForBar(bar market.Bar, sourceStart, sourceEnd time.Time, sourceCount int) Provenance {
	return Provenance{
		SourceTimeframe:   market.TimeframeDaily,
		TargetTimeframe:   market.TimeframeMonthly,
		InstrumentID:      bar.InstrumentID,
		SourceStart:       sourceStart,
		SourceEnd:         sourceEnd,
		SourceCount:       sourceCount,
		AggregationRuleID: "monthly-deterministic-v1",
	}
}

// MonthlyResult holds the aggregated monthly bars and their provenance.
type MonthlyResult struct {
	Bars       []market.Bar `json:"bars"`
	Provenance []Provenance `json:"provenance"`
}

// AggregateDailyToMonthly converts daily bars into monthly OHLCV bars and
// returns the result with provenance. Input bars must all share the same
// instrument, be strictly daily timeframe, share the same currency, and contain
// valid positive price/volume values.
func AggregateDailyToMonthly(input []market.Bar) (MonthlyResult, error) {
	var result MonthlyResult

	if len(input) == 0 {
		return result, fmt.Errorf("aggregate daily to monthly: input is empty")
	}

	// Validate and group by instrument
	instrumentBars := make(map[string][]market.Bar)
	var instrumentIDs []string
	seenIDs := make(map[string]bool)
	for _, bar := range input {
		idStr := string(bar.InstrumentID)
		if !seenIDs[idStr] {
			seenIDs[idStr] = true
			instrumentIDs = append(instrumentIDs, idStr)
		}
		instrumentBars[idStr] = append(instrumentBars[idStr], bar)
	}

	if len(instrumentBars) > 1 {
		return result, fmt.Errorf("aggregate daily to monthly: mixed instrument IDs: %v", instrumentIDs)
	}

	// Validate all bars are daily
	for _, bar := range input {
		if bar.Timeframe != market.TimeframeDaily {
			return result, fmt.Errorf("aggregate daily to monthly: expected daily timeframe, got %q", bar.Timeframe)
		}
	}

	// Validate currency consistency
	firstCurrency := input[0].Open.Currency
	for _, bar := range input {
		if bar.Open.Currency != firstCurrency || bar.High.Currency != firstCurrency ||
			bar.Low.Currency != firstCurrency || bar.Close.Currency != firstCurrency {
			return result, fmt.Errorf("aggregate daily to monthly: mixed currencies in input")
		}
	}

	// Validate decimal values and sort by timestamp
	validatedBars := make([]market.Bar, 0, len(input))
	for _, bar := range input {
		if err := validateBarDecimals(bar); err != nil {
			return result, err
		}
		validatedBars = append(validatedBars, bar)
	}
	sort.Slice(validatedBars, func(i, j int) bool {
		return validatedBars[i].Timestamp.Before(validatedBars[j].Timestamp)
	})

	// Group by month and aggregate
	type monthKey struct {
		year  int
		month int
	}
	type monthBars struct {
		timestamp time.Time
		open      *big.Rat
		high      *big.Rat
		low       *big.Rat
		close     *big.Rat
		volume    *big.Rat
		srcStart  time.Time
		srcEnd    time.Time
		srcCount  int
	}
	monthOrder := []monthKey{}
	monthMap := make(map[monthKey]*monthBars)

	for _, bar := range validatedBars {
		t := bar.Timestamp.UTC()
		// Use the day of the month to determine which month this bar belongs to.
		// The timestamp is already in UTC and represents a daily bar.
		// For aggregation, we group by the month component of the timestamp.
		mk := monthKey{year: t.Year(), month: int(t.Month())}

		amountToRat := func(amount market.Decimal) *big.Rat {
			r := new(big.Rat)
			r.SetString(amount.Value)
			return r
		}

		if _, exists := monthMap[mk]; !exists {
			monthOrder = append(monthOrder, mk)
			monthMap[mk] = &monthBars{
				timestamp: time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC),
				open:      amountToRat(bar.Open.Amount),
				high:      amountToRat(bar.High.Amount),
				low:       amountToRat(bar.Low.Amount),
				close:     amountToRat(bar.Close.Amount),
				volume:    amountToRat(bar.Volume.Amount),
				srcStart:  bar.Timestamp,
				srcEnd:    bar.Timestamp,
				srcCount:  1,
			}
		} else {
			mb := monthMap[mk]
			// Open: keep first
			// High: max
			if amountToRat(bar.High.Amount).Cmp(mb.high) > 0 {
				mb.high = amountToRat(bar.High.Amount)
			}
			// Low: min
			if amountToRat(bar.Low.Amount).Cmp(mb.low) < 0 {
				mb.low = amountToRat(bar.Low.Amount)
			}
			// Close: keep last (always overwritten)
			mb.close = amountToRat(bar.Close.Amount)
			// Volume: sum
			mb.volume.Add(mb.volume, amountToRat(bar.Volume.Amount))
			// Track per-month source range/count
			if bar.Timestamp.Before(mb.srcStart) {
				mb.srcStart = bar.Timestamp
			}
			if bar.Timestamp.After(mb.srcEnd) {
				mb.srcEnd = bar.Timestamp
			}
			mb.srcCount++
		}
	}

	// Sort months
	sort.Slice(monthOrder, func(i, j int) bool {
		a, b := monthOrder[i], monthOrder[j]
		if a.year != b.year {
			return a.year < b.year
		}
		return a.month < b.month
	})

	// Build result bars and per-month provenance
	for _, mk := range monthOrder {
		mb := monthMap[mk]
		bar := market.Bar{
			InstrumentID: input[0].InstrumentID,
			Timeframe:    market.TimeframeMonthly,
			Timestamp:    mb.timestamp,
			Open:         market.Price{Currency: firstCurrency, Amount: ratToDecimal(mb.open)},
			High:         market.Price{Currency: firstCurrency, Amount: ratToDecimal(mb.high)},
			Low:          market.Price{Currency: firstCurrency, Amount: ratToDecimal(mb.low)},
			Close:        market.Price{Currency: firstCurrency, Amount: ratToDecimal(mb.close)},
			Volume:       market.Quantity{Amount: ratToDecimal(mb.volume)},
		}
		result.Bars = append(result.Bars, bar)

		// Build provenance with per-month source range/count.
		result.Provenance = append(result.Provenance, ProvenanceForBar(bar, mb.srcStart, mb.srcEnd, mb.srcCount))
	}

	return result, nil
}

// validateBarDecimals checks that all price and volume values are valid and positive.
func validateBarDecimals(bar market.Bar) error {
	prices := []market.Decimal{bar.Open.Amount, bar.High.Amount, bar.Low.Amount, bar.Close.Amount}
	for _, p := range prices {
		if err := validateDecimal(p); err != nil {
			return fmt.Errorf("invalid price decimal: %w", err)
		}
	}
	if err := validateDecimal(bar.Volume.Amount); err != nil {
		return fmt.Errorf("invalid volume decimal: %w", err)
	}
	return nil
}

// validateDecimal checks that the decimal string is a valid positive number.
func validateDecimal(d market.Decimal) error {
	if d.Value == "" {
		return fmt.Errorf("empty decimal value")
	}
	r := new(big.Rat)
	r.SetString(d.Value)
	if r.Sign() <= 0 {
		return fmt.Errorf("decimal must be positive, got %s", d.Value)
	}
	return nil
}

// ratToDecimal converts a big.Rat to market.Decimal, producing a finite
// decimal string with at most 8 decimal places.
func ratToDecimal(r *big.Rat) market.Decimal {
	f := r.FloatString(8)
	return market.Decimal{Value: f}
}
