package atree

import (
	"fmt"
	"time"

	"gordenko.dev/dima/diploma"
	"gordenko.dev/dima/diploma/timeutil"
)

// AGGREGATE

type InstantAggregator struct {
	firstHourOfDay int
	lastDayOfMonth int
	time2period    func(uint32) uint32
	currentPeriod  uint32
	since          uint32
	until          uint32
	min            float64
	max            float64
	total          float64
	entries        int
}

type InstantAggregatorOptions struct {
	GroupBy        diploma.GroupBy
	FirstHourOfDay int
	LastDayOfMonth int
}

func NewInstantAggregator(opt InstantAggregatorOptions) (*InstantAggregator, error) {
	s := &InstantAggregator{
		firstHourOfDay: opt.FirstHourOfDay,
		lastDayOfMonth: opt.LastDayOfMonth,
	}

	switch opt.GroupBy {
	case diploma.GroupByHour:
		s.time2period = groupByHour

	case diploma.GroupByDay:
		if s.firstHourOfDay > 0 {
			s.time2period = s.groupByDayUsingFHD
		} else {
			s.time2period = groupByDay
		}

	case diploma.GroupByMonth:
		if s.firstHourOfDay > 0 {
			if s.lastDayOfMonth > 0 {
				s.time2period = s.groupByMonthUsingFHDAndLDM
			} else {
				s.time2period = s.groupByMonthUsingFHD
			}
		} else {
			if s.lastDayOfMonth > 0 {
				s.time2period = s.groupByMonthUsingLDM
			} else {
				s.time2period = groupByMonth
			}
		}

	default:
		return nil, fmt.Errorf("unknown groupBy %d option", opt.GroupBy)
	}

	return s, nil
}

// Приходят данные от свежих к старым, тоесть сперва получаю Until.
// return period complete flag
func (s *InstantAggregator) Feed(timestamp uint32, value float64, p *InstantPeriod) bool {
	period := s.time2period(timestamp)
	//fmt.Printf("feed: %s %v, period: %s\n", time.Unix(int64(timestamp), 0), value, time.Unix(int64(period), 0))
	if s.entries == 0 {
		s.currentPeriod = period
		s.since = timestamp
		s.until = timestamp
		s.min = value
		s.max = value
		s.total = value
		s.entries = 1
		return false
	}

	if period != s.currentPeriod {
		// готовый период
		s.FillPeriod(timestamp, p)
		s.currentPeriod = period
		s.since = timestamp
		s.until = timestamp
		s.min = value
		s.max = value
		s.total = value
		s.entries = 1
		return true
	}

	if value < s.min {
		s.min = value
	} else if value > s.max {
		s.max = value
	}
	// для подсчета AVG
	s.total += value
	s.entries++
	// начало периода
	s.since = timestamp
	return false
}

func (s *InstantAggregator) FillPeriod(prevTimestamp uint32, p *InstantPeriod) bool {
	if s.entries == 0 {
		return false
	}

	//fmt.Printf("FillPeriod: %s, prevTimestamp: %s\n", time.Unix(int64(s.currentPeriod), 0), time.Unix(int64(prevTimestamp), 0))
	p.Period = s.currentPeriod
	if prevTimestamp > 0 {
		p.Since = prevTimestamp
	} else {
		p.Since = s.since
	}
	p.Until = s.until
	p.Min = s.min
	p.Max = s.max
	p.Avg = s.total / float64(s.entries)
	return true
}

func (s *InstantAggregator) groupByDayUsingFHD(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	return uint32(tm.Unix())
}

func (s *InstantAggregator) groupByMonthUsingFHD(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	return uint32(tm.Unix())
}

func (s *InstantAggregator) groupByMonthUsingLDM(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Day() > s.lastDayOfMonth {
		tm = tm.AddDate(0, 1, 0)
	}
	return uint32(tm.Unix())
}

func (s *InstantAggregator) groupByMonthUsingFHDAndLDM(timestamp uint32) uint32 {
	// ВАЖНО!
	// Сперва проверяю время.
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	if tm.Day() > s.lastDayOfMonth {
		tm = tm.AddDate(0, 1, 0)
	}
	return uint32(tm.Unix())
}

