package auth

import (
	"context"
	"net/http"
	"strings"
)

// CtxSessionKey is the request-context key under which Middleware stashes
// the validated Session.
type CtxSessionKey struct{}

// SessionFromContext returns the Session previously stashed by Middleware,
// or nil if the request did not pass through Middleware.
func SessionFromContext(ctx context.Context) *Session {
	s, _ := ctx.Value(CtxSessionKey{}).(*Session)
	return s
}

// Middleware enforces that the request carries a valid session cookie.
// Exempt paths are checked by prefix and exact-match; everything else gets
// 401 on missing or invalid session.
//
// Bootstrap exception: the WebAuthn register endpoints are exempt here
// because their handlers do their own check ("is there an active session OR
// is the credential set empty?"). Putting the bootstrap logic in the
// handler keeps the middleware uniform.
type Middleware struct {
	sessions *SessionStore

	exemptPrefix []string // applied with strings.HasPrefix
	exemptExact  []string // applied with ==
}

// NewMiddleware returns a Middleware with the documented exemption list.
// The exemption list is closed: the auth handlers themselves cannot extend
// it, and any new public endpoint must be added here explicitly.
func NewMiddleware(sessions *SessionStore) *Middleware {
	return &Middleware{
		sessions: sessions,
		exemptExact: []string{
			"/api/system/health",
			"/api/auth/status",
			"/api/auth/webauthn/register/start",
			"/api/auth/webauthn/register/finish",
			"/api/auth/webauthn/login/start",
			"/api/auth/webauthn/login/finish",
			"/api/auth/recovery",
			// Machine-to-machine disk-usage ingress from the Feral du-cron;
			// authenticated by its own CC_DISK_TOKEN bearer, not a session.
			"/api/disk/report",
			// Machine-to-machine port-sync ingress from the home-PC Proton
			// script; authenticated by its own CC_PORTSYNC_TOKEN bearer.
			// (GET /api/portsync/ stays session-gated for the dashboard.)
			"/api/portsync/report",
			"/api/portsync/status",
		},
	}
}

// gatedPrefixes are the URL-path prefixes that require a valid session.
// Anything else passes straight through (the embedded frontend, the public
// /webhook/* ingress with its own HMAC, the WebSocket at /ws).
//
// Phase 4 added /sse/ to this list so authenticated SSE streams aren't
// readable by anyone who can reach the tailnet — a defense-in-depth measure
// even given the Tailscale-only network model.
var gatedPrefixes = []string{"/api/", "/sse/"}

// Handler returns the wrapping http.Handler.
func (m *Middleware) Handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gated := false
		for _, p := range gatedPrefixes {
			if strings.HasPrefix(r.URL.Path, p) {
				gated = true
				break
			}
		}
		if !gated {
			next.ServeHTTP(w, r)
			return
		}
		if m.isExempt(r.URL.Path) {
			next.ServeHTTP(w, r)
			return
		}
		sess, err := m.sessions.Validate(r.Context(), r)
		if err != nil {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		ctx := context.WithValue(r.Context(), CtxSessionKey{}, sess)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func (m *Middleware) isExempt(path string) bool {
	for _, p := range m.exemptPrefix {
		if strings.HasPrefix(path, p) {
			return true
		}
	}
	for _, p := range m.exemptExact {
		if p == path {
			return true
		}
	}
	return false
}
