package http

import (
	"context"
	"errors"
	"log/slog"
	"net"
	"net/http"
	"net/netip"

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

// Заголовки, которыми обратный прокси называет пришедшего.
//
// Имена нормативны: смена имени молча перестаёт узнавать всех, а проверка,
// которая сама ставит и сама читает своё имя, этого не замечает. Контур уже
// пишет эти имена соседним сервисам, и настройкой они не делаются: второе место,
// где их можно написать неверно, выгоды не даёт.
const (
	LoginHeader = "Remote-User"
	NameHeader  = "Remote-Name"
	EmailHeader = "Remote-Email"
)

// IdentityHeaderNames отдаёт имена заголовков входа целиком, в одном порядке.
//
// Перечисление тройки живёт здесь и только здесь. Всякий, кому нужен её состав —
// слой подстановки, проверка старта, строка журнала, — берёт его отсюда: второй
// список разошёлся бы с первым молча, а имена нормативны.
func IdentityHeaderNames() []string {
	return []string{LoginHeader, NameHeader, EmailHeader}
}

// ForwardedForHeader — заголовок, которым прокси называет адрес спрашивающего.
// Читает его только ограничитель частоты: барьером узнавания он не служит и
// служить не может — кто пришёл, решает адрес самого соединения.
const ForwardedForHeader = "X-Forwarded-For"

// accountKey — ключ, под которым узнанная учётная запись живёт в контексте
// запроса. Свой тип, а не строка: чужой ключ с тем же текстом иначе перезаписал
// бы значение.
type accountKey struct{}

// AccountOf отдаёт учётную запись, от имени которой идёт запрос. Второе значение
// ложно у неузнанного.
func AccountOf(r *http.Request) (*contract.UserAccount, bool) {
	account, ok := r.Context().Value(accountKey{}).(*contract.UserAccount)
	return account, ok
}

// withAccount кладёт узнанную учётную запись в контекст запроса.
func withAccount(r *http.Request, account *contract.UserAccount) *http.Request {
	return r.WithContext(context.WithValue(r.Context(), accountKey{}, account))
}

// TrustedHeaderIdentity узнаёт пришедшего по заголовку доверенного источника.
//
// # Область
//
// Слой вешается на цепочку корня приложения и только на неё. Область поэтому
// выводится из объявленного адресного пространства сервиса, а не перечисляется
// вторым списком: корень, переехавший в перечне, уносит слой с собой.
//
// Сужение закрывает вещь, которая от смены хранилища не зависит: узнавание не
// срабатывает на пробе здоровья, на метриках и на ресурсах приложения. Иначе
// запрос за каждой картинкой стоил бы обращения к базе, а первый такой запрос с
// новым именем — записи в неё.
//
// # Чего слой не делает
//
// Отказа он не выдаёт: отказ приходит там, где приходил всегда, — требованием
// учётной записи. Исключение одно — отказ базы: он кончается отказом сервиса, а
// не молчаливым проходом неузнанным, иначе человек увидел бы отказ входа там,
// где легла база.
func TrustedHeaderIdentity(
	users contract.UserRepository,
	trusted []netip.Prefix,
	logger *slog.Logger,
) func(http.Handler) http.Handler {
	if logger == nil {
		logger = slog.Default()
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Более одного значения — не выбор, а отказ. Прокси, настроенный
			// добавлять заголовок вместо замены, оставляет рядом со своим
			// значением присланное анонимом, и умолчание «берём первое» отдало
			// бы вход анониму.
			values := r.Header.Values(LoginHeader)
			if len(values) != 1 {
				if len(values) > 1 {
					// Уровень предупреждающий: два значения означают прокси,
					// который заголовок **добавляет** вместо замены, — то есть
					// ровно ту поломку контура, которую модель угроз называет
					// главной. Отладочным уровнем она в бою не видна вовсе.
					logger.Warn("Request carries more than one login header",
						"http.peer_addr", r.RemoteAddr,
						"capability", "access", "transport", "http")
				} else {
					// Заголовка нет вовсе. В этом контуре это значит, что прокси
					// никого не назвал; строка отладочная, потому что случай
					// штатный — так выглядит и человек, которому провайдер
					// отказал.
					logger.Debug("Request carries no login header",
						"http.peer_addr", r.RemoteAddr,
						"capability", "access", "transport", "http")
				}
				next.ServeHTTP(w, r)
				return
			}

			peer, ok := peerAddress(r.RemoteAddr)
			if !ok || !isTrusted(trusted, peer) {
				// Уровень предупреждающий, а не отладочный, и это решение о
				// цене. Заголовок с недоверенного адреса в этом контуре — не
				// рутина: контейнер портов наружу не публикует, снаружи всё
				// приходит прокси, то есть с доверенного адреса. Значит либо
				// перечень задан неверно, либо кто-то оказался внутри сети — и
				// то и другое владелец обязан увидеть.
				//
				// Значение заголовка при этом в журнал не идёт: оно целиком
				// задаётся тем, кто шлёт запрос. Адрес пира идёт — по нему
				// видно, чья это поломка: своя (перечень) или контура (прокси
				// заголовка не ставит).
				logger.Warn("Login header came from an untrusted peer",
					"http.peer_addr", r.RemoteAddr,
					"capability", "access", "transport", "http")
				next.ServeHTTP(w, r)
				return
			}

			account, created, err := users.EnsureUser(contract.Identity{
				Login: values[0],
				Name:  r.Header.Get(NameHeader),
				Email: r.Header.Get(EmailHeader),
			})
			if err != nil {
				if errors.Is(err, contract.ErrLoginNotAcceptable) {
					// Негодный логин — это негодный ввод, а не поломка сервиса:
					// пустой заголовок прокси шлёт штатно там, где никого не
					// назвал. Уровень поэтому отладочный, и запрос идёт дальше
					// неузнанным.
					logger.Debug("Login header value is not acceptable",
						"http.peer_addr", r.RemoteAddr,
						"capability", "access", "transport", "http")
					next.ServeHTTP(w, r)
					return
				}

				logger.Error("Failed to resolve account by login header",
					"error", err, "capability", "access", "transport", "http")
				fail(w, err)
				return
			}

			if created {
				// Заведение учётной записи — событие, и владелец обязан его
				// видеть: иначе «никто не заходил» неотличимо от «завелось
				// двадцать», а прокси, пропустивший чужой заголовок, не
				// оставляет следа вовсе. Убрать заведённую запись потом нечем —
				// учётная запись с записями не удаляется.
				//
				// Значение заголовка в строку не идёт: им довольно назваться,
				// чтобы стать этим человеком. Идут адрес пира и идентификатор
				// записи — оба выданы не спрашивающим.
				logger.Info("Account created from login header",
					"http.peer_addr", r.RemoteAddr,
					"account_id", account.ID,
					"capability", "access", "transport", "http")
			}

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

// RequireUser — слой предъявления адресов приложения.
//
// Отказ наступает **до чтения тела**: запись, за которую не заплатит узнанный
// отправитель, не должна попасть даже в память, а позже пришлось бы убирать уже
// уложенный файл — чего сервис не умеет вовсе.
//
// Ветви «узнан, а учётной записи нет» здесь больше нет: узнавание заводит
// учётную запись само, и предъявителя без неё не бывает.
func RequireUser() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if _, ok := AccountOf(r); !ok {
				fail(w, contract.ErrUnauthorized)
				return
			}

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

// peerAddress достаёт адрес того, кто открыл соединение.
//
// Берётся именно он, а не пересылаемый заголовок: значением пересылаемого
// распоряжается тот, кто шлёт запрос, и барьер, подделываемый той же строкой,
// которой он обходится, не барьер вовсе.
func peerAddress(remoteAddr string) (netip.Addr, bool) {
	host, _, err := net.SplitHostPort(remoteAddr)
	if err != nil {
		// Адрес без порта — законная форма у некоторых слушателей.
		host = remoteAddr
	}

	addr, err := netip.ParseAddr(host)
	if err != nil {
		return netip.Addr{}, false
	}

	// Адрес IPv4, приехавший в оболочке IPv6, сверяется с перечнем как IPv4:
	// иначе `127.0.0.1` в перечне не совпал бы с `::ffff:127.0.0.1` у пира.
	return addr.Unmap(), true
}

func isTrusted(trusted []netip.Prefix, peer netip.Addr) bool {
	for _, network := range trusted {
		if network.Contains(peer) {
			return true
		}
	}

	return false
}
