package http

import (
	"log/slog"
	"net/http"
	"net/netip"
)

// SubstituteIdentityHeaders — слой отладочного запуска: ставит запросу заголовки
// входа значениями из настроек.
//
// # Зачем он есть
//
// В бою заголовки ставит обратный прокси, сходивший к провайдеру. На машине
// разработчика прокси нет, а браузер заголовков не ставит — и приложение
// локально не открылось бы вовсе. Слой встаёт на место контура **внутри самого
// сервиса**: второго процесса и второго порта для этого не нужно.
//
// # Чего слой не делает
//
// Он правит заголовки запроса и ничего больше: учётной записи не заводит,
// отказов не выдаёт, в контекст не пишет. Узнавание получает запрос,
// неотличимый от пришедшего с сервера, и отлаживается поэтому боевая ветка, а
// не её отладочный двойник.
//
// # Что ему запрещено
//
// Подстановка — это настройка, которой сервис называет пришедшего сам, никого
// не спросив. Ограничений поэтому три, и каждое отсекает свой способ отдать
// чужой архив:
//
//   - пустой перечень значит «подставлять нечего», и слой отдаёт цепочку
//     нетронутой: звено не встаёт в неё вовсе, а запрос доходит до узнавания
//     тем же, каким пришёл. Судится пустота здесь, и представление у предиката
//     «подставляем ли» одно на всех — непустота самого перечня. Иначе
//     включённый предохранитель при пустой имитации срезал бы `Remote-Email` у
//     запроса, пришедшего без `Remote-User`;
//   - подставляется только при **отсутствии** `Remote-User`. Пришедший заголовок
//     не подменяется и не дополняется: узнавание отвергает запрос с двумя
//     значениями, и слой, дописавший второе, превратил бы законный отказ в
//     отладочный проход;
//   - подставляется только с адреса из перечня доверенных, и судит адрес та же
//     функция, какой судит его узнавание. Второй сверщик разошёлся бы с первым
//     молча: узнавание разворачивает IPv4 в оболочке IPv6.
//
// Подставляя, слой распоряжается **всей** тройкой заголовков входа: названные
// настройками ставит их значением, не названные удаляет. Запрос без
// `Remote-User`, но с `Remote-Email` иначе собрал бы личность из двух
// источников — логин свой, почта чужая.
func SubstituteIdentityHeaders(
	headers map[string]string,
	trusted []netip.Prefix,
	logger *slog.Logger,
) func(http.Handler) http.Handler {
	// Подставлять нечего: цепочка слоёв остаётся ровно той, что в бою, —
	// следующее звено возвращается как есть, лишнего звена не появляется.
	if len(headers) == 0 {
		return func(next http.Handler) http.Handler { return next }
	}

	if logger == nil {
		logger = slog.Default()
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Решает **наличие** заголовка, а не его значение: пустое значение
			// штатно шлёт прокси там, где никого не назвал, и подстановка на
			// этом месте назвала бы человеком того, кому провайдер отказал.
			if len(r.Header.Values(LoginHeader)) > 0 {
				next.ServeHTTP(w, r)
				return
			}

			peer, ok := peerAddress(r.RemoteAddr)
			if !ok || !isTrusted(trusted, peer) {
				// Своя строка, и уровень предупреждающий. Узнавание здесь
				// молчит предупреждением: заголовка нет, и оно считает случай
				// штатным. Без этой строки самый частый локальный отказ —
				// браузер пришёл с `::1`, а перечень называет `127.0.0.1` — не
				// отличим от поломки узнавания: человек видит отказ на всём
				// приложении и ни одного следа о том, почему подстановки не
				// было.
				//
				// Подставляемых значений в строке нет: логин — ключ к чужому
				// архиву, и запрет печатать его действует на подставленное
				// значение наравне с пришедшим.
				logger.Warn("Identity headers are not substituted for an untrusted peer",
					"http.peer_addr", r.RemoteAddr,
					"capability", "access", "transport", "http")
				next.ServeHTTP(w, r)
				return
			}

			// Тройка берётся из своего единственного дома, а не перечисляется
			// здесь заново.
			for _, name := range IdentityHeaderNames() {
				if value, named := headers[name]; named {
					r.Header.Set(name, value)
				} else {
					r.Header.Del(name)
				}
			}

			// Уровень `INFO`, и боевой журнал он не топит по построению: строка
			// пишется только тогда, когда подстановка действительно работает, а
			// в бою она выключена умолчанием предохранителя. Появляется строка
			// ровно в одном месте — в локальном прогоне, где стоит на каждом
			// запросе и где она и нужна: ею «узнан подстановкой» отличается от
			// «узнан прокси». На отладочном уровне её не видел бы никто:
			// настройки под уровень журнала у сервиса нет, и он зашит `INFO`.
			//
			// Подставленных значений в строке нет по той же причине, по какой их
			// нет в строке отказа выше.
			logger.Info("Identity headers substituted from configuration",
				"http.peer_addr", r.RemoteAddr,
				"capability", "access", "transport", "http")

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

// SubstitutedHeaderNames называет заголовки, которые слой подставит, в порядке
// объявления тройки.
//
// Нужен строке журнала при старте: перебор карты дал бы порядок, меняющийся от
// запуска к запуску, а перечисление имён по месту завело бы третий список.
func SubstitutedHeaderNames(headers map[string]string) []string {
	names := make([]string, 0, len(headers))
	for _, name := range IdentityHeaderNames() {
		if _, named := headers[name]; named {
			names = append(names, name)
		}
	}

	return names
}
