package replay

import (
	"context"
	"errors"

	"git.vakhrushev.me/av/healthlog/internal/fold"
	"git.vakhrushev.me/av/healthlog/internal/hae"
	"git.vakhrushev.me/av/healthlog/internal/store"
)

// Outcome — исход свёртки: одной доставки или их последовательности.
//
// Классы разведены потому, что читаются по-разному. FailedLayer — штатный исход
// (слой не выводится, таких тел в журнале заведомо есть), FailedMalformed —
// содержимое не разбирается, Deferred — работа не сделана по обстоятельствам,
// и доставка осталась в очереди. Только FailedOther означает, что что-то не так
// с самой свёрткой. Один общий счётчик отправлял бы человека искать дефект там,
// где его нет.
type Outcome struct {
	Folded          int
	FailedLayer     int
	FailedMalformed int
	// Deferred — доставка осталась `pending`: отмена или занятость базы. Не
	// отказ доставки, а несделанная работа; её подберёт следующий проход.
	Deferred    int
	FailedOther int
	// Partial — доставок, в теле которых остались непокрытые разбором секции.
	// Не отклонение, а половина потока; названо потому, что именно эти тела
	// ретеншену трогать нельзя.
	Partial int
	// Incomparable — столкновений с несравнимыми наборами полей. На живом потоке
	// они единичны, и на этом стоит отказ от объединения полей.
	Incomparable int
	// PointsHeld — точек, удержанных правилом полноты против пришедшей
	// доставки. Здесь по той же причине, что EntitiesHeld: сходимость
	// отпечатка удержание не проверяет ПО ПОСТРОЕНИЮ — живой приём и пересборка
	// пользуются одним правилом и одинаково сойдутся на одинаково удержанной
	// точке. То есть слишком строгое правило полноты выглядело бы идеальной
	// сходимостью.
	PointsHeld int
	// PointsErased — точек, чьё содержание унесла пришедшая победительница.
	// Здесь по той же причине, что PointsHeld, и с большим весом: это
	// единственное направление, в котором правило слияния теряет содержание.
	PointsErased int
	// EntitiesHeld — версий сущностей, удержанных правилом «не теряем
	// содержания». Без него правило слияния сущностей проверить нечем:
	// сходимость отпечатка его не проверяет ПО ПОСТРОЕНИЮ — живой приём и
	// пересборка пользуются одним правилом и одинаково сойдутся на одинаково
	// удержанной версии. То есть слишком строгое правило (замораживающее
	// тренировку на старой версии) выглядело бы идеальной сходимостью.
	EntitiesHeld int
	// EntitiesDiverging — версии одного ключа, приехавшие в одном теле с разным
	// содержанием. Событие другого рода, чем удержание, и считается отдельно:
	// смешанное число не отвечало бы ни на один из двух вопросов.
	EntitiesDiverging int
}

// Add накапливает исход одной доставки в общий.
func (o *Outcome) Add(other Outcome) {
	o.Folded += other.Folded
	o.FailedLayer += other.FailedLayer
	o.FailedMalformed += other.FailedMalformed
	o.Deferred += other.Deferred
	o.FailedOther += other.FailedOther
	o.Partial += other.Partial
	o.Incomparable += other.Incomparable
	o.PointsHeld += other.PointsHeld
	o.PointsErased += other.PointsErased
	o.EntitiesHeld += other.EntitiesHeld
	o.EntitiesDiverging += other.EntitiesDiverging
}

// classify раскладывает ошибку свёртки по классам исхода.
//
// Чистая функция, и это не украшение: она и есть та половина, которую задача
// требовала не дублировать между пересборкой и фоновым воркером, — а
// проверяется она перебором классов, без базы и без архива.
//
// Неэкспортируемая намеренно: её результат содержит поля `Partial` и
// `Incomparable`, `EntitiesHeld` и `EntitiesDiverging`, которые дописывает только Play, — вторая публичная дверь
// молча занижала бы именно тот счётчик, по которому принимается решение о
// судьбе тела в архиве.
func classify(err error) Outcome {
	var out Outcome
	switch {
	case err == nil:
		out.Folded++
	case store.Transient(err):
		// Статус доставки свёртка в этих случаях не трогает: она осталась
		// `pending` и будет свёрнута снова. Правило одно на обоих — то, по
		// которому свёртка решает не писать исход.
		out.Deferred++
	case errors.Is(err, hae.ErrLayerUnknown):
		out.FailedLayer++
	case errors.Is(err, hae.ErrMalformed):
		out.FailedMalformed++
	default:
		out.FailedOther++
	}
	return out
}

// Player сворачивает доставку по идентификатору и классифицирует исход.
//
// Общий и для пересборки журнала, и для фонового воркера приёма — второй
// классификатор разошёлся бы с первым молча, а по одному из его счётчиков
// (`Partial`) принимается решение о судьбе тела в архиве.
type Player struct {
	Fold *fold.Service
}

// Play сворачивает одну доставку и возвращает её исход.
//
// Классифицируется ТОЛЬКО ошибка свёртки: на контекст Play не смотрит, и это
// существенно. У двух вызывающих отменённый контекст означает противоположное —
// у пересборки в свёртку уходит тот же отменяемый контекст («нас остановили»),
// у воркера отвязанный от остановки, с собственным дедлайном («доставка не
// уложилась в бюджет»). Решение «работу прекратили снаружи» принимает цикл,
// каждый по своему контексту.
func (p Player) Play(ctx context.Context, deliveryID string) (Outcome, error) {
	st, err := p.Fold.Fold(ctx, deliveryID)
	out := classify(err)

	if err == nil {
		// Счётчики читаются только у успешной свёртки: при ошибке поля Stats
		// заполнены частично (Uncovered у отказавшего разбора всегда пуст, хотя
		// в базу список записан) — и Partial молча занижался бы. А по нему
		// принимается решение о ретеншене тел.
		if len(st.Uncovered) > 0 {
			out.Partial++
		}
		out.Incomparable += st.Incomparable
		out.PointsHeld += st.PointsHeld
		out.PointsErased += st.PointsErased
		out.EntitiesHeld += st.EntitiesHeld
		out.EntitiesDiverging += st.EntitiesDiverging
	}
	return out, err
}
