package http

import (
	"net/http"
	"net/netip"
	"strings"
	"sync"
	"time"

	"git.vakhrushev.me/av/transcriber/internal/clock"
	"git.vakhrushev.me/av/transcriber/internal/contract"
)

// Бюджет ограничителя частоты под корнем приложения.
//
// Числа скромные намеренно: сервисом пользуются единицы человек, а экран
// опрашивает карточку, пока запись идёт по конвейеру. Из них же выводится
// частота опроса, которую сервис объявляет приложению, — два числа об одном и
// том же разъехались бы при первой правке.
const (
	appRateMaxRequests = 120
	appRateWindowSec   = 60
)

// staleBudgetAge — через сколько неиспользуемый счётчик выбрасывается. Карта
// счётчиков растёт с числом адресов, и без уборки она стала бы местом, куда
// спрашивающий кладёт по строке на каждый свой адрес.
const staleBudgetAge = 10 * time.Minute

// RateLimit — свой ограничитель частоты под корнем приложения.
//
// Бюджет считается по адресу спрашивающего скользящим окном: столько-то запросов
// за столько-то секунд. Отказ уходит **нашей** формой тела — слой отвечает
// доменной ошибкой, а не телом библиотеки, и второй формы отказа на адресах
// приложения не появляется.
//
// **Адрес спрашивающего берётся из заголовка пересылки — и только тогда, когда
// соединение пришло с адреса из объявленного перечня доверенных.** Во всяком
// другом случае адресом считается адрес пира, а пришедший заголовок на ключ
// бюджета не влияет ничем.
//
// **Цепочка пересылки читается справа налево до первого недоверенного адреса.**
// Значением слева распоряжается тот, кто шлёт запрос: прокси заголовок
// дописывает, а не заменяет, и левое значение цепочки он берёт из присланного.
//
// Обе половины правила закрывают свою поломку. Сервис стоит за обратным прокси,
// и пир у него один на всех: бюджет, посчитанный по пиру, становится общим на
// весь сервис, и один спрашивающий исчерпывает его остальным. Обратная ошибка —
// верить заголовку без сверки пира — отдаёт обход ограничителя ровно тому, кого
// он ограничивает: значением пересылаемого заголовка распоряжается тот, кто шлёт
// запрос, и меняет он его на каждом запросе.
//
// **Узнавание и ограничитель берут адрес разными способами, и это намеренно.**
// Узнаванию нужен адрес самого соединения: им оно решает, верить ли заголовку
// вообще. Ограничителю нужен адрес того, кого он ограничивает, а тот за прокси в
// адресе соединения не виден вовсе.
//
// Слой стоит **перед** узнаванием: узнавание читает базу, а на новом имени ещё и
// пишет в неё; выполненное раньше ограничителя, оно работало бы на запросах,
// которые тот уже отверг, и поток отвергнутых обращений заводил бы учётные
// записи, которые потом не убираются ничем.
func RateLimit(trusted []netip.Prefix) func(http.Handler) http.Handler {
	limiter := newRateLimiter(appRateMaxRequests, appRateWindowSec*time.Second)

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !limiter.allow(clientAddress(r, trusted)) {
				fail(w, contract.ErrTooManyRequests)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// clientAddress — тот, кого ограничивают.
func clientAddress(r *http.Request, trusted []netip.Prefix) string {
	peer, ok := peerAddress(r.RemoteAddr)
	if !ok {
		// Адрес пира не разобран — ключ берётся строкой как есть: общий бюджет
		// лучше отсутствующего.
		return r.RemoteAddr
	}

	if !isTrusted(trusted, peer) {
		return peer.String()
	}

	if client, ok := forwardedClient(r, trusted); ok {
		return client
	}

	// Прокси адреса не назвал либо назвал одними доверенными: счётчик падает
	// обратно на адрес пира — то есть на общий. Лучше общий, чем ключ, который
	// выбирает сам спрашивающий.
	return peer.String()
}

// forwardedClient достаёт из цепочки пересылки того, кого ограничивают.
//
// **Цепочка читается справа налево, а доверенные адреса отбрасываются.**
// Значение слева пишет тот, кто шлёт запрос: прокси заголовок **дописывает**, а
// не заменяет, поэтому левым значением цепочки распоряжается аноним, и ключ
// бюджета, взятый оттуда, меняется у него на каждом запросе — бюджет обходится
// с первого. Справа же стоит адрес, который приписал ближайший к нам прокси, и
// каждый следующий шаг влево тем доверен, кто его приписал. Первый недоверенный
// справа и есть последний адрес, за который отвечает контур, а не спрашивающий.
//
// Заголовок читается **всеми** строками, а не одной: цепочка законно приходит
// несколькими заголовками, и разбор одной строки увидел бы только её часть — то
// есть снова кусок, которым распоряжается аноним.
func forwardedClient(r *http.Request, trusted []netip.Prefix) (string, bool) {
	hops := forwardedChain(r)

	for i := len(hops) - 1; i >= 0; i-- {
		addr, err := netip.ParseAddr(hops[i])
		if err != nil {
			// Значение, которое не разбирается адресом, доверенным считать
			// нечем: цепочка на нём кончается.
			return "", false
		}

		addr = addr.Unmap()
		if isTrusted(trusted, addr) {
			continue
		}

		return addr.String(), true
	}

	return "", false
}

// forwardedChain — цепочка пересылки одним списком: все строки заголовка по
// порядку, каждая разобрана по запятым.
func forwardedChain(r *http.Request) []string {
	var hops []string
	for _, value := range r.Header.Values(ForwardedForHeader) {
		for _, hop := range strings.Split(value, ",") {
			hop = strings.TrimSpace(hop)
			if hop != "" {
				hops = append(hops, hop)
			}
		}
	}
	return hops
}

// rateLimiter — счётчики бюджета по ключу.
//
// Окно скользящее и хранится метками времени: счётчик с обнулением по границе
// окна пропустил бы двойной бюджет на стыке двух окон.
type rateLimiter struct {
	mu      sync.Mutex
	max     int
	window  time.Duration
	budgets map[string]*budget
	swept   time.Time
}

type budget struct {
	hits []time.Time
}

func newRateLimiter(maxRequests int, window time.Duration) *rateLimiter {
	return &rateLimiter{
		max:     maxRequests,
		window:  window,
		budgets: map[string]*budget{},
		swept:   clock.Now(),
	}
}

func (l *rateLimiter) allow(key string) bool {
	now := clock.Now()

	l.mu.Lock()
	defer l.mu.Unlock()

	l.sweep(now)

	b, ok := l.budgets[key]
	if !ok {
		b = &budget{}
		l.budgets[key] = b
	}

	b.hits = fresh(b.hits, now.Add(-l.window))
	if len(b.hits) >= l.max {
		return false
	}

	b.hits = append(b.hits, now)
	return true
}

// sweep выбрасывает счётчики, которыми давно не пользовались. Идёт не чаще раза
// в окно: обход карты на каждом запросе стоил бы больше самого ограничителя.
func (l *rateLimiter) sweep(now time.Time) {
	if now.Sub(l.swept) < l.window {
		return
	}
	l.swept = now

	for key, b := range l.budgets {
		b.hits = fresh(b.hits, now.Add(-staleBudgetAge))
		if len(b.hits) == 0 {
			delete(l.budgets, key)
		}
	}
}

// fresh отбрасывает метки старше границы. Метки лежат по возрастанию, поэтому
// достаточно найти первую свежую.
func fresh(hits []time.Time, edge time.Time) []time.Time {
	for i, hit := range hits {
		if hit.After(edge) {
			return hits[i:]
		}
	}
	return nil
}
