package api

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"

	"github.com/coder/websocket"
)

// NewLiveKitProxy creates a reverse proxy handler that forwards both HTTP
// and WebSocket requests to the LiveKit server. This allows the client to
// reach LiveKit through OwnCord's existing HTTPS server, avoiding
// mixed-content blocks in WebView2 (secure page → insecure WebSocket).
//
// The client connects to wss://server:8443/livekit/ which is proxied to
// ws://localhost:7880/ on the LiveKit server.
func NewLiveKitProxy(livekitURL string, allowedOrigins []string) http.Handler {
	target, err := url.Parse(livekitURL)
	if err != nil {
		slog.Error("invalid LiveKit URL — falling back to localhost:7880",
			"url", livekitURL, "error", err)
		target, _ = url.Parse("http://localhost:7880")
	}

	// Normalise scheme for HTTP proxy target.
	httpTarget := *target
	switch httpTarget.Scheme {
	case "ws":
		httpTarget.Scheme = "http"
	case "wss":
		httpTarget.Scheme = "https"
	}

	// Normalise scheme for WebSocket proxy target.
	wsTarget := *target
	switch wsTarget.Scheme {
	case "http":
		wsTarget.Scheme = "ws"
	case "https":
		wsTarget.Scheme = "wss"
	}

	httpProxy := &httputil.ReverseProxy{
		Director: func(req *http.Request) {
			req.URL.Scheme = httpTarget.Scheme
			req.URL.Host = httpTarget.Host
			req.Host = httpTarget.Host
		},
	}

	// Paths that must never be forwarded to LiveKit (internal/admin endpoints).
	// Matched as exact path segments to avoid false positives (e.g. "/user-metrics").
	blockedSegments := map[string]bool{"admin": true, "metrics": true, "debug": true, "twirp": true}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Enforce the path allowlist and Origin check for EVERY request,
		// including WebSocket upgrades — otherwise a client could reach a
		// blocked/admin endpoint simply by sending an Upgrade header.

		// Block sensitive LiveKit endpoints (exact segment match).
		for seg := range strings.SplitSeq(strings.ToLower(r.URL.Path), "/") {
			if blockedSegments[seg] {
				writeJSON(w, http.StatusForbidden, errorResponse{
					Error:   "FORBIDDEN",
					Message: "access denied",
				})
				return
			}
		}

		// Validate Origin header (mirrors WS OriginPatterns).
		if !isOriginAllowed(r, allowedOrigins) {
			writeJSON(w, http.StatusForbidden, errorResponse{
				Error:   "FORBIDDEN",
				Message: "access denied",
			})
			return
		}

		// Detect WebSocket upgrade requests.
		if isWebSocketUpgrade(r) {
			proxyWebSocket(w, r, &wsTarget, allowedOrigins)
			return
		}

		httpProxy.ServeHTTP(w, r)
	})
}

func isWebSocketUpgrade(r *http.Request) bool {
	for _, v := range r.Header.Values("Connection") {
		if strings.EqualFold(strings.TrimSpace(v), "upgrade") {
			return strings.EqualFold(r.Header.Get("Upgrade"), "websocket")
		}
	}
	return false
}

// isOriginAllowed checks whether the request's Origin header matches one of the
// allowed origins. Requests with no Origin header (e.g. same-origin or non-browser)
// are permitted. An empty allowedOrigins list denies all cross-origin requests
// (require explicit "*" wildcard to allow all).
func isOriginAllowed(r *http.Request, allowedOrigins []string) bool {
	origin := r.Header.Get("Origin")
	if origin == "" {
		return true // non-browser or same-origin requests
	}
	if len(allowedOrigins) == 0 {
		return false // no allowlist configured — deny cross-origin
	}
	for _, pattern := range allowedOrigins {
		if pattern == "*" {
			return true
		}
		if strings.EqualFold(origin, pattern) {
			return true
		}
	}
	return false
}

