package mqe

import (
	"database/sql"
	"fmt"
	"time"
)

// func applyCorrections(corrections []model.F64Correction, measures []Measure) []Measure {
// 	for idx, measure := range measures {
// 		var isCorrected bool
// 		for _, correction := range corrections {
// 			if measure.Time >= correction.Time {
// 				measure.Value += correction.Value
// 				isCorrected = true
// 			}
// 		}
// 		if isCorrected {
// 			measures[idx] = measure
// 		}
// 	}
// 	return measures
// }

func applyCorrectionsToMeasure(corrections []_f64Correction, tm int64, value float64) float64 {
	for _, correction := range corrections {
		if tm >= correction.Time {
			value += correction.Value
		}
	}
	return value
}

func applyCorrectionsToMeasures(corrections []_f64Correction, measures []_measure) {
	for idx, m := range measures {
		for _, correction := range corrections {
			if m.Time >= correction.Time {
				m.Value += correction.Value
			}
		}
		measures[idx] = m
	}
}

func applyCorrectionsToRawMeasures(corrections []_f64Correction, measures []RawMeasure) {
	for idx, m := range measures {
		for _, correction := range corrections {
			if m.Time >= correction.Time {
				m.Value += correction.Value
			}
		}
		measures[idx] = m
	}
}

type _measure struct {
	Time  int64
	Value float64
}

// ВАЖНО!
// ВО ВСЕХ ЗАПРОСАХ ОБЯЗАТЕЛЬНА КОРРЕКТНАЯ СОРТИРОВКА!

type _f64Correction struct {
	Time  int64
	Value float64
}

func listF64CorrectionsTx(tx *sql.Tx, metricID int64) (list []_f64Correction, err error) {
	rows, err := tx.Query(
		"SELECT tm, value FROM metric_corrections WHERE metricID=? ORDER BY tm ASC",
		metricID)
	if err != nil {
		if err == sql.ErrNoRows {
			err = nil
		}
		return
	}
	defer rows.Close()

	for rows.Next() {
		var correction _f64Correction
		err = rows.Scan(&correction.Time, &correction.Value)
		if err != nil {
			return
		}
		list = append(list, correction)
	}
	return
}

// result = append(result, Measure{
// 	Time: tm,
// 	Values: []float64{
// 		value,
// 		value - prevValue,
// 	},
// })

// listCumulativeMeasures - возвращает список CumulativeMeasure.
// Метод ничего не знает про FirstHourOfDay, но извне границы Since и Until могут быть
// скорректированы с учетом FirstHourOfDay. Это не поломает выборку.
func (s *MeasureQueryEngine) listCumulativeMeasures(req MetricMeasuresFilter) (_ []Measure, err error) {
	tx, err := s.db.Driver().Begin()
	if err != nil {
		return
	}
	defer tx.Rollback()

	// corrections, err := listF64CorrectionsTx(tx, req.MetricID)
	// if err != nil {
	// 	return
	// }

	// ВАЖНО!
	// Оптимизация! Запрашиваем больше данных чем нужно, чтобы посчитать Total для
	// самого старого измерения в result.
	// Отнимаем от since еще 1 час (с запасом) чтобы зацепить extendedMeasure
	extendedSince := req.Since - 3600

	rows, err := tx.Query(`
	SELECT tm, value
	FROM f64
	WHERE metricID=? AND tm BETWEEN ? AND ?
	ORDER BY tm ASC`,
		req.MetricID, extendedSince, req.Until)
	if err != nil {
		if err == sql.ErrNoRows {
			err = nil
		}
		return
	}
	defer rows.Close()

	var (
		result   []Measure
		measures []_measure
		prev     *_measure
	)

	// Вычитываем все найденные показания
	for rows.Next() {
		var m _measure
		err = rows.Scan(&m.Time, &m.Value)
		if err != nil {
			return
		}

		if m.Time < req.Since {
			// пока не найдено первое показание внутри диапазона обновлем extendedValue
			prev = &_measure{
				Time:  m.Time,
				Value: m.Value,
			}
		} else {
			measures = append(measures, m)
			break
		}
	}

	// Остальные показания вычитываем без проверок
	for rows.Next() {
		var m _measure
		err = rows.Scan(&m.Time, &m.Value)
		if err != nil {
			return
		}
		measures = append(measures, m)
	}

	if err = rows.Err(); err != nil {
		return
	}

	if len(measures) == 0 {
		return
	}

	if prev == nil {
		// extended not found
		// extended показание не найдено, а значит для первого показания не сможем показать total.
		// Нужен дополнительный запрос.
		// Находим максимально свежее показание метрики до extendedSince, от которого и будем
		// считать total.
		var (
			tm    int64
			value float64
		)

		err = tx.QueryRow(`
		SELECT tm, value
		FROM f64
		WHERE metricID=? AND tm < ?
		ORDER BY tm DESC
		LIMIT 1`,
			req.MetricID, extendedSince).Scan(&tm, &value)

		if err != nil {
			if err != sql.ErrNoRows {
				return
			}
			err = nil
		} else {
			prev = &_measure{
				Time:  tm,
				Value: value,
			}
		}
	}

	// ВАЖНО!
	// Сперва применяем коррекции к показаниям, а затем рассчитываем сумму за период

	// if len(corrections) > 0 {
	// 	applyCorrectionsToMeasures(corrections, measures)
	// 	if prev != nil {
	// 		prev.Value = applyCorrectionsToMeasure(corrections, prev.Time, prev.Value)
	// 	}
	// }

	var prevValue float64
	if prev != nil {
		prevValue = prev.Value
	}

	for _, m := range measures {
		result = append(result, Measure{
			Time: m.Time,
			Values: []float64{
				m.Value,
				m.Value - prevValue,
			},
		})
		prevValue = m.Value
	}

	// Корректируем самый первый тотал. Eсли prev показания не было -> total = 0
	if prev == nil {
		first := result[0]
		first.Values[1] = 0
		result[0] = first
	}

	return result, nil
}

