Rewrite rules for custom post type slug

WordPress seems to be unable to generate the correct rules when you use more complex formats for the slug argument in register_post_type. In this case we have to use add_permastruct.

This example adds the post type and extra rules to enable singular products and brand archives with the correct URLs:

function wpd_products_post_type() {

    // post type args
    // rewrite and has_archive must be true!
    $args = array(
        'public' => true,
        'rewrite' => true,
        'has_archive' => true,
        'supports' => array( 'title', 'custom-fields' ),
    );

    register_post_type(
        'products',
        $args
    );

    // so WP will parse brand_name into query vars
    add_rewrite_tag(
        '%brand_name%',
        '([^&]+)'
    );

    // this sets the actual structure the post type will use
    add_permastruct(
        'products',
        'brand-%brand_name%/%products%',
        array( 'with_front' => false )
    );

    // to enable product archives by brand
    add_rewrite_rule(
        "^brand-([^/]+)?",
        'index.php?post_type=products&brand_name=$matches[1]',
        'top'
    );

}
add_action( 'init','wpd_products_post_type' );

You will still need your post_type_link filter to insert the meta value into the singular product URL.

You will also need to add a pre_get_posts filter for the product archives, to add a meta query for your brand_name.

You could also add an additional pre_get_posts filter for singular products to add in the meta query, as you may discover that you can currently put any value you want in the URL as brand name and the query will succeed, since WordPress only queries for slug by default.