// CUMULATIVE

type CumulativeAggregator struct {
	firstHourOfDay int
	lastDayOfMonth int
	time2period    func(uint32) uint32
	currentPeriod  uint32
	since          uint32
	until          uint32
	sinceValue     float64
	untilValue     float64
	entries        int
}

type CumulativeAggregatorOptions struct {
	GroupBy        diploma.GroupBy
	FirstHourOfDay int
	LastDayOfMonth int
}

func NewCumulativeAggregator(opt CumulativeAggregatorOptions) (*CumulativeAggregator, error) {
	s := &CumulativeAggregator{
		firstHourOfDay: opt.FirstHourOfDay,
		lastDayOfMonth: opt.LastDayOfMonth,
	}

	switch opt.GroupBy {
	case diploma.GroupByHour:
		s.time2period = groupByHour

	case diploma.GroupByDay:
		if s.firstHourOfDay > 0 {
			s.time2period = s.groupByDayUsingFHD
		} else {
			s.time2period = groupByDay
		}

	case diploma.GroupByMonth:
		if s.firstHourOfDay > 0 {
			if s.lastDayOfMonth > 0 {
				s.time2period = s.groupByMonthUsingFHDAndLDM
			} else {
				s.time2period = s.groupByMonthUsingFHD
			}
		} else {
			if s.lastDayOfMonth > 0 {
				s.time2period = s.groupByMonthUsingLDM
			} else {
				s.time2period = groupByMonth
			}
		}

	default:
		return nil, fmt.Errorf("unknown groupBy %d option", opt.GroupBy)
	}

	return s, nil
}

// return period complete flag
func (s *CumulativeAggregator) Feed(timestamp uint32, value float64, p *CumulativePeriod) bool {
	period := s.time2period(timestamp)
	if s.entries == 0 {
		s.currentPeriod = period
		s.since = timestamp
		s.until = timestamp
		s.sinceValue = value
		s.untilValue = value
		s.entries = 1
		return false
	}

	if period != s.currentPeriod {
		// готовый период
		s.FillPeriod(timestamp, value, p)
		s.currentPeriod = period
		s.since = timestamp
		s.until = timestamp
		s.sinceValue = value
		s.untilValue = value
		s.entries = 1
		return true
	}

	// начало периода
	s.since = timestamp
	s.sinceValue = value
	s.entries++
	return false
}

func (s *CumulativeAggregator) FillPeriod(prevTimestamp uint32, value float64, p *CumulativePeriod) bool {
	if s.entries == 0 {
		return false
	}
	p.Period = s.currentPeriod
	if prevTimestamp > 0 {
		p.Since = prevTimestamp
		p.Total = s.untilValue - value
	} else {
		p.Since = s.since
		p.Total = s.untilValue - s.sinceValue
	}
	p.Until = s.until
	p.EndValue = s.untilValue

	return true
}

func (s *CumulativeAggregator) groupByDayUsingFHD(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	return uint32(tm.Unix())
}

func (s *CumulativeAggregator) groupByMonthUsingFHD(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	return uint32(tm.Unix())
}

func (s *CumulativeAggregator) groupByMonthUsingLDM(timestamp uint32) uint32 {
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Day() > s.lastDayOfMonth {
		tm = tm.AddDate(0, 1, 0)
	}
	return uint32(tm.Unix())
}

func (s *CumulativeAggregator) groupByMonthUsingFHDAndLDM(timestamp uint32) uint32 {
	// ВАЖНО!
	// Сперва проверяю время.
	tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m")
	if tm.Hour() < s.firstHourOfDay {
		tm = tm.AddDate(0, 0, -1)
	}
	if tm.Day() > s.lastDayOfMonth {
		tm = tm.AddDate(0, 1, 0)
	}
	return uint32(tm.Unix())
}

func groupByHour(timestamp uint32) uint32 {
	return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "h").Unix())
}

func groupByDay(timestamp uint32) uint32 {
	return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d").Unix())
}

func groupByMonth(timestamp uint32) uint32 {
	return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m").Unix())
}