type AggregatedMeasuresFilter struct {
	MetricID       int64   `json:"metricID"`
	Since          int64   `json:"since"`
	Until          int64   `json:"until"`
	GroupBy        GroupBy `json:"groupBy"`
	LastDayOfMonth int     `json:"lastDayOfMonth"` // например, конец месяца 25 число
	FirstHourOfDay int     `json:"firstHourOfDay"` // например день начинается в 6:00
}

// listAggregatedCumulativeMeasures - возвращает список AggregatedCumulativeMeasure,
// сгрупированный по какому-то периоду
func (s *MeasureQueryEngine) listAggregatedCumulativeMeasures(req AggregatedMeasuresFilter) (_ []AggregatedMeasure, err error) {
	//pretty.PPrintln("listAggregatedCumulativeMeasures", req)

	tx, err := s.db.Driver().Begin()
	if err != nil {
		return
	}
	defer tx.Rollback()

	corrections, err := listF64CorrectionsTx(tx, req.MetricID)
	if err != nil {
		return
	}

	var (
		// чтобы не экранировать символы в шаблоне строки
		groupByFormat     string
		parsePeriodLayout string
	)

	switch req.GroupBy {
	case ByHour:
		groupByFormat = "%Y%m%d%H"
		parsePeriodLayout = hourPeriodLayout

	case ByDay:
		groupByFormat = "%Y%m%d"
		parsePeriodLayout = dayPeriodLayout

	case ByMonth:
		groupByFormat = "%Y%m"
		parsePeriodLayout = monthPeriodLayout

	default:
		// Защита от дурака
		err = fmt.Errorf("unknown groupBy: %s", req.GroupBy)
		return
	}

	// ВАЖНО!
	// Оптимизация! Запрашиваем больше данных чем нужно, чтобы посчитать начало
	// первого и конец последнего периодов без дополнительных запросов.
	//

	// Отнимаем 3 минуты чтобы зацепить последнее показание предыдущего периода
	extendedSince := req.Since - 3*60
	// Добавляем 1 час, чтобы зацепить первое показание следующего периода
	extendedUntil := req.Until + 60*60

	//fmt.Printf("extendedSince: %s\n\n", extendedSince)

	// Для каждого периода выбираем первое и последнее (по времени) измерение.
	// Если делать выборку одним запросом без UNION, а именно сделать
	// INNER JOIN ... ON f64.tm=x.maxtm OR f64.tm=x.mintm - все начинает тормозить.
	// Два запроса работают достаточно быстро.

	// ВАЖНО!
	// Не забыть про сортировку tm DESC!

	dt, err := getPeriodDateTime(req.GroupBy, req.LastDayOfMonth, req.FirstHourOfDay)
	if err != nil {
		return
	}

	//fmt.Printf("DT: %s\n\n", dt)

	query := fmt.Sprintf(`
	WITH 
		measures AS (SELECT * FROM f64 WHERE metricID=? AND tm BETWEEN ? AND ?),
		aggregated AS (
			SELECT MIN(tm) as minTm, MAX(tm) as maxTm, DATE_FORMAT(%s, '%s') as periodStr
			FROM measures
			GROUP BY periodStr
		)
	SELECT aggregated.periodStr, firstMeasures.tm, firstMeasures.value, lastMeasures.tm, lastMeasures.value
	FROM aggregated
		INNER JOIN measures firstMeasures ON firstMeasures.tm = aggregated.minTm
		INNER JOIN measures lastMeasures ON lastMeasures.tm = aggregated.maxTm
	ORDER BY aggregated.periodStr ASC`,
		dt, groupByFormat)

	rows, err := tx.Query(query, req.MetricID, extendedSince, extendedUntil)
	if err != nil {
		if err == sql.ErrNoRows {
			err = nil
		}
		return
	}
	defer rows.Close()

	var result []AggregatedMeasure

	periodCalculator := NewPeriodCalculator(PeriodCalculatorOptions{
		GroupBy:        req.GroupBy,
		LastDayOfMonth: req.LastDayOfMonth,
		FirstHourOfDay: req.FirstHourOfDay,
		Since:          time.Unix(req.Since, 0),
		Until:          time.Unix(req.Until, 0),
	})

	periodProducer := NewPeriodProducer(PeriodProducerOptions{
		MetricID:          req.MetricID,
		ParsePeriodLayout: parsePeriodLayout,
		Location:          s.location,
		Until:             req.Until,
		Corrections:       corrections,
		Rows:              rows,
		Tx:                tx,
	})

	result, err = SplitByPeriods(periodProducer, periodCalculator)
	if err != nil {
		return
	}
	return result, nil
}

