# -*- coding: utf-8 -*-
"""
paml_tester
===========
A minimal Hello-World PyPI package built to demonstrate
clean packaging with PEP 621 / setuptools.

Quick start
-----------
>>> from paml_tester import reverse_string, clamp, days_until
>>> reverse_string("PyPI")
'IPyP'
>>> clamp(150, 0, 100)
100
"""

from paml_tester.utils import (
    reverse_string,
    is_palindrome,
    word_count,
    clamp,
    percentage,
    factorial,
    days_until,
    age_in_years,
)

__all__ = [
    "reverse_string",
    "is_palindrome",
    "word_count",
    "clamp",
    "percentage",
    "factorial",
    "days_until",
    "age_in_years",
]

__version__ = "0.1.0"
__author__ = "paml_tester contributors"

# # Linear Regression

# from sklearn.linear_model import LinearRegression


# # Train
# lr = LinearRegression()
# lr.fit(X_train, y_train)

# # Predict
# y_pred_train = lr.predict(X_train)
# y_pred_test  = lr.predict(X_test)

# from sklearn.metrics import mean_absolute_error, mean_squared_error

# # MAE
# mean_absolute_error(y_test, y_pred_test)

# # RMSE
# np.sqrt(mean_squared_error(y_test, y_pred_test))

# # R² Score
# lr.score(X_test, y_test)

# # Random Forest - Classification
# from sklearn.ensemble import RandomForestClassifier

# # Train
# rf = RandomForestClassifier(n_estimators  = 100,
#                              max_depth     = None,
#                              random_state  = 42)
# rf.fit(X_train, y_train)

# # Predict
# y_pred = rf.predict(X_test)

# # Metrics

# from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report

# print(accuracy_score(y_test, y_pred))
# print(confusion_matrix(y_test, y_pred))
# print(classification_report(y_test, y_pred))

# # Random Forest - Regression
# from sklearn.ensemble import RandomForestRegressor
# from sklearn.metrics import mean_absolute_error, mean_squared_error

# # Train
# rf = RandomForestRegressor(n_estimators = 100,
#                             max_depth    = None,
#                             random_state = 42)
# rf.fit(X_train, y_train)

# # Predict
# y_pred = rf.predict(X_test)

# # Metrics
# print(mean_absolute_error(y_test, y_pred))
# print(np.sqrt(mean_squared_error(y_test, y_pred)))
# print(rf.score(X_test, y_test))   # R²


# # Hyper Parameter Tuning
# from sklearn.model_selection import GridSearchCV

# param_grid = {
#     "n_estimators"    : [100, 200, 300],
#     "max_depth"       : [3, 5, 10, None],
#     "min_samples_split": [2, 5, 10]
# }

# grid = GridSearchCV(estimator  = RandomForestClassifier(random_state=42),
#                     param_grid = param_grid,
#                     cv         = 5,
#                     scoring    = "accuracy")

# grid.fit(X_train, y_train)

# print("Best Params:", grid.best_params_)

# # Retrain with best params
# best_rf = grid.best_estimator_
# y_pred  = best_rf.predict(X_test)


# # Feature Importance

# import pandas as pd
# import matplotlib.pyplot as plt

# importance = pd.DataFrame({
#     "Feature"    : X_train.columns,
#     "Importance" : rf.feature_importances_
# }).sort_values("Importance", ascending=False)

# plt.figure(figsize=(10, 6))
# plt.bar(importance["Feature"], importance["Importance"])
# plt.xticks(rotation=90)
# plt.title("Random Forest Feature Importance")
# plt.tight_layout()
# plt.show()


# # K Means Clustering

# import pandas as pd
# import numpy as np
# import matplotlib.pyplot as plt
# import seaborn as sns
# from sklearn.cluster import KMeans
# from sklearn.metrics import silhouette_score
# from sklearn.preprocessing import StandardScaler

# # ============================================================
# # STEP 1 — LOAD DATA
# # ============================================================
# df = pd.read_csv("your_dataset.csv")
# print(df.head())
# print(df.shape)

# # ============================================================
# # STEP 2 — FEATURE ENGINEERING
# # Encode any categorical columns before clustering
# # K-Means only works on numerical data
# # ============================================================
# df = pd.get_dummies(df, drop_first=True)

