// Package grpcserver provides gRPC server setup with TLS, interceptor
// chain, and method-map authentication for Metacircular services.
//
// Access control is enforced via a [MethodMap] that classifies each RPC
// as public, auth-required, or admin-required. Methods not listed in any
// map are denied by default — forgetting to register a new RPC results
// in a denied request, not an open one.
package grpcserver

import (
	"context"
	"crypto/tls"
	"fmt"
	"log/slog"
	"net"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"

	"git.wntrmute.dev/kyle/mcdsl/auth"
)

// MethodMap classifies gRPC methods for access control.
type MethodMap struct {
	// Public methods require no authentication.
	Public map[string]bool

	// AuthRequired methods require a valid MCIAS bearer token.
	AuthRequired map[string]bool

	// AdminRequired methods require a valid token with the admin role.
	AdminRequired map[string]bool
}

// Server wraps a grpc.Server with Metacircular auth interceptors.
type Server struct {
	// GRPCServer is the underlying grpc.Server. Services register their
	// implementations on it before calling Serve.
	GRPCServer *grpc.Server

	// Logger is used by the logging interceptor.
	Logger *slog.Logger

	listener net.Listener
}

// New creates a gRPC server with TLS (if certFile and keyFile are
// non-empty) and an interceptor chain: logging → auth → handler.
//
// The auth interceptor uses methods to determine the access level for
// each RPC. Methods not in any map are denied by default.
//
// If certFile and keyFile are empty, TLS is skipped (for testing).
func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error) {
	chain := grpc.ChainUnaryInterceptor(
		loggingInterceptor(logger),
		authInterceptor(authenticator, methods),
	)

	var opts []grpc.ServerOption
	opts = append(opts, chain)

	if certFile != "" && keyFile != "" {
		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
		if err != nil {
			return nil, fmt.Errorf("grpcserver: load TLS cert: %w", err)
		}
		tlsCfg := &tls.Config{
			Certificates: []tls.Certificate{cert},
			MinVersion:   tls.VersionTLS13,
		}
		opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg)))
	}

	return &Server{
		GRPCServer: grpc.NewServer(opts...),
		Logger:     logger,
	}, nil
}

// Serve starts the gRPC server on the given address. It blocks until
// the server is stopped.
func (s *Server) Serve(addr string) error {
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		return fmt.Errorf("grpcserver: listen %s: %w", addr, err)
	}
	s.listener = lis
	s.Logger.Info("starting gRPC server", "addr", addr)
	return s.GRPCServer.Serve(lis)
}

// Stop gracefully stops the gRPC server, waiting for in-flight RPCs
// to complete.
func (s *Server) Stop() {
	s.GRPCServer.GracefulStop()
}

// TokenInfoFromContext extracts [auth.TokenInfo] from a gRPC request
// context. Returns nil if no token info is present (e.g., for public
// methods).
func TokenInfoFromContext(ctx context.Context) *auth.TokenInfo {
	return auth.TokenInfoFromContext(ctx)
}

// loggingInterceptor logs each RPC after it completes.
func loggingInterceptor(logger *slog.Logger) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		start := time.Now()
		resp, err := handler(ctx, req)
		code := status.Code(err)
		logger.Info("grpc",
			"method", info.FullMethod,
			"code", code.String(),
			"duration", time.Since(start),
		)
		return resp, err
	}
}

// authInterceptor enforces access control based on the MethodMap.
//
// Evaluation order:
//  1. Public → pass through, no auth.
//  2. AdminRequired → validate token, require IsAdmin.
//  3. AuthRequired → validate token.
//  4. Not in any map → deny (default deny).
func authInterceptor(authenticator *auth.Authenticator, methods MethodMap) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		method := info.FullMethod

		// Public methods: no auth.
		if methods.Public[method] {
			return handler(ctx, req)
		}

		// All other methods require a valid token.
		tokenInfo, err := extractAndValidate(ctx, authenticator)
		if err != nil {
			return nil, err
		}

		// Admin-required methods: check admin role.
		if methods.AdminRequired[method] {
			if !tokenInfo.IsAdmin {
				return nil, status.Errorf(codes.PermissionDenied, "admin role required")
			}
			ctx = auth.ContextWithTokenInfo(ctx, tokenInfo)
			return handler(ctx, req)
		}

		// Auth-required methods: token is sufficient.
		if methods.AuthRequired[method] {
			ctx = auth.ContextWithTokenInfo(ctx, tokenInfo)
			return handler(ctx, req)
		}

		// Default deny: method not in any map.
		return nil, status.Errorf(codes.PermissionDenied, "method not authorized")
	}
}

// extractAndValidate extracts the bearer token from gRPC metadata and
// validates it via the Authenticator.
func extractAndValidate(ctx context.Context, authenticator *auth.Authenticator) (*auth.TokenInfo, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
	}

	vals := md.Get("authorization")
	if len(vals) == 0 {
		return nil, status.Errorf(codes.Unauthenticated, "missing authorization header")
	}

	token := vals[0]
	const bearerPrefix = "Bearer "
	if len(token) > len(bearerPrefix) && token[:len(bearerPrefix)] == bearerPrefix {
		token = token[len(bearerPrefix):]
	}

	info, err := authenticator.ValidateToken(token)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "invalid token")
	}

	return info, nil
}