type RangeTotal struct {
	Since      int64   `json:"since"`      // реальная граница
	Until      int64   `json:"until"`      // реальная граница
	StartValue float64 `json:"startValue"` // на конец периода
	EndValue   float64 `json:"endValue"`   // на конец периода
}

type getCumulativeTotalFilter struct {
	MetricID       int64
	Since          int64
	Until          int64
	LastDayOfMonth int
	FirstHourOfDay int
}

func (s *MeasureQueryEngine) getCumulativeTotal(req getCumulativeTotalFilter) (_ *RangeTotal, err error) {
	measures, err := s.listAggregatedCumulativeMeasures(AggregatedMeasuresFilter{
		MetricID:       req.MetricID,
		Since:          req.Since,
		Until:          req.Until,
		GroupBy:        ByDay,
		LastDayOfMonth: req.LastDayOfMonth,
		FirstHourOfDay: req.FirstHourOfDay,
	})
	if err != nil {
		return
	}

	if len(measures) == 0 {
		return
	}

	//pretty.PPrintln("TOTAL", measures)

	first := measures[0]
	last := measures[len(measures)-1]

	return &RangeTotal{
		Since:      first.Since,
		Until:      last.Until,
		StartValue: first.Values[0] - first.Values[1], // value on period end - period total
		EndValue:   last.Values[0],
	}, nil
}

