package mtls

import (
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"
)

// nodeUUIDKey is the unexported context key for the verified node UUID.
type nodeUUIDKey struct{}

// enrollFullMethod is the one RPC exempt from mTLS enforcement.
// The agent has no certificate yet when it calls Enroll; the bootstrap token
// provides authentication for that call instead.
const enrollFullMethod = "/pangolin.agent.v1.AgentService/Enroll"

// UnaryServerInterceptor returns a gRPC unary interceptor that enforces mTLS identity.
//
// For the Enroll method: the call is passed through without a certificate check.
// For all other methods: a verified client certificate is required; its CN is
// injected into the context as the authoritative node UUID.
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(
		ctx context.Context,
		req interface{},
		info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler,
	) (interface{}, error) {
		if info.FullMethod == enrollFullMethod {
			return handler(ctx, req)
		}

		nodeUUID, err := extractNodeUUID(ctx, info.FullMethod)
		if err != nil {
			return nil, err
		}

		return handler(context.WithValue(ctx, nodeUUIDKey{}, nodeUUID), req)
	}
}

// StreamServerInterceptor returns a gRPC stream interceptor that enforces mTLS identity.
// Same whitelist logic as the unary interceptor applies.
func StreamServerInterceptor() grpc.StreamServerInterceptor {
	return func(
		srv interface{},
		ss grpc.ServerStream,
		info *grpc.StreamServerInfo,
		handler grpc.StreamHandler,
	) error {
		if info.FullMethod == enrollFullMethod {
			return handler(srv, ss)
		}

		nodeUUID, err := extractNodeUUID(ss.Context(), info.FullMethod)
		if err != nil {
			return err
		}

		enriched := &wrappedStream{
			ServerStream: ss,
			ctx:          context.WithValue(ss.Context(), nodeUUIDKey{}, nodeUUID),
		}
		return handler(srv, enriched)
	}
}

// NodeUUIDFromContext retrieves the verified node UUID that was injected by the
// interceptor. Returns ("", false) when the context carries no UUID (e.g. in
// Enroll handlers where the identity is not yet established).
func NodeUUIDFromContext(ctx context.Context) (string, bool) {
	v, ok := ctx.Value(nodeUUIDKey{}).(string)
	return v, ok && v != ""
}

// ─── internals ───────────────────────────────────────────────────────────────

// extractNodeUUID reads the verified TLS peer certificate's CN from the context.
// It returns an Unauthenticated status error when no valid cert is present.
func extractNodeUUID(ctx context.Context, method string) (string, error) {
	p, ok := peer.FromContext(ctx)
	if !ok {
		return "", status.Errorf(codes.Unauthenticated,
			"mtls: no peer info for method %s", method)
	}

	tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
	if !ok {
		return "", status.Errorf(codes.Unauthenticated,
			"mtls: non-TLS connection for method %s", method)
	}

	chains := tlsInfo.State.VerifiedChains
	if len(chains) == 0 || len(chains[0]) == 0 {
		return "", status.Errorf(codes.Unauthenticated,
			"mtls: client certificate required for method %s", method)
	}

	cn := chains[0][0].Subject.CommonName
	if cn == "" {
		return "", status.Errorf(codes.Unauthenticated,
			"mtls: empty CN in client certificate for method %s", method)
	}
	return cn, nil
}

// wrappedStream overrides the context on a ServerStream.
type wrappedStream struct {
	grpc.ServerStream
	ctx context.Context
}

func (w *wrappedStream) Context() context.Context {
	return w.ctx
}
