"""
Lab 08a: Classify phishing sites with scikit-learn
Chapter 8 - Applied Cybersecurity: Foundations to Mastery

Steps follow the book exactly. Run cell by cell or as a script.

Toolchain (pinned to 11 May 2026 access date):
  scikit-learn==1.8.0
  ucimlrepo
  pandas
"""

# ── Step 1: Fetch the dataset ─────────────────────────────────────────────────
from ucimlrepo import fetch_ucirepo
import pandas as pd
import numpy as np

print("Fetching UCI Phishing Websites dataset (id=327)...")
ds = fetch_ucirepo(id=327)

X = ds.data.features   # 30 all-numeric features
y = ds.data.targets.squeeze()   # binary: 1 = phishing, -1 = legitimate

print(f"Dataset shape: {X.shape}")
print(f"Class distribution:\n{y.value_counts().rename({1: 'phishing', -1: 'legitimate'})}")

# ── Step 2: Stratified train / validation / test split ────────────────────────
from sklearn.model_selection import train_test_split

# Remap -1 -> 0 for clarity
y_bin = (y == 1).astype(int)

X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y_bin, test_size=0.15, random_state=42, stratify=y_bin
)
X_train, X_val, y_train, y_val = train_test_split(
    X_trainval, y_trainval, test_size=0.1765, random_state=42, stratify=y_trainval
)
# Results in roughly 7,700 / 1,650 / 1,650 as the book specifies

print(f"\nSplit sizes: train={len(X_train)}, val={len(X_val)}, test={len(X_test)}")

# ── Step 3: Fit four classifiers ──────────────────────────────────────────────
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.metrics import classification_report, f1_score

models = {
    "DecisionTree":       DecisionTreeClassifier(random_state=42),
    "RandomForest":       RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1),
    "GradientBoosting":   GradientBoostingClassifier(random_state=42),
    "SVC":                SVC(kernel="rbf", random_state=42),
}

print("\n=== Per-class precision/recall/F1 on held-out TEST set ===\n")
for name, clf in models.items():
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(f"-- {name} --")
    print(classification_report(y_test, y_pred,
                                 target_names=["legitimate", "phishing"],
                                 digits=3))

# ── Step 4: Stage the accuracy trap ───────────────────────────────────────────
# Down-sample phishing to ~1% base rate
from sklearn.dummy import DummyClassifier

print("\n=== The accuracy trap: phishing down-sampled to 1% base rate ===\n")

# Keep all legitimate (y==0), keep 1% of phishing (y==1)
mask_legit = y_train == 0
mask_phish = y_train == 1
phish_idx  = np.where(mask_phish)[0]
keep_n     = max(1, int(len(phish_idx) * 0.01))
keep_idx   = np.random.default_rng(42).choice(phish_idx, size=keep_n, replace=False)

X_imbal = pd.concat([X_train[mask_legit], X_train.iloc[keep_idx]])
y_imbal = np.concatenate([y_train[mask_legit], y_train.iloc[keep_idx]])

# Build matching imbalanced test
mask_t_l = y_test == 0
mask_t_p = y_test == 1
phish_t   = np.where(mask_t_p)[0]
keep_t    = max(1, int(len(phish_t) * 0.01))
keep_t_idx = np.random.default_rng(42).choice(phish_t, size=keep_t, replace=False)
X_test_imbal = pd.concat([X_test[mask_t_l], X_test.iloc[keep_t_idx]])
y_test_imbal = np.concatenate([y_test[mask_t_l], y_test.iloc[keep_t_idx]])

print(f"Imbalanced test distribution: {pd.Series(y_test_imbal).value_counts().to_dict()}")

dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_imbal, y_imbal)
y_dummy = dummy.predict(X_test_imbal)

print("\nDummyClassifier (always predict 'legitimate'):")
print(classification_report(y_test_imbal, y_dummy,
                             target_names=["legitimate", "phishing"],
                             digits=3))
print("NOTE: ~99% accuracy, 0% recall on phishing. This is the accuracy trap.")

# Now recover recall with a real model on the same imbalanced data
rf_imbal = RandomForestClassifier(n_estimators=100, random_state=42,
                                   class_weight="balanced", n_jobs=-1)
rf_imbal.fit(X_imbal, y_imbal)
y_rf_imbal = rf_imbal.predict(X_test_imbal)
print("\nRandomForest (class_weight='balanced') on imbalanced data:")
print(classification_report(y_test_imbal, y_rf_imbal,
                             target_names=["legitimate", "phishing"],
                             digits=3))
print("Real recall recovered - this is the lesson of Lab 08a.")