// proxyWebSocket opens a backend WS connection and shovels data in both
// directions until either side closes.
func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL, allowedOrigins []string) {
	// Build backend URL preserving the request path and query.
	backendURL := *target
	backendURL.Path = r.URL.Path
	backendURL.RawQuery = r.URL.RawQuery

	// Connect to LiveKit backend.
	backConn, dialResp, err := websocket.Dial(r.Context(), backendURL.String(), &websocket.DialOptions{
		Subprotocols: r.Header.Values("Sec-WebSocket-Protocol"),
	})
	if dialResp != nil && dialResp.Body != nil {
		defer dialResp.Body.Close() //nolint:errcheck // best-effort close
	}
	if err != nil {
		// websocket.Dial wraps a *url.Error, which embeds the full request URL —
		// including the access_token query parameter, a live LiveKit room-join
		// JWT. Anyone with log access (stdout, a shipper, or the admin panel's
		// live view, whose ring buffer captures DEBUG+ regardless of the
		// configured stdout level) could replay it inside its 5-minute TTL as
		// the victim's participant identity. Strip the credential before the
		// error reaches slog: the raw query blob first, so an encoded form is
		// caught too, then the decoded token.
		safeErr := redactKey(err.Error(), backendURL.RawQuery)
		safeErr = redactKey(safeErr, backendURL.Query().Get("access_token"))
		slog.Warn("livekit proxy: backend dial failed", "host", backendURL.Host, "path", backendURL.Path, "err", safeErr)
		writeJSON(w, http.StatusBadGateway, errorResponse{
			Error:   "BAD_GATEWAY",
			Message: "backend unavailable",
		})
		return
	}
	defer backConn.Close(websocket.StatusNormalClosure, "") //nolint:errcheck // best-effort close on defer

	// Accept the frontend WebSocket.
	frontConn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
		Subprotocols:   []string{backConn.Subprotocol()},
		OriginPatterns: allowedOrigins,
	})
	if err != nil {
		slog.Warn("livekit proxy: frontend accept failed", "err", err)
		return
	}
	defer frontConn.Close(websocket.StatusNormalClosure, "") //nolint:errcheck // best-effort close on defer

	// Use a cancellable context so when one direction finishes, the other
	// goroutine's copyWS read/write is unblocked and can drain cleanly.
	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	errc := make(chan error, 2)

	// Frontend → Backend
	go func() {
		errc <- copyWS(ctx, backConn, frontConn)
	}()

	// Backend → Frontend
	go func() {
		errc <- copyWS(ctx, frontConn, backConn)
	}()

	// Wait for either direction to finish, then cancel+drain both.
	<-errc
	cancel()
	<-errc
}

// wsProxyMaxMessageSize is the maximum WebSocket message size the LiveKit
// proxy will forward. Messages exceeding this are dropped to prevent OOM.
// 256 KB is generous for LiveKit signaling (typically < 10 KB).
const wsProxyMaxMessageSize = 256 * 1024

// copyWS reads messages from src and writes them to dst until an error or
// context cancellation. H-5: Messages exceeding wsProxyMaxMessageSize are
// rejected to prevent memory exhaustion via oversized frames.
func copyWS(ctx context.Context, dst, src *websocket.Conn) error {
	for {
		msgType, reader, err := src.Reader(ctx)
		if err != nil {
			return err
		}
		// Wrap reader with a size limit to prevent OOM from oversized messages.
		limited := io.LimitReader(reader, wsProxyMaxMessageSize+1)
		writer, err := dst.Writer(ctx, msgType)
		if err != nil {
			return err
		}
		n, copyErr := io.Copy(writer, limited)
		if copyErr != nil {
			return copyErr
		}
		if n > wsProxyMaxMessageSize {
			return fmt.Errorf("livekit proxy: message exceeds %d byte limit", wsProxyMaxMessageSize)
		}
		if closeErr := writer.Close(); closeErr != nil {
			return closeErr
		}
	}
}
