package http

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

	"time"

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

// webappRoute — чем в журнале обозначается всякий путь, отданный приложению.
const webappRoute = "<приложение>"

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

type journalKey struct{}

// noteWebappOutcome оставляет исход раздачи слою журнала.
func noteWebappOutcome(r *http.Request, outcome string) {
	if note, ok := r.Context().Value(journalKey{}).(*journalNote); ok {
		note.outcome = outcome
	}
}

// Journal пишет строку о каждом входящем запросе.
//
// **Путь, которым распоряжается спрашивающий, в журнал не идёт** — вместо него
// маршрут из закрытого перечня и длина: по ним видно, что происходит, а
// дословная запись сделала бы журнал местом, куда аноним пишет свой текст
// произвольной длины. Корень приложения от этого правила не изъят: путь под ним
// выбирает тот же спрашивающий.
//
// Журнал у сервиса **один**: второй, куда чужая библиотека клала путь целиком
// вместе с адресом отправителя, ушёл вместе с ней.
func Journal(mounts []Mount, 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) {
			start := clock.Start()

			note := &journalNote{}
			ctx := context.WithValue(r.Context(), journalKey{}, note)

			recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
			next.ServeHTTP(recorder, r.WithContext(ctx))

			level := slog.LevelInfo
			if IsObservationAddress(mounts, r.URL.Path) {
				// Опрос здоровья и метрик идёт постоянно и полезного не несёт.
				level = slog.LevelDebug
			}

			attrs := []any{
				"http.method", r.Method,
				"http.route", JournalRoute(r, mounts),
				"http.status_code", recorder.status,
				"duration_ms", time.Since(start).Milliseconds(),
				// Длина идёт на всякую строку, а не только на раздачу
				// приложения: обобщённое значение маршрута теперь достаётся и
				// путям под корнем приложения, и без длины по строке не видно,
				// спросили короткий адрес или мегабайт текста.
				"http.path_length", len(r.URL.Path),
				"transport", "http",
			}

			if note.outcome != "" {
				attrs = append(attrs, "webapp.outcome", note.outcome)
			}

			logger.Log(ctx, level, "Incoming request", attrs...)
		})
	}
}

// JournalRoute готовит путь запроса к записи в журнал.
//
// **Дословно в журнал идёт только то, что принадлежит закрытому перечню**:
// точные адреса наблюдения и образцы адресов приложения. Всё прочее — и путь
// вне корней сервиса, и путь под корнем приложения, не совпавший ни с одним
// образцом, — обозначается одним значением, а длина уходит отдельным полем.
//
// Прежнее правило судило по принадлежности пути сервису и оставляло зазор
// шириной в корень приложения: `/app/<произвольный текст>` принадлежит сервису,
// отвечает отказом неузнанному и при этом уезжал в журнал дословно. Множеством
// значений под корнем распоряжается спрашивающий ровно так же, как и вне его, —
// значит и правило одно на обе половины.
//
// Идентификатор записи из строки при этом не пропадает: его пишет обработчик
// полем `record_id`, и пишет он тот, что прочитал, а не тот, что попросили.
//
// Имя файла в хранилище из журнала выводимо быть не должно, и сегодня оно туда
// не попадает по построению: адрес копии записи назван идентификатором самой
// записи, а имя файла на диске в путь не входит вовсе.
func JournalRoute(r *http.Request, mounts []Mount) string {
	if exact, ok := ExactAddressOf(mounts, r.URL.Path); ok {
		return exact
	}

	if pattern, ok := appRoutePatternOf(r); ok {
		return pattern
	}

	return webappRoute
}

// appRouteIndex — маршрутизатор, заведённый ради одного вопроса: какому образцу
// приложения отвечает этот запрос.
//
// Маршрутизатор, а не свой разбор пути: образец `{id}` разбирает стандартная
// библиотека, и второй разбор рядом с ней разошёлся бы с настоящей
// маршрутизацией молча. Образцы берутся тем же перечнем, которым вешаются
// обработчики.
var appRouteIndex = newAppRouteIndex()

func newAppRouteIndex() *http.ServeMux {
	mux := http.NewServeMux()
	for _, pattern := range AppRoutePatterns {
		mux.Handle(pattern, http.NotFoundHandler())
	}
	return mux
}

// knownAppRoutes — тот же перечень множеством: ответ маршрутизатора сверяется с
// ним. Перенаправление на очищенный путь маршрутизатор отдаёт образцом,
// собранным из самого пути, и без сверки такой ответ уехал бы в журнал
// дословно — то есть ровно тем, чего правило не допускает.
var knownAppRoutes = knownAppRouteSet()

func knownAppRouteSet() map[string]struct{} {
	out := make(map[string]struct{}, len(AppRoutePatterns))
	for _, pattern := range AppRoutePatterns {
		out[pattern] = struct{}{}
	}
	return out
}

// appRoutePatternOf называет образец адреса приложения. Второе значение ложно у
// всего, что ни одному образцу не отвечает.
func appRoutePatternOf(r *http.Request) (string, bool) {
	_, pattern := appRouteIndex.Handler(r)
	if _, ok := knownAppRoutes[pattern]; !ok {
		return "", false
	}
	return pattern, true
}

// statusRecorder запоминает код ответа: журнал пишется после обработчика, а
// готовый код читать больше неоткуда.
type statusRecorder struct {
	http.ResponseWriter
	status  int
	written bool
}

func (w *statusRecorder) WriteHeader(status int) {
	if !w.written {
		w.status = status
		w.written = true
	}
	w.ResponseWriter.WriteHeader(status)
}

func (w *statusRecorder) Write(p []byte) (int, error) {
	w.written = true
	return w.ResponseWriter.Write(p)
}
