package api

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

	"github.com/ponzischeme89/memby/server/internal/store"
)

// requestIdentity is the answer to "who did this, from where, on what build" — the
// context every log line from a request needs and no single layer holds. The middleware
// knows the route and the client headers before the session exists; [Server.authed]
// learns the viewer and the television afterwards.
//
// It is carried by pointer through the request context so the outermost middleware can
// still read what an inner layer filled in. A request is served on one goroutine and the
// handler has returned by the time the middleware reads this, so no lock is needed.
type requestIdentity struct {
	component string
	user      string
	device    string
	client    string
	protocol  string
}

type identityKey struct{}

// withRequestIdentity installs an empty identity for this request and returns it.
func withRequestIdentity(r *http.Request) (*http.Request, *requestIdentity) {
	identity := &requestIdentity{
		component: componentFor(r.URL.Path),
		client:    clientVersion(r),
		protocol:  clientProtocol(r),
	}
	return r.WithContext(context.WithValue(r.Context(), identityKey{}, identity)), identity
}

func identityFrom(ctx context.Context) *requestIdentity {
	identity, _ := ctx.Value(identityKey{}).(*requestIdentity)
	return identity
}

// identify records who the request turned out to belong to, so every later line — the
// handler's own events and the request line the middleware writes at the end — names the
// viewer and the television rather than a token.
func identify(ctx context.Context, sess store.Session) {
	identity := identityFrom(ctx)
	if identity == nil {
		return
	}
	if sess.Username != "" {
		identity.user = sess.Username
	}
	if sess.DeviceName != "" {
		identity.device = sess.DeviceName
	} else if sess.DeviceID != "" {
		identity.device = sess.DeviceID
	}
	if sess.ClientVersion != "" {
		identity.client = sess.ClientVersion
	}
	if sess.ClientProtocol != "" {
		identity.protocol = sess.ClientProtocol
	}
}

func (i *requestIdentity) attrs() []any {
	if i == nil {
		return nil
	}
	attrs := make([]any, 0, 8)
	if i.component != "" {
		attrs = append(attrs, "component", i.component)
	}
	attrs = append(attrs, i.viewerAttrs()...)
	if i.client != "" {
		attrs = append(attrs, "client", i.client)
	}
	return attrs
}

// viewerAttrs names the person and the television, and only when they are known: an
// unauthenticated probe has neither, and "user=unknown" on every health check is noise.
func (i *requestIdentity) viewerAttrs() []any {
	if i == nil {
		return nil
	}
	attrs := make([]any, 0, 4)
	if i.user != "" {
		attrs = append(attrs, "user", i.user)
	}
	if i.device != "" {
		attrs = append(attrs, "device", i.device)
	}
	return attrs
}

// loggerFor returns the request's logger: the server logger with the viewer, television,
// app build and area of the app already attached. Handlers use it so an event only has
// to say what happened, and every event from one request is attributable without the
// reader correlating lines by hand.
//
// Outside a request — a scheduled sync, a health probe — it degrades to the plain server
// logger rather than refusing to log.
func (s *Server) loggerFor(ctx context.Context) *slog.Logger {
	attrs := identityFrom(ctx).attrs()
	if len(attrs) == 0 {
		return s.log
	}
	return s.log.With(attrs...)
}

// componentFor names the part of the app a request came from.
//
// It is derived from the route rather than declared by the client: the TV would have to
// thread a surface name through every repository call to report one, and the route
// already identifies the screen unambiguously — /v1/home is the launcher, an image is
// artwork for something already on screen, /v1/items/{id}/playback is the player asking
// what to play. Deriving it also means an old APK's traffic is attributed correctly.
func componentFor(path string) string {
	switch {
	case path == "/healthz", path == "/readyz":
		return "health"
	case path == "/v1/status":
		return "status"
	case path == "/v1/update", strings.HasPrefix(path, "/updates/"):
		return "updates"
	case strings.HasPrefix(path, "/v1/auth/devices"):
		return "devices"
	case strings.HasPrefix(path, "/v1/auth/"):
		return "auth"
	case path == "/v1/home", path == "/v1/features":
		return "home"
	case path == "/v1/preferences", path == "/v1/theme":
		return "settings"
	case path == "/v1/screensaver", path == "/v1/preroll":
		return "screensaver"
	case strings.HasPrefix(path, "/v1/search"), strings.HasPrefix(path, "/v1/genres/"), path == "/v1/library/items":
		return "search"
	case strings.HasPrefix(path, "/v1/requests"):
		return "requests"
	case strings.HasPrefix(path, "/v1/recommendations"), path == "/v1/for-you":
		return "recommendations"
	case strings.HasPrefix(path, "/v1/my-shows"), strings.HasPrefix(path, "/v1/notifications"):
		return "my-shows"
	case strings.HasPrefix(path, "/v1/playback/"), isPlaybackItemPath(path):
		return "playback"
	case strings.HasPrefix(path, "/v1/images/"):
		return "artwork"
	case strings.HasPrefix(path, "/v1/items/"):
		return "details"
	case strings.HasPrefix(path, "/v1/analytics/"):
		return "analytics"
	case strings.HasPrefix(path, "/admin"):
		return "admin"
	case strings.HasPrefix(path, "/hooks/"):
		return "webhooks"
	case strings.HasPrefix(path, "/install"), path == "/":
		return "installer"
	default:
		return "api"
	}
}

// The player's own calls hang off an item, so they are told apart from the detail page by
// their trailing segment rather than their prefix.
func isPlaybackItemPath(path string) bool {
	if !strings.HasPrefix(path, "/v1/items/") {
		return false
	}
	// Fetching a subtitle is two segments deep rather than one, and matching its trailing
	// "search" on its own would claim any future per-item search as playback. A seek
	// preview is the same shape: the frame number is the last segment, not the word.
	if strings.Contains(path, "/subtitles/") || strings.Contains(path, "/trickplay") {
		return true
	}
	switch path[strings.LastIndex(path, "/")+1:] {
	case "playback", "next", "trailer":
		return true
	}
	return false
}