# # ============================================================
# # STEP 3 — DROP TARGET / NON-NUMERIC COLUMNS IF ANY
# # Clustering is unsupervised — no target column needed
# # ============================================================
# df_cluster = df.select_dtypes(include="number")

# # ============================================================
# # STEP 4 — SCALE THE DATA
# # K-Means is distance based — scaling ensures no single
# # feature dominates just because of its unit/range
# # ============================================================
# scaler    = StandardScaler()
# df_scaled = scaler.fit_transform(df_cluster)

# # ============================================================
# # STEP 5 — ELBOW METHOD
# # Run K-Means for K = 1 to 10
# # Plot WCSS (Within Cluster Sum of Squares) for each K
# # The "elbow" point = optimal number of clusters
# # ============================================================
# wcss = []

# for k in range(1, 11):
#     kmeans = KMeans(n_clusters   = k,
#                     init         = "k-means++",  # smarter init than random
#                     random_state = 42)
#     kmeans.fit(df_scaled)
#     wcss.append(kmeans.inertia_)   # inertia_ = WCSS score

# plt.figure(figsize=(10, 5))
# plt.plot(range(1, 11), wcss, marker="o", color="red")
# plt.title("Elbow Method — Finding Optimal K")
# plt.xlabel("Number of Clusters (K)")
# plt.ylabel("WCSS")
# plt.show()

# # ============================================================
# # STEP 6 — SILHOUETTE SCORE
# # Confirms the K suggested by the elbow
# # Higher score = better separated clusters
# # Score close to 1 = good, close to 0 = overlap, negative = bad
# # ============================================================
# print("\nSilhouette Scores:")
# for k in range(2, 8):
#     score = silhouette_score(df_scaled, 
#                              KMeans(n_clusters   = k,
#                                     random_state = 42).fit_predict(df_scaled))
#     print(f"  K = {k}  →  Silhouette Score: {score:.4f}")

# # ============================================================
# # STEP 7 — APPLY OPTIMAL K
# # Replace 5 with whatever K the elbow + silhouette suggested
# # ============================================================
# optimal_k = 5   # <-- change this based on your elbow plot

# kmeans = KMeans(n_clusters   = optimal_k,
#                 init         = "k-means++",
#                 random_state = 42)

# df_cluster["Cluster"] = kmeans.fit_predict(df_scaled)

# # ============================================================
# # STEP 8 — VISUALISE CLUSTERS
# # Plot first 2 columns against each other coloured by cluster
# # Also plot the centroids as black X marks
# # ============================================================
# plt.figure(figsize=(12, 8))

# sns.scatterplot(x       = df_cluster.columns[0],
#                 y       = df_cluster.columns[1],
#                 hue     = "Cluster",
#                 data    = df_cluster,
#                 palette = "viridis",
#                 s       = 100,
#                 alpha   = 0.7)

# # Plot centroids
# # inverse_transform converts scaled centroids back to original scale
# centroids = scaler.inverse_transform(kmeans.cluster_centers_)

# plt.scatter(centroids[:, 0],
#             centroids[:, 1],
#             s      = 300,
#             c      = "black",
#             marker = "X",
#             label  = "Centroids")

# plt.title(f"K-Means Clusters (K={optimal_k})")
# plt.xlabel(df_cluster.columns[0])
# plt.ylabel(df_cluster.columns[1])
# plt.legend()
# plt.show()

# # ============================================================
# # STEP 9 — CLUSTER SUMMARY
# # Understand what each cluster looks like
# # Mean values per cluster tells you the "profile" of each group
# # ============================================================
# print("\nCluster Sizes:")
# print(df_cluster["Cluster"].value_counts())

# print("\nCluster Profiles (Mean per Cluster):")
# print(df_cluster.groupby("Cluster").mean().round(2))


# # MLP — Multi Layer Perceptron

# import pandas as pd
# import numpy as np
# import matplotlib.pyplot as plt
# from sklearn.neural_network import MLPClassifier, MLPRegressor
# from sklearn.model_selection import train_test_split
# from sklearn.preprocessing import StandardScaler
# from sklearn.metrics import (accuracy_score, confusion_matrix, 
#                              classification_report, mean_absolute_error, 
#                              mean_squared_error)

