package aggregation

import (
	"context"
	"fmt"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/socket"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage"
)

// Aggregator aggregates daily bars into monthly bars and persists them.
type Aggregator struct {
	bars        storage.BarStore
	instruments storage.InstrumentStore
}

// NewAggregator creates a new MonthlyBarAggregator backed by the given stores.
func NewAggregator(bars storage.BarStore, instruments storage.InstrumentStore) *Aggregator {
	return &Aggregator{
		bars:        bars,
		instruments: instruments,
	}
}

var _ socket.MonthlyBarAggregator = (*Aggregator)(nil)

// ListDailyBarsForInstrument fetches daily bars for an instrument within the range.
func ListDailyBarsForInstrument(ctx context.Context, store storage.BarStore, id market.InstrumentID, from, to time.Time) ([]market.Bar, error) {
	// Fetch with a wide range to ensure we capture all bars.
	return store.GetBars(ctx, id, market.TimeframeDaily, from.Add(-30*24*time.Hour), to.Add(30*24*time.Hour))
}

// AggregateMonthlyBars fetches daily bars for all matching instruments,
// aggregates them into monthly bars, stores the monthly bars, and returns counts/provenance.
func (a *Aggregator) AggregateMonthlyBars(ctx context.Context, req socket.AggregateMonthlyRequest) (socket.AggregateMonthlyResult, error) {
	var result socket.AggregateMonthlyResult

	if a.bars == nil {
		return result, fmt.Errorf("monthly aggregation: bar store is not available")
	}

	// Discover instruments by matching the filter criteria.
	// We list all instruments and filter client-side since the bar store doesn't
	// have a filter API. This is acceptable for the scope of this task.
	// In production, the instrument store would provide a filtered query.
	insts, err := a.listMatchingInstruments(ctx, req)
	if err != nil {
		return result, fmt.Errorf("monthly aggregation: list instruments: %w", err)
	}

	if len(insts) == 0 {
		return result, fmt.Errorf("monthly aggregation: no matching instruments found")
	}

	allProvenance := make([]socket.MonthlyProvenanceEntry, 0, len(insts))
	totalSourceDailyCount := 0

	for _, inst := range insts {
		dailyBars, err := a.bars.GetBars(ctx, inst.ID, market.TimeframeDaily, req.From, req.To)
		if err != nil {
			return result, fmt.Errorf("monthly aggregation: get daily bars for instrument %s: %w", inst.ID, err)
		}

		if len(dailyBars) == 0 {
			return result, fmt.Errorf("monthly aggregation: no daily bars for instrument %s between %s and %s: %w",
				inst.ID,
				req.From.Format("20060102"),
				req.To.Format("20060102"),
				socket.ErrMonthlySourceBarsNotFound,
			)
		}

		aggregation, err := AggregateDailyToMonthly(dailyBars)
		if err != nil {
			return result, fmt.Errorf("monthly aggregation: instrument %s: %w", inst.ID, err)
		}

		// Store each monthly bar.
		for _, bar := range aggregation.Bars {
			if err := a.bars.UpsertBar(ctx, bar); err != nil {
				return result, fmt.Errorf("monthly aggregation: store monthly bar: %w", err)
			}
		}

		// Build provenance entries.
		for _, prov := range aggregation.Provenance {
			allProvenance = append(allProvenance, socket.MonthlyProvenanceEntry{
				InstrumentID:      prov.InstrumentID,
				SourceDailyCount:  prov.SourceCount,
				MonthlyBarCount:   1, // each provenance record maps to one monthly bar
				SourceStart:       prov.SourceStart,
				SourceEnd:         prov.SourceEnd,
				AggregationRuleID: prov.AggregationRuleID,
			})
			totalSourceDailyCount += prov.SourceCount
		}

		result.Instruments++
		result.MonthlyBarCount += len(aggregation.Bars)
	}

	result.SourceDailyCount = totalSourceDailyCount
	result.Provenance = allProvenance
	return result, nil
}

// listMatchingInstruments queries the instrument store and filters by the
// request criteria.
func (a *Aggregator) listMatchingInstruments(ctx context.Context, req socket.AggregateMonthlyRequest) ([]market.Instrument, error) {
	if a.instruments == nil {
		return nil, fmt.Errorf("monthly aggregation: instrument store is not available")
	}
	insts, err := a.instruments.ListInstruments(ctx)
	if err != nil {
		return nil, fmt.Errorf("list instruments: %w", err)
	}

	// Filter by provider, market, venue.
	filtered := make([]market.Instrument, 0, len(insts))
	for _, inst := range insts {
		if req.Provider != "" && inst.ProviderSymbols[string(req.Provider)] == "" {
			continue
		}
		if req.Selector.Market != "" && inst.Market != req.Selector.Market {
			continue
		}
		if req.Selector.Venue != "" && inst.Venue != req.Selector.Venue {
			continue
		}
		filtered = append(filtered, inst)
	}

	// If symbols are specified, narrow down to those symbols.
	if len(req.Selector.Symbols) > 0 {
		symbolSet := make(map[string]bool, len(req.Selector.Symbols))
		for _, s := range req.Selector.Symbols {
			symbolSet[s] = true
		}
		filteredByName := make([]market.Instrument, 0, len(filtered))
		for _, inst := range filtered {
			if symbolSet[inst.Symbol] {
				filteredByName = append(filteredByName, inst)
				continue
			}
			if req.Provider == "" {
				continue
			}
			providerSymbol := inst.ProviderSymbols[string(req.Provider)]
			if providerSymbol != "" && symbolSet[providerSymbol] {
				filteredByName = append(filteredByName, inst)
			}
		}
		filtered = filteredByName
	}

	return filtered, nil
}
