package server

import (
	"context"
	"io/fs"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/jcmturner/gokrb5/v8/keytab"
	"github.com/uopi/uopi/internal/access"
	"github.com/uopi/uopi/internal/api"
	"github.com/uopi/uopi/internal/audit"
	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/confmgr"
	"github.com/uopi/uopi/internal/controllogic"
	"github.com/uopi/uopi/internal/datasource/synthetic"
	"github.com/uopi/uopi/internal/metrics"
	"github.com/uopi/uopi/internal/panelacl"
	"github.com/uopi/uopi/internal/storage"
)

const apiPrefix = "/api/v1"

type Server struct {
	httpServer  *http.Server
	tlsCert     string
	tlsKey      string
	tlsRedirect string // plain-HTTP addr that 301-redirects to HTTPS; empty = off
	log         *slog.Logger
}

// New creates the HTTP server, registers all routes, and returns a ready-to-start Server.
// synth may be nil if the synthetic data source is not enabled.
//
// basicAuthFn, when non-nil, enables built-in HTTP Basic authentication validated
// by that function (typically PAM): REST requests are challenged (401 Basic) and
// WebSocket upgrades validate proactively-resent credentials best-effort. It is
// mutually exclusive with Kerberos (krbKeytab). When tlsCert and tlsKey are both
// set the server serves HTTPS via ListenAndServeTLS.
func New(addr string, webFS fs.FS, brk *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, cfgStore *confmgr.Store, policy *access.Policy, acl *panelacl.Store, ctrlLogic *controllogic.Store, ctrlEngine *controllogic.Engine, dialogs *DialogHub, debug *DebugHub, rec audit.Recorder, channelFinderURL, archiverURL, trustedUserHeader string, krbKeytab *keytab.Keytab, krbSPN string, basicAuthFn func(user, pass string) error, basicAuthRealm, tlsCert, tlsKey, tlsRedirect string, uiDefaultZoom float64, log *slog.Logger) *Server {
	if rec == nil {
		rec = audit.Nop()
	}
	mux := http.NewServeMux()

	// When built-in authentication (Kerberos or Basic) is enabled but no external
	// proxy header is configured, use an internal header to carry the validated
	// username downstream.
	userHeader := trustedUserHeader
	if (krbKeytab != nil || basicAuthFn != nil) && userHeader == "" {
		userHeader = internalUserHeader
	}

	// Health check
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	// WebSocket endpoint
	var wsHandlerH http.Handler = &wsHandler{broker: brk, log: log, userHeader: userHeader, policy: policy, audit: rec, dialogs: dialogs, debug: debug}

	// Prometheus-format metrics
	mux.HandleFunc("/metrics", metrics.Handler(brk.ActiveSubscriptions))

	// REST API — registered on a dedicated mux so it can be wrapped with the
	// access-control middleware (identity resolution + global level enforcement).
	apiMux := http.NewServeMux()
	api.New(brk, synth, store, cfgStore, policy, acl, ctrlLogic, ctrlEngine, rec, channelFinderURL, archiverURL, uiDefaultZoom, log).Register(apiMux, apiPrefix)
	var apiHandler http.Handler = accessMiddleware(policy, userHeader, apiMux)

	// Native SPNEGO/Kerberos authentication (optional). REST/page requests are
	// challenged (401 Negotiate); WebSocket upgrades validate proactively-sent
	// credentials best-effort. Both stash the validated username in userHeader.
	var frontendHandler http.Handler = http.FileServerFS(webFS)

	if krbKeytab != nil {
		apiHandler = kerberosAuth(krbKeytab, krbSPN, userHeader, true, log, apiHandler)
		wsHandlerH = kerberosAuth(krbKeytab, krbSPN, userHeader, false, log, wsHandlerH)
	} else if basicAuthFn != nil {
		// Built-in HTTP Basic authentication (validated by basicAuthFn, e.g. PAM).
		// A shared positive-result cache spares a validation round-trip on every
		// browser request. REST is challenged; WebSocket upgrades are best-effort.
		cache := newCredCache(5 * time.Minute)
		apiHandler = basicAuth(basicAuthFn, userHeader, basicAuthRealm, true, cache, log, apiHandler)
		wsHandlerH = basicAuth(basicAuthFn, userHeader, basicAuthRealm, false, cache, log, wsHandlerH)
		// Challenge the top-level page load too. Browsers only show the native
		// Basic login dialog in response to a 401 on a navigation, not on the
		// SPA's background fetch('/api/v1/me'); without this the user is never
		// prompted and silently resolves to default_user. Once credentials are
		// entered the browser caches them for the origin and resends them on
		// every asset, API call and the WebSocket upgrade.
		frontendHandler = basicAuth(basicAuthFn, userHeader, basicAuthRealm, true, cache, log, frontendHandler)
	}
	mux.Handle("/ws", wsHandlerH)
	mux.Handle(apiPrefix+"/", apiHandler)

	// Embedded frontend — must be last (catch-all)
	mux.Handle("/", frontendHandler)

	return &Server{
		httpServer: &http.Server{
			Addr:         addr,
			Handler:      mux,
			ReadTimeout:  10 * time.Second,
			WriteTimeout: 30 * time.Second,
			IdleTimeout:  120 * time.Second,
		},
		tlsCert:     tlsCert,
		tlsKey:      tlsKey,
		tlsRedirect: tlsRedirect,
		log:         log,
	}
}

