import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from google.colab import drive
from sklearn.metrics import (adjusted_rand_score, classification_report,
                             normalized_mutual_info_score)
from sklearn.preprocessing import StandardScaler

drive.mount("/content/drive")

csv_path = "/content/drive/MyDrive/Academic_Resources/Semester 08/20XW87/PS14/wholesale_customers_data.csv"
df = pd.read_csv(csv_path)

print(df.head())

"""### DF Report"""

print(df.describe())

"""### Drop 'Channel' and 'Region' columns"""

df = df.drop(columns=["Channel", "Region"])

display(df.head())

"""### Consider Groceries and Milk attributes. Normalize these attribute values by scaling it from 0 mean to unit variance. Visualize normalized dataset."""

plt.scatter(df["Grocery"], df["Milk"], alpha=0.6)
plt.title("Normalized Groceries vs Milk")
plt.xlabel("Grocery (before normalized)")
plt.ylabel("Milk (before normalized)")
plt.show()

# Select Groceries and Milk columns
data_to_normalize = df[["Grocery", "Milk"]]

# Normalize (StandardScaler standardizes to mean=0, std=1)
scaler = StandardScaler()
normalized_data = scaler.fit_transform(data_to_normalize)

# Convert back to DataFrame for convenience
normalized_df = pd.DataFrame(normalized_data, columns=["Grocery", "Milk"])

print(normalized_df.mean())
print(normalized_df.std())

mean_grocery = normalized_df["Grocery"].mean()
mean_milk = normalized_df["Milk"].mean()

plt.figure(figsize=(7, 5))

plt.scatter(normalized_df["Grocery"], normalized_df["Milk"], alpha=0.6)

plt.axhline(mean_milk, linestyle="--", label=f"Milk Mean ({mean_milk:.2e})")
plt.axvline(mean_grocery, linestyle="--", label=f"Grocery Mean ({mean_grocery:.2e})")

plt.title("Normalized Grocery vs Milk")
plt.xlabel("Grocery (Standardized)")
plt.ylabel("Milk (Standardized)")

plt.legend()
plt.grid()
plt.show()

"""### Implementation of DBSCAN"""


def euclidean_distance(p1, p2):
    return np.sqrt(np.sum((p1 - p2) ** 2))


def region_query(data, point_idx, eps):
    neighbors = []
    for i in range(len(data)):
        if euclidean_distance(data[point_idx], data[i]) <= eps:
            neighbors.append(i)
    return neighbors


def expand_cluster(data, labels, point_idx, neighbors, cluster_id, eps, min_pts):
    labels[point_idx] = cluster_id
    i = 0
    while i < len(neighbors):
        neighbor_idx = neighbors[i]
        if labels[neighbor_idx] == -1:
            labels[neighbor_idx] = cluster_id
        elif labels[neighbor_idx] == 0:
            labels[neighbor_idx] = cluster_id
            new_neighbors = region_query(data, neighbor_idx, eps)
            if len(new_neighbors) >= min_pts:
                neighbors += new_neighbors
        i += 1


def dbscan(data, eps=0.5, min_pts=15):
    labels = [0] * len(data)  # 0 means unvisited
    cluster_id = 0

    for point_idx in range(len(data)):
        if labels[point_idx] != 0:
            continue
        neighbors = region_query(data, point_idx, eps)
        if len(neighbors) < min_pts:
            labels[point_idx] = -1  # Noise
        else:
            cluster_id += 1
            expand_cluster(data, labels, point_idx, neighbors, cluster_id, eps, min_pts)

    return labels


# Run on normalized grocery & milk data
data_np = normalized_df.values
labels = dbscan(data_np, eps=0.5, min_pts=15)

# Convert labels to numpy array for plotting
labels = np.array(labels)

plt.figure(figsize=(8, 6))
unique_labels = set(labels)
colors = plt.cm.get_cmap("tab10", len(unique_labels))

for k in unique_labels:
    class_member_mask = labels == k
    xy = data_np[class_member_mask]
    if k == -1:
        # Noise in black
        plt.scatter(xy[:, 0], xy[:, 1], c="k", marker="x", label="Noise")
    else:
        plt.scatter(xy[:, 0], xy[:, 1], c=[colors(k - 1)], label=f"Cluster {k}")
plt.title("DBSCAN Clustering Results (Custom Implementation)")
plt.xlabel("Grocery (normalized)")
plt.ylabel("Milk (normalized)")
plt.legend()
plt.show()

from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons

# Generate moon-shaped data
X, _ = make_moons(n_samples=2000, noise=0.05)

# Run DBSCAN
dbscan_builtin = DBSCAN(eps=0.3, min_samples=15)
labels_builtin = dbscan_builtin.fit_predict(X)

# Plot
plt.figure(figsize=(8, 6))
unique_labels = set(labels_builtin)
colors = plt.cm.get_cmap("tab10", len(unique_labels))

for k in unique_labels:
    class_member_mask = labels_builtin == k
    xy = X[class_member_mask]
    if k == -1:
        plt.scatter(xy[:, 0], xy[:, 1], c="k", marker="x", label="Noise")
    else:
        plt.scatter(xy[:, 0], xy[:, 1], c=[colors(k)], label=f"Cluster {k}")
plt.title("DBSCAN on make_moons")
plt.legend()
plt.show()

# Generate synthetic moon-shaped data
X, true_labels = make_moons(n_samples=2000, noise=0.05)

# Custom DBSCAN on X
custom_labels = dbscan(X, eps=0.3, min_pts=15)
custom_labels = np.array(custom_labels)

# Sklearn DBSCAN on X
dbscan_builtin = DBSCAN(eps=0.3, min_samples=15)
sklearn_labels = dbscan_builtin.fit_predict(X)

# Clustering metrics comparing the two
ari = adjusted_rand_score(custom_labels, sklearn_labels)
nmi = normalized_mutual_info_score(custom_labels, sklearn_labels)

print(f"Adjusted Rand Index (ARI) between custom and sklearn: {ari:.4f}")
print(f"Normalized Mutual Information (NMI) between custom and sklearn: {nmi:.4f}")

# Classification report (treating custom as true, sklearn as predicted)
print("\nClassification report (custom as true, sklearn as predicted):")
print(classification_report(custom_labels, sklearn_labels))

"""### Add some noise data and again visualize the results."""

# Add random noise points
noise = np.random.uniform(low=-2, high=3, size=(200, 2))

# Combine with original data
X_noisy = np.vstack([X, noise])

# Run DBSCAN again
dbscan_noisy = DBSCAN(eps=0.3, min_samples=15)
labels_noisy = dbscan_noisy.fit_predict(X_noisy)

# Plot
plt.figure(figsize=(8, 6))
unique_labels = set(labels_noisy)
colors = plt.cm.get_cmap("tab10", len(unique_labels))

for k in unique_labels:
    class_member_mask = labels_noisy == k
    xy = X_noisy[class_member_mask]

    if k == -1:
        plt.scatter(xy[:, 0], xy[:, 1], c="k", marker="x", label="Noise")
    else:
        plt.scatter(xy[:, 0], xy[:, 1], c=[colors(k)], label=f"Cluster {k}")

plt.title("DBSCAN with Added Noise")
plt.legend()
plt.show()