/*
// GetCumulativeTotal - фикс, добавить опции FirstHourInDay, IsFindPeriodEndsInFuture
func (s *MeasureQueryEngine) getCumulativeTotal(req getCumulativeTotalFilter) (_ *RangeTotal, err error) {
	tx, err := s.db.Driver().Begin()
	if err != nil {
		return
	}
	defer tx.Rollback()

	var (
		firstTm, lastTm, futureTm                                       int64
		firstValue, lastValue, futureValue                              float64
		foundInPast, foundInFuture, foundFirstInRange, foundLastInRange bool
	)

	// Поиск первого показания

	// Заглядываем в прошлое на 3 минуты
	firstTm, firstValue, foundInPast, err = s.findLastInRange(tx, req.MetricID, req.Since-3*60, req.Since)
	if err != nil {
		return
	}

	if !foundInPast {
		//
		firstTm, firstValue, foundFirstInRange, err = s.findFirstInRange(tx, req.MetricID, req.Since, req.Until)
		if err != nil {
			return
		}

		if !foundFirstInRange {
			// Начало периода не найдено - значит нет показаний для указанного периода
			return
		}
	}

	// Поиск последнего показания

	lastTm, lastValue, foundLastInRange, err = s.findLastInRange(tx, req.MetricID, req.Since, req.Until)
	if err != nil {
		return
	}

	if req.CanPeriodEndsInFuture {
		// Если можем искать конец в периода в будущем
		if foundLastInRange {
			if lastTm > (req.Until - 3*60) {
				// Если lastTm в последние 3 минуты периода - период корректно завершен
				return &RangeTotal{
					Since: firstTm,
					Until: lastTm,
					Value: lastValue,
					Total: lastValue - firstValue,
				}, nil
			}
		}
		// Период не завершен в последние 3 минуты периода, поэтому ищем в будущем
		futureTm, futureValue, foundInFuture, err = s.findFirstAfter(tx, req.MetricID, req.Until)
		if err != nil {
			return
		}

		if foundInFuture {
			return &RangeTotal{
				Since: firstTm,
				Until: futureTm,
				Value: futureValue,
				Total: futureValue - firstValue,
			}, nil
		} else {
			// Супер важная проверка, ибо возможна ситуация, когда когда нашли начало
			// текущего периода в конце предыдущего (foundInPast). А это показание было
			// последним в БД.
			if foundLastInRange {
				// Последнее показание внутри периода нашли, неважно когда оно пришло - нам
				// подходит.
				return &RangeTotal{
					Since: firstTm,
					Until: lastTm,
					Value: lastValue,
					Total: lastValue - firstValue,
				}, nil
			}
			// Последнее показание внутри периода не найдено, а значит их не было вообще.
			// Просто выходим
			return
		}
	} else {
		// В будущем искать нельзя

		// Супер важная проверка, ибо возможна ситуация, когда когда нашли начало
		// текущего периода в конце предыдущего (foundInPast). А это показание было
		// последним в БД.
		if foundLastInRange {
			// Последнее показание внутри периода нашли, неважно когда оно пришло - нам
			// подходит.
			return &RangeTotal{
				Since: firstTm,
				Until: lastTm,
				Value: lastValue,
				Total: lastValue - firstValue,
			}, nil
		}
		// Последнее показание внутри периода не найдено, а значит их не было вообще.
		// Просто выходим
		return
	}
}
*/

func (s *MeasureQueryEngine) findLastInRangeTx(tx *sql.Tx, metricID int64, since, until int64) (tm int64, value float64, found bool, err error) {
	err = tx.QueryRow(`
	SELECT tm, value, true
	FROM f64
	WHERE metricID=? AND tm >= ? AND tm < ?
	ORDER BY tm DESC
	LIMIT 1`,
		metricID, since, until).Scan(&tm, &value, &found)

	if err != nil {
		if err != sql.ErrNoRows {
			return
		}
		// Нет записей
		err = nil
	}
	return
}

func (s *MeasureQueryEngine) findFirstInRangeTx(tx *sql.Tx, metricID int64, since, until int64) (tm int64, value float64, found bool, err error) {
	err = tx.QueryRow(`
	SELECT tm, value, true
	FROM f64
	WHERE metricID=? AND tm >= ? AND tm < ?
	ORDER BY tm ASC
	LIMIT 1`,
		metricID, since, until).Scan(&tm, &value, &found)

	if err != nil {
		if err != sql.ErrNoRows {
			return
		}
		// Нет записей
		err = nil
	}
	return
}

func (s *MeasureQueryEngine) findFirstAfterTx(tx *sql.Tx, metricID int64, since int64) (tm int64, value float64, found bool, err error) {
	err = tx.QueryRow(`
	SELECT tm, value, true
	FROM f64
	WHERE metricID=? AND tm >= ?
	ORDER BY tm ASC
	LIMIT 1`,
		metricID, since).Scan(&tm, &value, &found)

	if err != nil {
		if err != sql.ErrNoRows {
			return
		}
		// Нет записей
		err = nil
	}
	return
}