# # ============================================================
# # STEP 1 — LOAD DATA
# # ============================================================
# df = pd.read_csv("your_dataset.csv")
# print(df.head())
# print(df.shape)

# # ============================================================
# # STEP 2 — DEFINE X AND Y
# # ============================================================
# X = df.drop("target", axis=1)
# y = df["target"]

# # ============================================================
# # STEP 3 — TRAIN TEST SPLIT
# # ============================================================
# X_train, X_test, y_train, y_test = train_test_split(X, y,
#                                                      test_size    = 0.2,
#                                                      random_state = 42)

# # ============================================================
# # STEP 4 — SCALE THE DATA
# # MLP is distance/gradient based — scaling is mandatory
# # Always fit on train, only transform on test
# # ============================================================
# scaler  = StandardScaler()
# X_train = scaler.fit_transform(X_train)
# X_test  = scaler.transform(X_test)

# # ============================================================
# # STEP 5A — MLP CLASSIFIER (use for classification problems)
# # e.g. HeartDisease, Loan, Successful, OS
# # ============================================================
# mlp_clf = MLPClassifier(
#                 hidden_layer_sizes = (100, 50),  # 2 hidden layers: 100 & 50 neurons
#                 activation         = "relu",      # relu is default & most common
#                 solver             = "adam",      # adam is best for most cases
#                 max_iter           = 500,         # increase if model doesn't converge
#                 random_state       = 42)

# mlp_clf.fit(X_train, y_train)
# y_pred = mlp_clf.predict(X_test)

# # Metrics
# print("Accuracy  :", accuracy_score(y_test, y_pred))
# print("\nConfusion Matrix:")
# print(confusion_matrix(y_test, y_pred))
# print("\nClassification Report:")
# print(classification_report(y_test, y_pred))

# # ============================================================
# # STEP 5B — MLP REGRESSOR (use for regression problems)
# # e.g. Rent, HDI, Premium, Popularity Score
# # ============================================================
# mlp_reg = MLPRegressor(
#                 hidden_layer_sizes = (100, 50),  # 2 hidden layers: 100 & 50 neurons
#                 activation         = "relu",      # relu is default & most common
#                 solver             = "adam",      # adam is best for most cases
#                 max_iter           = 500,         # increase if model doesn't converge
#                 random_state       = 42)

# mlp_reg.fit(X_train, y_train)
# y_pred = mlp_reg.predict(X_test)

# # Metrics
# print("MAE  :", mean_absolute_error(y_test, y_pred))
# print("RMSE :", np.sqrt(mean_squared_error(y_test, y_pred)))
# print("R²   :", mlp_reg.score(X_test, y_test))

# # ============================================================
# # STEP 6 — PLOT LOSS CURVE
# # Shows if model converged properly
# # Loss should decrease and flatten out — if still dropping,
# # increase max_iter
# # ============================================================
# plt.figure(figsize=(10, 5))
# plt.plot(mlp_clf.loss_curve_, color="red")   # swap to mlp_reg for regression
# plt.title("MLP Loss Curve")
# plt.xlabel("Iterations")
# plt.ylabel("Loss")
# plt.show()

# # ============================================================
# # STEP 7 — HYPERPARAMETER TUNING (optional but good practice)
# # ============================================================
# from sklearn.model_selection import GridSearchCV

# param_grid = {
#     "hidden_layer_sizes" : [(50,), (100,), (100, 50), (100, 50, 25)],
#     "activation"         : ["relu", "tanh"],
#     "max_iter"           : [300, 500]
# }

# grid = GridSearchCV(estimator  = MLPClassifier(random_state=42),
#                     param_grid = param_grid,
#                     cv         = 5,
#                     scoring    = "accuracy")

# grid.fit(X_train, y_train)

# print("Best Params:", grid.best_params_)

# best_mlp = grid.best_estimator_
# y_pred   = best_mlp.predict(X_test)
# print("Tuned Accuracy:", accuracy_score(y_test, y_pred))