#!/usr/bin/env python3
"""Bab 06 — Machine Learning Fundamentals Playground.

Standard library only. Demonstrates:
- tabular dataset;
- train/validation/test split;
- majority baseline;
- simple rule-based classifier;
- confusion matrix and metrics;
- threshold tradeoff;
- regression mean baseline;
- leakage demo.
"""
from __future__ import annotations

import math
import random
from statistics import mean

SEED = 42
random.seed(SEED)

Record = dict[str, float | int | str]


def make_student_dataset(n: int = 48) -> list[Record]:
    """Create a small synthetic dataset for learning support prediction."""
    rows: list[Record] = []
    for student_id in range(1, n + 1):
        attendance = round(random.uniform(0.55, 1.0), 2)
        task_completion = round(random.uniform(0.35, 1.0), 2)
        quiz_score = random.randint(35, 98)
        consultation_count = random.randint(0, 5)

        risk_score = (
            (1 - attendance) * 0.35
            + (1 - task_completion) * 0.30
            + max(0, 70 - quiz_score) / 70 * 0.25
            + max(0, 2 - consultation_count) / 2 * 0.10
        )
        # Hidden context simulates real-world factors not captured by our simple features:
        # family support, illness, device access, motivation, teacher notes, etc.
        hidden_context = random.uniform(-0.12, 0.12)
        needs_support = int(risk_score + hidden_context >= 0.36)
        next_quiz_score = max(0, min(100, int(quiz_score + attendance * 8 + task_completion * 7 - needs_support * 8)))

        rows.append(
            {
                "student_id": student_id,
                "attendance": attendance,
                "task_completion": task_completion,
                "quiz_score": quiz_score,
                "consultation_count": consultation_count,
                "needs_support": needs_support,
                "next_quiz_score": next_quiz_score,
                # This is intentionally leaky: it is only known after intervention/decision.
                "leaky_after_review_flag": needs_support,
            }
        )
    return rows


def split_data(rows: list[Record], train_ratio: float = 0.6, valid_ratio: float = 0.2) -> tuple[list[Record], list[Record], list[Record]]:
    shuffled = rows[:]
    random.shuffle(shuffled)
    n = len(shuffled)
    n_train = int(n * train_ratio)
    n_valid = int(n * valid_ratio)
    return shuffled[:n_train], shuffled[n_train : n_train + n_valid], shuffled[n_train + n_valid :]


def majority_label(rows: list[Record], label: str = "needs_support") -> int:
    counts = {0: 0, 1: 0}
    for row in rows:
        counts[int(row[label])] += 1
    return 1 if counts[1] >= counts[0] else 0


def predict_majority(rows: list[Record], majority: int) -> list[int]:
    return [majority for _ in rows]


def risk_score(row: Record) -> float:
    return (
        (1 - float(row["attendance"])) * 0.35
        + (1 - float(row["task_completion"])) * 0.30
        + max(0, 70 - float(row["quiz_score"])) / 70 * 0.25
        + max(0, 2 - float(row["consultation_count"])) / 2 * 0.10
    )


def predict_rule(rows: list[Record], threshold: float = 0.38) -> list[int]:
    return [int(risk_score(row) >= threshold) for row in rows]


def predict_leaky(rows: list[Record]) -> list[int]:
    return [int(row["leaky_after_review_flag"]) for row in rows]


def confusion_matrix(y_true: list[int], y_pred: list[int]) -> dict[str, int]:
    if len(y_true) != len(y_pred):
        raise ValueError("y_true dan y_pred harus sama panjang")
    tp = sum(1 for y, p in zip(y_true, y_pred) if y == 1 and p == 1)
    fp = sum(1 for y, p in zip(y_true, y_pred) if y == 0 and p == 1)
    tn = sum(1 for y, p in zip(y_true, y_pred) if y == 0 and p == 0)
    fn = sum(1 for y, p in zip(y_true, y_pred) if y == 1 and p == 0)
    return {"TP": tp, "FP": fp, "TN": tn, "FN": fn}


def safe_div(num: float, den: float) -> float:
    return 0.0 if den == 0 else num / den


def classification_metrics(y_true: list[int], y_pred: list[int]) -> dict[str, float | int]:
    cm = confusion_matrix(y_true, y_pred)
    tp, fp, tn, fn = cm["TP"], cm["FP"], cm["TN"], cm["FN"]
    accuracy = safe_div(tp + tn, tp + fp + tn + fn)
    precision = safe_div(tp, tp + fp)
    recall = safe_div(tp, tp + fn)
    f1 = safe_div(2 * precision * recall, precision + recall)
    return {**cm, "accuracy": accuracy, "precision": precision, "recall": recall, "f1": f1}


def print_metrics(name: str, rows: list[Record], preds: list[int]) -> None:
    y_true = [int(row["needs_support"]) for row in rows]
    metrics = classification_metrics(y_true, preds)
    print(f"\n{name}")
    print("-" * len(name))
    for key in ["TP", "FP", "TN", "FN", "accuracy", "precision", "recall", "f1"]:
        value = metrics[key]
        if isinstance(value, float):
            print(f"{key:>9}: {value:.3f}")
        else:
            print(f"{key:>9}: {value}")


def mae(predictions: list[float], actuals: list[float]) -> float:
    if len(predictions) != len(actuals):
        raise ValueError("predictions dan actuals harus sama panjang")
    return mean([abs(p - a) for p, a in zip(predictions, actuals)])


def regression_mean_baseline(train: list[Record], target: str = "next_quiz_score") -> float:
    return mean([float(row[target]) for row in train])


def show_error_examples(rows: list[Record], preds: list[int], max_items: int = 3) -> None:
    print("\nContoh error analysis")
    print("--------------------")
    shown = 0
    for row, pred in zip(rows, preds):
        true = int(row["needs_support"])
        if true != pred:
            print(
                f"student={row['student_id']} true={true} pred={pred} "
                f"attendance={row['attendance']} task={row['task_completion']} quiz={row['quiz_score']} risk={risk_score(row):.3f}"
            )
            shown += 1
            if shown >= max_items:
                break
    if shown == 0:
        print("Tidak ada error pada subset ini. Curiga jika ini terjadi karena fitur bocor.")


def main() -> None:
    print("Bab 06 — Machine Learning Fundamentals Playground")
    print("=" * 72)
    print(f"seed: {SEED}")

    rows = make_student_dataset()
    train, valid, test = split_data(rows)
    print(f"jumlah data: total={len(rows)} train={len(train)} valid={len(valid)} test={len(test)}")

    majority = majority_label(train)
    print(f"majority label di train: {majority}")

    print_metrics("Validation — majority baseline", valid, predict_majority(valid, majority))

    for threshold in [0.45, 0.38, 0.30]:
        print_metrics(f"Validation — rule model threshold={threshold}", valid, predict_rule(valid, threshold))

    best_threshold = 0.38
    print_metrics("Test final — rule model", test, predict_rule(test, best_threshold))

    print_metrics("Leakage demo — jangan ditiru", test, predict_leaky(test))

    baseline_value = regression_mean_baseline(train)
    actual_next = [float(row["next_quiz_score"]) for row in test]
    pred_next = [baseline_value for _ in test]
    print("\nRegression mean baseline")
    print("------------------------")
    print(f"prediksi konstan: {baseline_value:.2f}")
    print(f"MAE test: {mae(pred_next, actual_next):.3f}")

    show_error_examples(test, predict_rule(test, best_threshold))

    print("\nCatatan: leakage demo terlihat bagus karena memakai fitur yang sebenarnya tidak boleh tersedia saat prediksi.")


if __name__ == "__main__":
    main()