// accessMiddleware resolves the end-user identity from the trusted proxy header
// (falling back to the configured default_user), stores it on the request
// context, and enforces the user's global access level on every API request:
//   - LevelNone  → 403 for all requests.
//   - LevelRead  → 403 for any mutating request (non GET/HEAD).
//   - LevelWrite → no restriction.
//
// The /me endpoint is always reachable so the frontend can discover the
// caller's identity and level even when otherwise restricted.
func accessMiddleware(policy *access.Policy, userHeader string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var raw string
		if userHeader != "" {
			raw = r.Header.Get(userHeader)
		}
		user := policy.ResolveUser(raw)
		r = r.WithContext(access.WithUser(r.Context(), user))

		// Always allow identity discovery.
		if strings.HasSuffix(r.URL.Path, apiPrefix+"/me") {
			next.ServeHTTP(w, r)
			return
		}

		switch policy.Level(user) {
		case access.LevelNone:
			http.Error(w, "access denied", http.StatusForbidden)
			return
		case access.LevelRead:
			if r.Method != http.MethodGet && r.Method != http.MethodHead {
				http.Error(w, "read-only access", http.StatusForbidden)
				return
			}
		}
		next.ServeHTTP(w, r)
	})
}

// Start listens and serves until ctx is cancelled.
func (s *Server) Start(ctx context.Context) error {
	tls := s.tlsCert != "" && s.tlsKey != ""
	s.log.Info("listening", "addr", s.httpServer.Addr, "tls", tls)

	errCh := make(chan error, 1)
	go func() {
		var err error
		if tls {
			err = s.httpServer.ListenAndServeTLS(s.tlsCert, s.tlsKey)
		} else {
			err = s.httpServer.ListenAndServe()
		}
		if err != nil && err != http.ErrServerClosed {
			errCh <- err
		}
	}()

	// Optional plain-HTTP redirector: upgrade http:// visitors to the HTTPS
	// service instead of letting them hit the TLS port with a cleartext request.
	var redirectSrv *http.Server
	if tls && s.tlsRedirect != "" {
		redirectSrv = &http.Server{
			Addr:         s.tlsRedirect,
			Handler:      httpsRedirectHandler(s.httpServer.Addr),
			ReadTimeout:  10 * time.Second,
			WriteTimeout: 10 * time.Second,
		}
		s.log.Info("http→https redirect listening", "addr", s.tlsRedirect)
		go func() {
			if err := redirectSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
				errCh <- err
			}
		}()
	}

	select {
	case err := <-errCh:
		return err
	case <-ctx.Done():
		shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		s.log.Info("shutting down")
		if redirectSrv != nil {
			_ = redirectSrv.Shutdown(shutCtx)
		}
		return s.httpServer.Shutdown(shutCtx)
	}
}

// httpsRedirectHandler 301-redirects any plain-HTTP request to the HTTPS service.
// It preserves the requested hostname and path, swapping in the TLS listener's
// port (omitted when 443).
func httpsRedirectHandler(tlsAddr string) http.Handler {
	_, tlsPort, _ := net.SplitHostPort(tlsAddr)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		host := r.Host
		if h, _, err := net.SplitHostPort(host); err == nil {
			host = h
		}
		target := "https://" + host
		if tlsPort != "" && tlsPort != "443" {
			target += ":" + tlsPort
		}
		target += r.URL.RequestURI()
		http.Redirect(w, r, target, http.StatusMovedPermanently)
	})
}
