def preprocess_address(address: str) -> str:
    """Normalize address string for better comparison"""
    if not address:
        return ""
    # Convert to lowercase and remove special characters
    processed = re.sub(r"[^\w\s]", " ", str(address).lower())
    # Normalize whitespace
    processed = " ".join(processed.split())
    return processed

def find_customer_clusters(customers: list[StripeCustomer]):
    """
    Cluster customers based on similarity of their billing addresses.

    Args:
        customers: List of StripeCustomer objects with billing_addresses

    Returns:
        dict: Clusters of similar customers, keyed by cluster ID
    """
    # Filter out customers with no billing addresses
    customers_with_addresses = [c for c in customers if c.billing_addresses]

    if not customers_with_addresses:
        logging.info("No customer addresses found")
        return {}

    # Prepare addresses for vectorization
    # For customers with multiple addresses, join them with a space
    addresses = [
        preprocess_address(" ".join(c.billing_addresses))
        for c in customers_with_addresses
    ]

    # Create TF-IDF vectors
    vectorizer = TfidfVectorizer(
        analyzer="word",
        ngram_range=(1, 2),  # Use both unigrams and bigrams
        min_df=1,
        stop_words="english",
    )
    tfidf_matrix = vectorizer.fit_transform(addresses)

    # Calculate similarity matrix
    similarity_matrix = cosine_similarity(tfidf_matrix)

    # Perform DBSCAN clustering
    clustering = DBSCAN(
        eps=0.2,  # Maximum distance between two samples to be considered in same cluster
        min_samples=2,  # Minimum samples in a cluster
        metric="precomputed",  # Use our pre-computed similarity matrix
    ).fit(1 - similarity_matrix)  # Convert similarity to distance

    # Organize results by cluster
    clusters = defaultdict(list)
    for idx, label in enumerate(clustering.labels_):
        if label != -1:  # -1 represents noise points
            customer = customers_with_addresses[idx]
            clusters[label].append(
                {
                    "customer_id": customer.id,
                    "email": customer.email,
                    "addresses": customer.billing_addresses,
                }
            )

    # Log results
    logging.info(f"Found {len(clusters)} clusters of similar addresses")
    for cluster_id, members in clusters.items():
        logging.info(f"\nCluster {cluster_id}:")
        for member in members:
            logging.info(f"Customer: {member['customer_id']}")
            logging.info(f"Email: {member['email']}")
            logging.info(f"Addresses: {member['addresses']}")
            logging.info("---")

    return dict(clusters)
